Appearance
loader 的流程
loader 的使用
在 webpack 中有两种方式去触发资源的 loader 方式。
- webpack 中的配置信息
js
module.exports = {
module: {
rules: [
{
// 使用正则去匹配
test: /\.styl$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {},
},
{
loader: 'postcss-loader',
options: {
ident: 'postcss',
plugins: [
require('postcss-cssnext')(),
require('cssnano')(),
require('postcss-pxtorem')({
rootValue: 16,
unitPrecision: 5,
propWhiteList: [],
}),
require('postcss-sprites')(),
],
},
},
{
loader: 'stylus-loader',
options: {},
},
],
},
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'happypack/loader?id=css-pack'],
},
{
test: /\.(png|jpg)$/,
use: ['happypack/loader?id=image'],
},
{
test: /\.js$/,
// 将对.js文件的处理转交给id为babel的HappyPack的实列
use: ['happypack/loader?id=babel'],
// loader: 'babel-loader',
exclude: path.resolve(__dirname, 'node_modules'), // 排除文件
},
],
},
};
- 内联的方式去加载资源
js
import 'style-loader!css-loader?modules!@/css/css.css';
匹配的流程
从前面的 make 的流程中我们知道所有的资源都是通过一个 XXXModuleFactory 工厂函数去创建的, 那么在这个工厂函数实例化的时候
js
class NormalModuleFactory extends Tapable {
constructor(context, resolverFactory, options) {
super();
//
this.ruleSet = new RuleSet(options.defaultRules.concat(options.rules));
}
}
使用默认的 Rules 规则和 用户的自定义的 rules 去 new RuleSet()
一个 ruleSet 实例对象. 那么我们看一下 new RuleSet()
的过程
js
module.exports = class RuleSet {
constructor(rules) {
this.references = Object.create(null);
// 将所有的rule通过normalizeRules进行处理
this.rules = RuleSet.normalizeRules(rules, this.references, 'ref-');
}
/**
* 对rule进行扁平化处理
* @param {[type]} rules [description]
* @param {[type]} refs [description]
* @param {[type]} ident [description]
* @return {[type]} [description]
*/
static normalizeRules(rules, refs, ident) {
if (Array.isArray(rules)) {
return rules.map((rule, idx) => {
return RuleSet.normalizeRule(rule, refs, `${ident}-${idx}`);
});
} else if (rules) {
return [RuleSet.normalizeRule(rules, refs, ident)];
} else {
return [];
}
}
static normalizeRule(rule, refs, ident) {
if (typeof rule === 'string') {
return {
use: [
{
loader: rule,
},
],
};
}
if (!rule) {
throw new Error('Unexcepted null when object was expected as rule');
}
if (typeof rule !== 'object') {
throw new Error('Unexcepted ' + typeof rule + ' when object was expected as rule (' + rule + ')');
}
const newRule = {};
let useSource;
let resourceSource;
let condition;
const checkUseSource = newSource => {
if (useSource && useSource !== newSource) {
throw new Error(
RuleSet.buildErrorMessage(
rule,
new Error('Rule can only have one result source (provided ' + newSource + ' and ' + useSource + ')')
)
);
}
useSource = newSource;
};
const checkResourceSource = newSource => {
if (resourceSource && resourceSource !== newSource) {
throw new Error(
RuleSet.buildErrorMessage(
rule,
new Error('Rule can only have one resource source (provided ' + newSource + ' and ' + resourceSource + ')')
)
);
}
resourceSource = newSource;
};
// 处理 test、include、exclude
//
if (rule.test || rule.include || rule.exclude) {
checkResourceSource('test + include + exclude');
// 将其保存在condition条件属性对象上
condition = {
test: rule.test,
include: rule.include,
exclude: rule.exclude,
};
try {
// 生成 resource 的匹配函数 如 test: /.css$/ => newRule.resource = str => /.css$/.test.bind( /.css$/);
newRule.resource = RuleSet.normalizeCondition(condition);
} catch (error) {
throw new Error(RuleSet.buildErrorMessage(condition, error));
}
}
// 处理 Rule.reource 生成如同 test 的条件匹配
if (rule.resource) {
checkResourceSource('resource');
try {
newRule.resource = RuleSet.normalizeCondition(rule.resource);
} catch (error) {
throw new Error(RuleSet.buildErrorMessage(rule.resource, error));
}
}
if (rule.realResource) {
try {
newRule.realResource = RuleSet.normalizeCondition(rule.realResource);
} catch (error) {
throw new Error(RuleSet.buildErrorMessage(rule.realResource, error));
}
}
// 跟test的过程一样
if (rule.resourceQuery) {
try {
newRule.resourceQuery = RuleSet.normalizeCondition(rule.resourceQuery);
} catch (error) {
throw new Error(RuleSet.buildErrorMessage(rule.resourceQuery, error));
}
}
//
if (rule.compiler) {
try {
newRule.compiler = RuleSet.normalizeCondition(rule.compiler);
} catch (error) {
throw new Error(RuleSet.buildErrorMessage(rule.compiler, error));
}
}
if (rule.issuer) {
try {
newRule.issuer = RuleSet.normalizeCondition(rule.issuer);
} catch (error) {
throw new Error(RuleSet.buildErrorMessage(rule.issuer, error));
}
}
if (rule.loader && rule.loaders) {
throw new Error(
RuleSet.buildErrorMessage(rule, new Error('Provided loader and loaders for rule (use only one of them)'))
);
}
const loader = rule.loaders || rule.loader;
if (typeof loader === 'string' && !rule.options && !rule.query) {
checkUseSource('loader');
newRule.use = RuleSet.normalizeUse(loader.split('!'), ident);
} else if (typeof loader === 'string' && (rule.options || rule.query)) {
checkUseSource('loader + options/query');
newRule.use = RuleSet.normalizeUse(
{
loader: loader,
options: rule.options,
query: rule.query,
},
ident
);
} else if (loader && (rule.options || rule.query)) {
throw new Error(
RuleSet.buildErrorMessage(
rule,
new Error('options/query cannot be used with loaders (use options for each array item)')
)
);
} else if (loader) {
checkUseSource('loaders');
newRule.use = RuleSet.normalizeUse(loader, ident);
} else if (rule.options || rule.query) {
throw new Error(
RuleSet.buildErrorMessage(rule, new Error('options/query provided without loader (use loader + options)'))
);
}
if (rule.use) {
checkUseSource('use');
newRule.use = RuleSet.normalizeUse(rule.use, ident);
}
if (rule.rules) {
newRule.rules = RuleSet.normalizeRules(rule.rules, refs, `${ident}-rules`);
}
if (rule.oneOf) {
newRule.oneOf = RuleSet.normalizeRules(rule.oneOf, refs, `${ident}-oneOf`);
}
const keys = Object.keys(rule).filter(key => {
return ![
'resource',
'resourceQuery',
'compiler',
'test',
'include',
'exclude',
'issuer',
'loader',
'options',
'query',
'loaders',
'use',
'rules',
'oneOf',
].includes(key);
});
for (const key of keys) {
newRule[key] = rule[key];
}
if (Array.isArray(newRule.use)) {
for (const item of newRule.use) {
if (item.ident) {
refs[item.ident] = item.options;
}
}
}
return newRule;
}
static buildErrorMessage(condition, error) {
const conditionAsText = JSON.stringify(
condition,
(key, value) => {
return value === undefined ? 'undefined' : value;
},
2
);
return error.message + ' in ' + conditionAsText;
}
static normalizeUse(use, ident) {
if (typeof use === 'function') {
return data => RuleSet.normalizeUse(use(data), ident);
}
if (Array.isArray(use)) {
return use
.map((item, idx) => RuleSet.normalizeUse(item, `${ident}-${idx}`))
.reduce((arr, items) => arr.concat(items), []);
}
return [RuleSet.normalizeUseItem(use, ident)];
}
static normalizeUseItemString(useItemString) {
const idx = useItemString.indexOf('?');
if (idx >= 0) {
return {
loader: useItemString.substr(0, idx),
options: useItemString.substr(idx + 1),
};
}
return {
loader: useItemString,
options: undefined,
};
}
static normalizeUseItem(item, ident) {
if (typeof item === 'string') {
return RuleSet.normalizeUseItemString(item);
}
const newItem = {};
if (item.options && item.query) {
throw new Error('Provided options and query in use');
}
if (!item.loader) {
throw new Error('No loader specified');
}
newItem.options = item.options || item.query;
if (typeof newItem.options === 'object' && newItem.options) {
if (newItem.options.ident) {
newItem.ident = newItem.options.ident;
} else {
newItem.ident = ident;
}
}
const keys = Object.keys(item).filter(function(key) {
return !['options', 'query'].includes(key);
});
for (const key of keys) {
newItem[key] = item[key];
}
return newItem;
}
/**
* 处理条件类型的属性 如 test include
* @param {[type]} condition [description]
* @return {[type]} [description]
*/
static normalizeCondition(condition) {
if (!condition) throw new Error('Expected condition but got falsy value');
// 如果是单个条件的 直接生成 str => str.indexOf(condition) === 0 函数
if (typeof condition === 'string') {
return str => str.indexOf(condition) === 0;
}
if (typeof condition === 'function') {
return condition;
}
// 正则类型的 /\.css$/ 生成 /\.css$/.test方法
if (condition instanceof RegExp) {
return condition.test.bind(condition);
}
if (Array.isArray(condition)) {
const items = condition.map(c => RuleSet.normalizeCondition(c));
return orMatcher(items);
}
// 其他基础类型不支持
if (typeof condition !== 'object') {
throw Error('Unexcepted ' + typeof condition + ' when condition was expected (' + condition + ')');
}
const matchers = [];
// 对于对象类型的 如 test , include enclude其会保存在同一个condition : {}上
Object.keys(condition).forEach(key => {
const value = condition[key];
switch (key) {
case 'or':
case 'include':
case 'test':
// 如果存在test 那么就会在matchers = [ condition.test.bind(condition) , ]
if (value) matchers.push(RuleSet.normalizeCondition(value));
break;
case 'and':
// 处理and条件 如
if (value) {
const items = value.map(c => RuleSet.normalizeCondition(c));
matchers.push(andMatcher(items));
}
break;
case 'not':
case 'exclude':
if (value) {
const matcher = RuleSet.normalizeCondition(value);
matchers.push(notMatcher(matcher));
}
break;
default:
throw new Error('Unexcepted property ' + key + ' in condition');
}
});
if (matchers.length === 0) {
throw new Error('Excepted condition but got ' + condition);
}
if (matchers.length === 1) {
return matchers[0];
}
return andMatcher(matchers);
}
};
上面长长的一大坨的过程就是将配置的 rule 处理成一个扁平化的 rule 规则
js
this.ruleSet = [
{ type: 'javascript/auto', resolve: {} },
{ type: 'javascript/esm', resolve: { mainFields: ['browser', 'main'] } },
{ type: 'json' },
{ type: 'webassembly/experimental' },
{
use: [
{ loader: 'd:\\guzhangh\\webpack\\webpack\\node_modules\\mini-css-extract-plugin\\dist\\loader.js' },
{ options: {}, ident: 'ref--4-1', loader: 'css-loader' },
{
options: {
ident: 'postcss',
plugins: [
{
version: '6.0.23',
plugins: [null],
postcssPlugin: 'postcss-cssnext',
postcssVersion: '6.0.23',
},
null,
null,
null,
],
},
ident: 'postcss',
loader: 'postcss-loader',
},
{ options: {}, ident: 'ref--4-3', loader: 'stylus-loader' },
],
},
{
use: [
{ loader: 'd:\\guzhangh\\webpack\\webpack\\node_modules\\mini-css-extract-plugin\\dist\\loader.js' },
{ loader: 'happypack/loader', options: 'id=css-pack' },
],
},
{ use: [{ loader: 'happypack/loader', options: 'id=image' }] },
{ use: [{ loader: 'happypack/loader', options: 'id=babel' }] },
];
然后我们在 make 流程的 resolve 之后,但是在真正的resolve之前,首先需要通过匹配过滤找到这个module所对应的所有的loaders。
其先处理 inline loaders
js
class NormalModuleFactory extends Tapable {
constructor(context, resolverFactory, options) {
super();
this.hooks.resolver.tap('NormalModuleFactory', () => (data, callback) => {
const contextInfo = data.contextInfo;
const context = data.context;
const request = data.request;
const loaderResolver = this.getResolver('loader');
const normalResolver = this.getResolver('normal', data.resolveOptions);
let matchResource = undefined;
let requestWithoutMatchResource = request;
const matchResourceMatch = MATCH_RESOURCE_REGEX.exec(request);
if (matchResourceMatch) {
matchResource = matchResourceMatch[1];
if (/^\.\.?\//.test(matchResource)) {
matchResource = path.join(context, matchResource);
}
requestWithoutMatchResource = request.substr(matchResourceMatch[0].length);
}
const noPreAutoLoaders = requestWithoutMatchResource.startsWith('-!');
const noAutoLoaders = noPreAutoLoaders || requestWithoutMatchResource.startsWith('!');
const noPrePostAutoLoaders = requestWithoutMatchResource.startsWith('!!');
let elements = requestWithoutMatchResource
.replace(/^-?!+/, '')
.replace(/!!+/g, '!')
.split('!');
let resource = elements.pop();
elements = elements.map(identToLoaderRequest);
asyncLib.parallel(
[
callback => this.resolveRequestArray(contextInfo, context, elements, loaderResolver, callback),
callback => {
if (resource === '' || resource[0] === '?') {
return callback(null, {
resource,
});
}
normalResolver.resolve(contextInfo, context, resource, {}, (err, resource, resourceResolveData) => {
if (err) return callback(err);
callback(null, {
resourceResolveData,
resource,
});
});
},
],
(err, results) => {
if (err) return callback(err);
let loaders = results[0];
const resourceResolveData = results[1].resourceResolveData;
resource = results[1].resource;
// translate option idents
try {
for (const item of loaders) {
if (typeof item.options === 'string' && item.options[0] === '?') {
const ident = item.options.substr(1);
item.options = this.ruleSet.findOptionsByIdent(ident);
item.ident = ident;
}
}
} catch (e) {
return callback(e);
}
if (resource === false) {
// ignored
return callback(
null,
new RawModule('/* (ignored) */', `ignored ${context} ${request}`, `${request} (ignored)`)
);
}
const userRequest =
(matchResource !== undefined ? `${matchResource}!=!` : '') +
loaders
.map(loaderToIdent)
.concat([resource])
.join('!');
let resourcePath = matchResource !== undefined ? matchResource : resource;
let resourceQuery = '';
const queryIndex = resourcePath.indexOf('?');
if (queryIndex >= 0) {
resourceQuery = resourcePath.substr(queryIndex);
resourcePath = resourcePath.substr(0, queryIndex);
}
const result = this.ruleSet.exec({
resource: resourcePath,
realResource: matchResource !== undefined ? resource.replace(/\?.*/, '') : resourcePath,
resourceQuery,
issuer: contextInfo.issuer,
compiler: contextInfo.compiler,
});
const settings = {};
const useLoadersPost = [];
const useLoaders = [];
const useLoadersPre = [];
for (const r of result) {
if (r.type === 'use') {
if (r.enforce === 'post' && !noPrePostAutoLoaders) {
useLoadersPost.push(r.value);
} else if (r.enforce === 'pre' && !noPreAutoLoaders && !noPrePostAutoLoaders) {
useLoadersPre.push(r.value);
} else if (!r.enforce && !noAutoLoaders && !noPrePostAutoLoaders) {
useLoaders.push(r.value);
}
} else if (
typeof r.value === 'object' &&
r.value !== null &&
typeof settings[r.type] === 'object' &&
settings[r.type] !== null
) {
settings[r.type] = cachedCleverMerge(settings[r.type], r.value);
} else {
settings[r.type] = r.value;
}
}
asyncLib.parallel(
[
this.resolveRequestArray.bind(this, contextInfo, this.context, useLoadersPost, loaderResolver),
this.resolveRequestArray.bind(this, contextInfo, this.context, useLoaders, loaderResolver),
this.resolveRequestArray.bind(this, contextInfo, this.context, useLoadersPre, loaderResolver),
],
(err, results) => {
if (err) return callback(err);
if (matchResource === undefined) {
loaders = results[0].concat(loaders, results[1], results[2]);
} else {
loaders = results[0].concat(results[1], loaders, results[2]);
}
process.nextTick(() => {
const type = settings.type;
const resolveOptions = settings.resolve;
callback(null, {
context: context,
request: loaders
.map(loaderToIdent)
.concat([resource])
.join('!'),
dependencies: data.dependencies,
userRequest,
rawRequest: request,
loaders,
resource,
matchResource,
resourceResolveData,
settings,
type,
parser: this.getParser(type, settings.parser),
generator: this.getGenerator(type, settings.generator),
resolveOptions,
});
});
}
);
}
);
});
}
}