进阶开发,跟我一起拿捏webpack loader原理
前言
上一篇是日常开发,我该掌握哪些webpack loader知识,这一篇主要从源码层面分析,webpack
内部是怎么控制loader
执行,让我们不仅知道怎么用,还能知道为什么这么用,最后还会分析常用loader
的内部实现原理,帮助我们更好的在项目中使用loader
loader 执行原理
loader
存在多种形式,比如inline loader
、pre loader
、post loader
等,这些loader
在执行的时候会按照指定的顺序执行,那么我们来看下webpack
内部是怎样获取到资源匹配的loader
,及怎么控制获取到的loader
执行,以webpack
处理js
文件为例,主要分为两步
- 第一步:
webpack
在构建时候,首先js
文件会从资源链接上获取inline loader
,然后在根据rules
匹配到对应的rule
,在根据rule
获取loader
配置,最后根据pre
、post
、inline
、normal
按照一定的规则组合loader
数组 - 第二步:将上一步组合之后的
loader
数组,传递给loader-runner
,loader-runner
内部控制loader
数组执行
简单概括如下图所示
下面将分别介绍
webpack
组装loaders
数组原理loader-runner
执行loaders
数组原理- 同步与异步
loader
原理 picth loader
阻断原理raw loader
原理
webpack组装loaders数组原理
webpack
内部获取loaders
的原理,以normalModule
为例:
- 在
compilcation.resolve hook
上注册callback
,在这个callback
内开始获取匹配当前module
的loaders
- 先获取
inline loader
,然后在通过resolveRequestArray
方法将匹配到的loader
转换为loader
对象 - 在通过
this.ruleSet.exec
获取rules
中配置的loader
,包括pre loader
、normal loader
、post loader
- 然后在通过
resolveRequestArray
方法将匹配到的normal loader
等转换为loader
对象 - 在然后将获取到的所有
loader
按照post
、inline
、normal
、pre
的顺序进行排列 - 最后
runLoads
执行的时候传入的loaders
数组执行loader
转换逻辑
流程图如下所示
this.hooks.resolve.tapAsync(
{
name: "NormalModuleFactory",
stage: 100
},
(data, callback) => {
...
const loaderResolver = this.getResolver("loader");
const firstChar = requestWithoutMatchResource.charCodeAt(0);
const secondChar = requestWithoutMatchResource.charCodeAt(1);
// 判断line loader的三种前缀
noPreAutoLoaders = firstChar === 45 && secondChar === 33; // startsWith "-!"
noAutoLoaders = noPreAutoLoaders || firstChar === 33; // startsWith "!"
noPrePostAutoLoaders = firstChar === 33 && secondChar === 33; // startsWith "!!";
const rawElements = requestWithoutMatchResource
.slice(
noPreAutoLoaders || noPrePostAutoLoaders
? 2
: noAutoLoaders
? 1
: 0
)
.split(/!+/);
unresolvedResource = rawElements.pop();
// 获取inline loader
elements = rawElements.map(el => {
const { path, query } = cachedParseResourceWithoutFragment(el);
return {
loader: path,
options: query ? query.slice(1) : undefined
};
});
// 将inline loader转化为内部loader对象,包含loader的绝对路径及option
this.resolveRequestArray(
elements,
loaderResolver
);
// 调用this.ruleSet.exec根据resourceData.path及loader的test等参数匹配符合的loader
const result = this.ruleSet.exec({
resource: resourceDataForRules.path,
realResource: resourceData.path,
resourceQuery: resourceDataForRules.query,
...
});
// 将上一步匹配到的loader,分布放到对应的数组中,便于组合loader的顺序
for (const r of result) {
if (r.type === "use") {
// 配置中的normal loader,在inline loader的逻辑那里会处理noAutoLoaders、noPrePostAutoLoaders、noPreAutoLoaders的值
if (!noAutoLoaders && !noPrePostAutoLoaders) {
useLoaders.push(r.value);
}
} else if (r.type === "use-post") {
if (!noPrePostAutoLoaders) {
// 配置中的post loader
useLoadersPost.push(r.value);
}
} else if (r.type === "use-pre") {
if (!noPreAutoLoaders && !noPrePostAutoLoaders) {
// // 配置中的pre loader
useLoadersPre.push(r.value);
}
}
}
let postLoaders, normalLoaders, preLoaders;
const continueCallback = needCalls(3, err => {
if (err) {
return callback(err);
}
// 为了保证loader的执行顺序,所以loader的数组顺序为[postLoaders, loaders, normalLoaders, preLoaders] 这里的loader是inline loader
// 先赋值postLoaders,保证postLoaders在最前面
const allLoaders = postLoaders;
// 处理inline loader
for (const loader of loaders) allLoaders.push(loader);
// 处理normal loader
for (const loader of normalLoaders) allLoaders.push(loader);
// 保证preLoaders在数组的最后
for (const loader of preLoaders) allLoaders.push(loader);
try {
// resolveData.createData做对象合并
Object.assign(data.createData, {
...
loaders: allLoaders,
});
} catch (e) {
return callback(e);
}
callback();
});
// 将post loader转换为内部loader对象
this.resolveRequestArray(
useLoadersPost,
loaderResolver
);
// 将normal loader转换为内部loader对象
this.resolveRequestArray(
useLoaders,
loaderResolver
);
// 将pre loader转换为内部loader对象
this.resolveRequestArray(
useLoadersPre,
loaderResolver
);
);
// 将inline loader、pre loader等转化为绝对路径的loader
resolveRequestArray(
contextInfo,
context,
array,
resolver,
) {
asyncLib.map(
array,
(item, callback) => {
// 调用loaderResolver.resolve方法查找loader文件
resolver.resolve(
contextInfo,
context,
item.loader,
resolveContext,
(err, result) => {
const parsedResult = this._parseResourceWithoutFragment(result);
// 格式化loader,将每个loader转化成下面的对象形式
const resolved = {
loader: parsedResult.path,
options: item.options,
ident: item.options === undefined ? undefined : item.ident
};
return callback(null, resolved);
}
);
},
callback
);
}
loader-runner执行loaders数组原理
webpack
将loader
的执行,单独封装了一个loader-runner的库,专门来执行loader
相关的逻辑,这里以loader-runner@4.3.0
的源代码为例
run loader
核心原理就是
- 将
webpack
传入的loaders
数组,转换成对象数组 - 然后控制
loaderIndex
从0-loaders.length - 1
顺序调用pitch loader
,通过判断pitch loader
是否返回非undefiend
的值,如果是则跳过后续loader
执行,否则继续执行picth loader
的执行 picth loader
执行完之后,先获取module source
,然后在开始按照loaderIndex
从loaders.length - 1 => 0
的顺序依次调用normal loader
,当所有normal loader
调用完毕之后,调用run loader
传入的callback
,表示当前模块对应的所有loader
执行完毕
流程图如下图所示
exports.runLoaders = function runLoaders(options, callback) {
// read options
var processResource = options.processResource || ((readResource, context, resource, callback) => {
context.addDependency(resource);
readResource(resource, callback);
}).bind(null, options.readResource || readFile);
// 初始化loaders,将webpack传入的loaders数组,转换成内部的obj数组对象
loaders = loaders.map(createLoaderObject);
// loader执行下标,loader的执行顺序,全依赖该loaderIndex
loaderContext.loaderIndex = 0;
loaderContext.loaders = loaders;
// 省略部分代码...
var processOptions = {
resourceBuffer: null,
processResource: processResource
};
// 先执行pitch loader
iteratePitchingLoaders(processOptions, loaderContext, function(err, result) {
if(err) {
return callback(err);
}
callback(null, {
result: result,
});
});
};
function iteratePitchingLoaders(options, loaderContext, callback) {
// 如果所有的pitch loader执行完了,就读取module source,然后执行normal loader
if(loaderContext.loaderIndex >= loaderContext.loaders.length)
return processResource(options, loaderContext, callback);
// 从0 -> loaderContext.loaders.length 执行picth loader
var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
// 如果当前pitch loader执行完了,则继续递归调用iteratePitchingLoaders
if(currentLoaderObject.pitchExecuted) {
loaderContext.loaderIndex++;
return iteratePitchingLoaders(options, loaderContext, callback);
}
// 获取loader模块暴露的属性,比如picth loader function
loadLoader(currentLoaderObject, function(err) {
if(err) {
return callback(err);
}
var fn = currentLoaderObject.pitch;
currentLoaderObject.pitchExecuted = true;
// 如果当前loader没有暴露picth loader,则继续递归执行iteratePitchingLoaders
if(!fn) return iteratePitchingLoaders(options, loaderContext, callback);
// 如果当前loader暴露了picth loader,则使用runSyncOrAsync函数调用picth loader function
runSyncOrAsync(
fn,
// 构造传入picth loader的参数
loaderContext, [loaderContext.remainingRequest, loaderContext.previousRequest, currentLoaderObject.data = {}],
function(err) {
if(err) return callback(err);
var args = Array.prototype.slice.call(arguments, 1);
// 如果调用的picth loader function 返回了非undefined的值,则loaderIndex--,并开始调用normal loader,否则继续递归调用iteratePitchingLoaders
var hasArg = args.some(function(value) {
return value !== undefined;
});
if(hasArg) {
loaderContext.loaderIndex--;
iterateNormalLoaders(options, loaderContext, args, callback);
} else {
iteratePitchingLoaders(options, loaderContext, callback);
}
}
);
});
}
function runSyncOrAsync(fn, context, args, callback) {
var isSync = true;
var isDone = false;
var isError = false; // internal error
var reportedError = false;
// 在loader context 上暴露this.async方法,调用该方法返回一个callback function
context.async = function async() {
if(isDone) {
if(reportedError) return; // ignore
throw new Error("async(): The callback was already called.");
}
isSync = false;
return innerCallback;
};
// 处理this.callback调用场景
var innerCallback = context.callback = function() {
if(isDone) {
if(reportedError) return; // ignore
throw new Error("callback(): The callback was already called.");
}
isDone = true;
isSync = false;
try {
callback.apply(null, arguments);
} catch(e) {
isError = true;
throw e;
}
};
try {
// 调用picth loader function or normal loader function
var result = (function LOADER_EXECUTION() {
return fn.apply(context, args);
}());
// 处理loader内直接return source 及 return Promise<source>
if(isSync) {
isDone = true;
if(result === undefined)
return callback();
if(result && typeof result === "object" && typeof result.then === "function") {
return result.then(function(r) {
callback(null, r);
}, callback);
}
return callback(null, result);
}
} catch(e) {
...
callback(e);
}
}
function processResource(options, loaderContext, callback) {
// 调用normal loader之前将loaderIndex重置为loaderContext.loaders.length - 1,保证normal loader的执行顺序
loaderContext.loaderIndex = loaderContext.loaders.length - 1;
var resourcePath = loaderContext.resourcePath;
if(resourcePath) {
// 获取module source
options.processResource(loaderContext, resourcePath, function(err) {
if(err) return callback(err);
var args = Array.prototype.slice.call(arguments, 1);
options.resourceBuffer = args[0];
// 调用normal loader处理module source
iterateNormalLoaders(options, loaderContext, args, callback);
});
} else {
iterateNormalLoaders(options, loaderContext, [null], callback);
}
}
function iterateNormalLoaders(options, loaderContext, args, callback) {
// 如果loaderIndex小于0,表示所有normal loader执行完毕,应该直接调用run loader callback
if(loaderContext.loaderIndex < 0)
return callback(null, args);
var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
// 如果当前normal loader执行过了,则loaderIndex--之后继续递归调用iterateNormalLoaders
if(currentLoaderObject.normalExecuted) {
loaderContext.loaderIndex--;
return iterateNormalLoaders(options, loaderContext, args, callback);
}
var fn = currentLoaderObject.normal;
currentLoaderObject.normalExecuted = true;
if(!fn) {
return iterateNormalLoaders(options, loaderContext, args, callback);
}
// 通过loader暴露的raw字段,决定当前loader接受的source是string类型还是buffer类型
convertArgs(args, currentLoaderObject.raw);
// 通过runSyncOrAsync执行normal loader function
runSyncOrAsync(fn, loaderContext, args, function(err) {
if(err) return callback(err);
var args = Array.prototype.slice.call(arguments, 1);
// 继续递归调用iterateNormalLoaders
iterateNormalLoaders(options, loaderContext, args, callback);
});
}
同步与异步loader的原理
- 如果是异步
loader
,在loader
内调用this.async
时会将isSync
设置为false
,避免被当成同步loader
处理,最后会返回一个innerCallback
,而这个innerCallback
调用的时候,我们会传入处理后的参数,在innerCallback
内会调用runSyncOrAsync
传入的callback
- 如果是同步
loader
,则直接调用runSyncOrAsync
传入的callback
context.async = function async() {
if(isDone) {
if(reportedError) return; // ignore
throw new Error("async(): The callback was already called.");
}
isSync = false;
return innerCallback;
};
// 处理this.callback调用场景
var innerCallback = context.callback = function() {
if(isDone) {
if(reportedError) return; // ignore
throw new Error("callback(): The callback was already called.");
}
isDone = true;
isSync = false;
try {
callback.apply(null, arguments);
} catch(e) {
isError = true;
throw e;
}
};
// 调用picth方法或者normal方法
var result = (function LOADER_EXECUTION() {
return fn.apply(context, args);
}());
// 如果是同步loader,也就是未调用this.async方法的场景,则会直接调用runSyncOrAsync传入的callback
if(isSync) {
isDone = true;
if(result === undefined)
return callback();
if(result && typeof result === "object" && typeof result.then === "function") {
return result.then(function(r) {
callback(null, r);
}, callback);
}
return callback(null, result);
}
// 异步loader
module.exports = function (content) {
// 异步loader
const callback = this.async() // 获取一个callback具柄
asyncOpration(xxxx, (result) => {
// 必须要调用callback,告诉runLoader当前loader跳用完成了
callback(null, result)
})
}
// 同步loader
module.exports = function (content) {
// 直接返回
return content
}
module.exports = function (content) {
// 调用this.callback返回
this.callback(null, content)
}
picth loader阻断原理
runLoaders
会按照loaders
数组的顺序,先执行picth loader
- 当
picth
方法执行完之后,会判断返回值是否为undefined
,如果是非undefined
的值,就loaderIndex--
,并开始调用iterateNormalLoaders
开始执行normal loader
的逻辑
执行示例
use: ['a-loader', 'b-loader', 'c-loader'],
// 假设b-loader有picth方法,并返回非undefined的值,那么最终的执行顺序如下所示
|- a-loader `pitch`
|- b-loader `pitch` returns a module
|- a-loader normal execution
精简后的picth loader阻断原理代码
exports.runLoaders = function runLoaders(options, callback) {
...
// 先执行pitch loader
iteratePitchingLoaders(processOptions, loaderContext, function(err, result) {
if(err) {
return callback(err);
}
callback(null, {
result: result,
});
});
};
function iteratePitchingLoaders(options, loaderContext, callback) {
// 获取loader模块暴露的属性,比如picth loader function
loadLoader(currentLoaderObject, function(err) {
var fn = currentLoaderObject.pitch;
// 如果当前loader暴露了picth loader,则使用runSyncOrAsync函数调用picth loader function
runSyncOrAsync(
fn,
// 构造传入picth loader的参数
loaderContext, [loaderContext.remainingRequest, loaderContext.previousRequest, currentLoaderObject.data = {}],
function(err) {
var args = Array.prototype.slice.call(arguments, 1);
// 如果调用的picth loader function 返回了非undefined的值,则loaderIndex--,并开始调用normal loader,否则继续递归调用iteratePitchingLoaders
var hasArg = args.some(function(value) {
return value !== undefined;
});
if(hasArg) {
loaderContext.loaderIndex--;
iterateNormalLoaders(options, loaderContext, args, callback);
} else {
iteratePitchingLoaders(options, loaderContext, callback);
}
}
);
});
}
raw loader原理
- 只对
normal loader
生效,根据暴露的loader
模块上是否有raw
属性来做处理 - 如果
raw
为true
则将内容处理成buffer
类型,否则处理成string
类型
// 如果raw为true则将内容处理成buffer类型,如果raw为false,则将内容处理成string类型
function convertArgs(args, raw) {
// 如果没有设置raw,且内容是buffer,则转化成string
if(!raw && Buffer.isBuffer(args[0]))
args[0] = utf8BufferToString(args[0]);
// 如果设置raw,且内容是string,则转化成buffer
else if(raw && typeof args[0] === "string")
args[0] = Buffer.from(args[0], "utf-8");
}
// 执行normal loader逻辑
function iterateNormalLoaders(options, loaderContext, args, callback) {
// 通过loader暴露的raw字段,决定当前loader接受的source是string类型还是buffer类型
convertArgs(args, currentLoaderObject.raw);
// 通过runSyncOrAsync执行normal loader function
runSyncOrAsync(fn, loaderContext, args, function(err) {
if(err) return callback(err);
var args = Array.prototype.slice.call(arguments, 1);
// 继续递归调用iterateNormalLoaders
iterateNormalLoaders(options, loaderContext, args, callback);
});
}
常用loader及原理
了解那么多,当然需要看下实际项目中使用的loader
都是怎么实现,方便我们更近一步使用与理解loader
babel-loader@8.2.5
babel-loader
可以说是我们用的最多的loader
之一了,主要作用将ts
语法转换成js
语法、es6+
语法转换成es5
语法等
内部处理流程如下图所示
async function loader(source, inputSourceMap, overrides) {
const filename = this.resourcePath;
let loaderOptions = loaderUtils.getOptions(this);
// 获取babel加载config的function
const { loadPartialConfigAsync = babel.loadPartialConfig } = babel;
// 获取babel config
const config = await loadPartialConfigAsync(
injectCaller(programmaticOptions, this.target),
);
let result;
// 如果当前module存在缓存,则直接返回,否则,使用babel.transform进行处理
if (cacheDirectory) {
result = await cache({source});
} else {
result = await transform(source, options);
}
if (result) {
// 将babel获取到的代码,return 出去
const { code, map, metadata } = result;
return [code, map];
}
}
从源码可以看到babel-loader
是一个normal loader
,内部是同步返回babel
处理之后的内容
less-loader@11.0.0
less-loader
也是我们用的比较多的一个loader
,专门用来处理less
样式
内部处理流程如下图所示
async function lessLoader(source) {
const options = this.getOptions(schema);
// 调用this.async,使用异步 loader的方式返回result
const callback = this.async();
const lessOptions = getLessOptions(this, options, implementation);
let data = source;
let result;
try {
// 调用less.render方法将less语法转换为css语法
result = await implementation.render(data, lessOptions);
} catch (error) {
callback(new LessError(error));
return;
}
const { css, imports } = result;
// 如果less文件内部有@import这样的语法,那么将@import 的file,通过this.addDependency传入,让webpack能够监视到文件变化
imports.forEach((item) => {
// Custom `importer` can return only `contents` so item will be relative
if (path.isAbsolute(normalizedItem)) {
this.addDependency(normalizedItem);
}
});
let map = typeof result.map === "string" ? JSON.parse(result.map) : result.map;
// 将less处理的结果返回
callback(null, css, map);
}
export default lessLoader;
从源码可以看到less-loader
是一个normal loader
,内部是异步返回less
处理之后的内容
css-loader@6.7.4
css-loader
是基础loader
,专门用来处理css
样式,将css
模块转化为webpack
能够处理的内容
内部处理流程如下图所示
export default async function loader(content, map, meta) {
const rawOptions = this.getOptions(schema);
const callback = this.async();
// Reuse CSS AST (PostCSS AST e.g 'postcss-loader') to avoid reparsing
if (meta) {
const { ast } = meta;
if (
ast &&
ast.type === "postcss" &&
satisfies(ast.version, `^${postcssPkg.version}`)
) {
// eslint-disable-next-line no-param-reassign
content = ast.root;
}
}
const { resourcePath } = this;
let result;
try {
result = await postcss(plugins).process(content, {});
} catch (error) {
return;
}
const imports = []
.concat(icssPluginImports.sort(sort))
.concat(importPluginImports.sort(sort))
.concat(urlPluginImports.sort(sort));
const api = []
.concat(importPluginApi.sort(sort))
.concat(icssPluginApi.sort(sort));
const importCode = getImportCode(imports, options);
let moduleCode;
try {
moduleCode = getModuleCode(result, api, replacements, options, this);
} catch (error) {
callback(error);
return;
}
const exportCode = getExportCode(
exports,
replacements,
needToUseIcssPlugin,
options
);
callback(null, `${importCode}${moduleCode}${exportCode}`);
}
从源码可以看到css-loader
是一个normal loader
,内部是同步返回postcss
处理之后的内容
css module
格式, export default
的就是一个对象,对象内包含的就是我们的key
还有样式的样式的新key
// Imports
import ___CSS_LOADER_API_NO_SOURCEMAP_IMPORT___ from "../node_modules/.pnpm/css-loader@6.7.4_webpack@5.74.0/node_modules/css-loader/dist/runtime/noSourceMaps.js";
import ___CSS_LOADER_API_IMPORT___ from "../node_modules/.pnpm/css-loader@6.7.4_webpack@5.74.0/node_modules/css-loader/dist/runtime/api.js";
var ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_NO_SOURCEMAP_IMPORT___);
// Module
___CSS_LOADER_EXPORT___.push([module.id, ".DFDoHskQhc8WMFqQHxvM {\n background-color: #ccc;\n}\n", ""]);
// Exports
___CSS_LOADER_EXPORT___.locals = {
"wrap": "DFDoHskQhc8WMFqQHxvM"
};
export default ___CSS_LOADER_EXPORT___;
非css module
格式,export default
就是一个数组
// Imports
import ___CSS_LOADER_API_NO_SOURCEMAP_IMPORT___ from "../node_modules/.pnpm/css-loader@6.7.4_webpack@5.74.0/node_modules/css-loader/dist/runtime/noSourceMaps.js";
import ___CSS_LOADER_API_IMPORT___ from "../node_modules/.pnpm/css-loader@6.7.4_webpack@5.74.0/node_modules/css-loader/dist/runtime/api.js";
var ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_NO_SOURCEMAP_IMPORT___);
// Module
___CSS_LOADER_EXPORT___.push([module.id, "body {\n background-color: #ccc;\n}\n", ""]);
// Exports
export default ___CSS_LOADER_EXPORT___;
从css-loader
我们可以知道import './index.css'
经过css
处理之后,相当于import './index.css.js'
文件,通过export default
的方式导出模块内容
style-loader@3.3.2
style-loader
基础loader
,专门用来处理开发环境css
样式,将css-loader
转化后的内容,通过style
标签等的方式插入到html
中,让最终的样式生效
内部处理流程:
style-loader
是一个picth loader
,内部是同步返回非undefined
的值,以use: ['style-loader', 'css-loader']
为例,那么loader
的执行顺序就是
- picth loader(style-loader)
- 结束,因为style-loader在最左边
所以为了让样式生效,style picth
返回的内容,如下
// 返回的内容内重新引入了css模块文件,并使用inline loader处理
import content, * as namedExport from "!!../node_modules/.pnpm/css-loader@6.7.4_webpack@5.74.0/node_modules/css-loader/dist/cjs.js??ruleSet[1].rules[2].use[1]!./index.css";
var options = {};
options.domAPI = domAPI;
// 对css loader处理后的内容通过,相应的api插入文档
var update = API(content, options);
export default ___CSS_LOADER_EXPORT___;
具体的过程如下所示
第一次处理index.css
:picth style-loader
=> 返回非undefiend
值,所以跳过了后面css-loader
的执行
第二次处理index.css
:因为第一次style-loader picth
返回的内容里面包含import content from !!.css-loader/dist/cjs.js!./index.css
代码,所以会重新处理index.css
模块,而inline-loader
前面使用了!!
,表示禁用rules
中匹配到的的normal loader
与picth loader
,所以只会命中css-loader
流程图如下所示
在webpack
的moduleGrapha
中index.css
会出现有个module
实例
"./src/index.css
模块内容包含style-loader
返回的内容"./node_modules/css-loader/dist/cjs.js!./src/index.css"
包含css
内容及css-loader
提供的部分代码
css
模块最终经过了css-loader
处理,同时又具有了style-loader
插入html
的能力
这就是picth loader
设计的初衷,在不改变原有模块代码及css-loader
的前提下,增强及拓展了css
的能力
const loaderAPI = () => {};
loaderAPI.pitch = function loader(request) {
const options = this.getOptions(_options.default);
const injectType = options.injectType || "styleTag";
const insertType = typeof options.insert === "function" ? "function" : options.insert && _path.default.isAbsolute(options.insert) ? "module-path" : "selector";
const styleTagTransformType = typeof options.styleTagTransform === "function" ? "function" : options.styleTagTransform && _path.default.isAbsolute(options.styleTagTransform) ? "module-path" : "default";
switch (injectType) {
case "styleTag":
case "autoStyleTag":
case "singletonStyleTag":
default:
{
const isSingleton = injectType === "singletonStyleTag";
const isAuto = injectType === "autoStyleTag";
const hmrCode = this.hot ? (0, _utils.getStyleHmrCode)(esModule, this, request, false) : "";
return `
${(0, _utils.getImportStyleContentCode)(esModule, this, request)}
${esModule ? "" : `content = content.__esModule ? content.default : content;`}
var options = ${JSON.stringify(runtimeOptions)};
var update = API(content, options);
${hmrCode}
${(0, _utils.getExportStyleCode)(esModule, this, request)}
`;}
}
};
var _default = loaderAPI;
exports.default = _default;
function getImportStyleContentCode(esModule, loaderContext, request) {
// 使用行内loader处理源样式模块,!!禁用rules中的所有loader
const modulePath = stringifyRequest(loaderContext, `!!${request}`);
return esModule ? `import content, * as namedExport from ${modulePath};` : `var content = require(${modulePath});`;
}
MiniCssExtractPlugin.loader@2.7.5
从上面use: ['style-loader', 'css-loader']
可以看到css
模块已经可以被webpack
正确处理了,但是这样处理有一个问题,样式文件是以style
标签或者link
标签插入的,没有生成单独的css
文件,而在实际生产环境考虑到网络、缓存等问题,会将css
内容提取到一个或者几个文件内,那么这个时候style-loader
就做不到了,需要使用到另外一个loader
,即MiniCssExtractPlugin.loader
关于MiniCssExtractPlugin.loader
的原理可以查看面试官:生产环境构建时为什么要提取css文件?
file-loader@6.2.0
file-loader
webpack5
之前常用的loader
,专门用来处理image
等静态资源文件,现在webpack5
以内置image
、font
等静态资源的处理能力,所以使用没那么频繁,但是file-loader
的内部实现我们还是可以进行了解
内部处理流程如下图所示
export default function loader(content) {
const options = getOptions(this);
const context = options.context || this.rootContext;
const name = options.name || '[contenthash].[ext]';
const url = interpolateName(this, name, {
context,
content,
regExp: options.regExp,
});
let outputPath = url;
let publicPath = `__webpack_public_path__ + ${JSON.stringify(outputPath)}`;
if (typeof options.emitFile === 'undefined' || options.emitFile) {
const assetInfo = {};
assetInfo.sourceFilename = normalizePath(
path.relative(this.rootContext, this.resourcePath)
);
// 将传入的内容当成单独的文件输出,输出路径是outputPath,名称是sourceFilename
this.emitFile(outputPath, content, null, assetInfo);
}
const esModule =
typeof options.esModule !== 'undefined' ? options.esModule : true;
// 将当前资源返回成js能够识别的模块
return `${esModule ? 'export default' : 'module.exports ='} ${publicPath};`;
}
// 当前loader接受的source 接受buffer类型,而不是string类型
export const raw = true;
从源码可以看到file-loader
是一个normal loader
,内部是同步返回file loader
处理之后的内容,但是使用了raw
来表示当前loader
接受的内容,必须是buffer
类型
cache-loader@4.1.0
cache-loader
webpack5
之前常用的loader
,专门用来处理缓存,现在webpack5
已经内置了更完善的缓存策略,所以cache-loader
使用的比较少了,但是cache-loader
的实现,还是还是可以借鉴,因为cache-loader
即即提供了normal loader
又提供了picth loader
内部处理流程:
- 使用
picth loader
读缓存,如果有缓存,则直接返回缓存内容,跳过后续loader
的执行,如果没有缓存,则继续执行后续的loader
- 使用
normal loader
存缓存,只要在picth loader
的时候没有读取到缓存,那么就一定会执行到cache-loader
的normal loader
,然后在normal loader
的时候将module
的依赖及处理当前module
的loader
文件路径存起来,方便缓存复用的时候判断缓存是否有效 - 通过
module
的依赖文件及处理当前module
的loader
文件最后的修改时间是否一致,一致则缓存有效,不一致,则缓存失效 - 因为涉及到文件的读写,所以
pitch loader
与normal loader
都是异步loader
cache-loader
因为是通过文件的最后修改时间来判断缓存是否有效,对于ci
场景每次都是重写拉文件,导致最后修改时间变化,所以需要修改下cache-loader
通过文件md5
的方式来判断缓存是否失效
流程图如下所示
function loader(...args) {
const options = Object.assign({}, defaults, getOptions(this));
// 获取innercallback具柄
const callback = this.async();
const { data } = this;
// 获取依赖项,及处理该依赖的loader
const dependencies = this.getDependencies().concat(
this.loaders.map((l) => l.path)
);
// 获取动态加载的依赖项,及处理该依赖的loader
const contextDependencies = this.getContextDependencies();
const FS = this.fs || fs;
const toDepDetails = (dep, mapCallback) => {
// 获取文件的状态
FS.stat(dep, (err, stats) => {
// 获取文件的修改时间
const mtime = stats.mtime.getTime();
mapCallback(null, {
path: pathWithCacheContext(options.cacheContext, dep),
mtime,
});
});
};
async.parallel(
[
(cb) => async.mapLimit(dependencies, 20, toDepDetails, cb),
(cb) => async.mapLimit(contextDependencies, 20, toDepDetails, cb),
],
(err, taskResults) => {
const [deps, contextDeps] = taskResults;
// 将当前module的dependencies、contextDependencies及处理当前module的loader存起来
writeFn(
data.cacheKey,
{
remainingRequest: pathWithCacheContext(
options.cacheContext,
data.remainingRequest
),
dependencies: deps,
contextDependencies: contextDeps,
result: args,
},
() => {
// ignore errors here
callback(null, ...args);
}
);
}
);
}
function pitch(remainingRequest, prevRequest, dataInput) {
const options = Object.assign({}, defaults, getOptions(this));
const callback = this.async();
const data = dataInput;
data.remainingRequest = remainingRequest;
data.cacheKey = cacheKeyFn(options, data.remainingRequest);
// 通过cachekey,获取cache-loader上一次的缓存
readFn(data.cacheKey, (readErr, cacheData) => {
const FS = this.fs || fs;
async.each(
cacheData.dependencies.concat(cacheData.contextDependencies),
(dep, eachCallback) => {
const contextDep = {
...dep,
path: pathWithCacheContext(options.cacheContext, dep.path),
};
// 依次判断之前cache-loader内存储的dependencies、contextDependencies及loader文件的最后修改时间
FS.stat(contextDep.path, (statErr, stats) => {
// 只要有一个依赖 or loader文件的最后修改时间变了,则不复用缓存
if (compareFn(compStats, compDep) !== true) {
eachCallback(true);
return;
}
eachCallback();
});
},
(err) => {
// 复用缓存之后,将当前模块的dependencies、contextDependencies还原,方便webpack继续递归解析依赖树
cacheData.dependencies.forEach((dep) =>
this.addDependency(pathWithCacheContext(cacheContext, dep.path))
);
cacheData.contextDependencies.forEach((dep) =>
this.addContextDependency(
pathWithCacheContext(cacheContext, dep.path)
)
);
callback(null, ...cacheData.result);
}
);
});
}
const directories = new Set();
function cacheKey(options, request) {
const { cacheIdentifier, cacheDirectory } = options;
const hash = digest(`${cacheIdentifier}\n${request}`);
return path.join(cacheDirectory, `${hash}.json`);
}
function compare(stats, dep) {
return stats.mtime.getTime() === dep.mtime;
}
export const raw = true;
export { loader as default, pitch };
可以看到cache-loader
既是normal loader
也与picth loader
,内部是异步返回内容,但是使用了raw
来表示当前normal loader
接受的内容,必须是buffer
类型
总结
掌握以下知识点,不仅可以让我们熟练使用,还能够从原理层面了解webpack loader
loaders
顺序是在webpack
内部组合完成,loader
按照post
、inline
、normal
、pre
的顺序进行排序组装,然后传给loader-runner
执行
loader-runner
封装成了一个单独的npm
包,其内通过数组下标index
来控制loader
的执行顺序
异步loader
与同步loader
的区别就是,异步loader
需要主动调用this.async
,拿到一个innercallback
,然后在异步调用完成之后调用innercallback
的时机不同
picth loader
阻断原理就是,每次判断picth loader
的返回值是否为非undefined
的值,如果是则index--
, 然后直接跳过后续的pitch loader
执行,直接进入执行前一个的normal loader
逻辑
raw
属性只作用于normal loader
,原理就是判断内容是否是buffer
类型,如果不是则通过Buffer.from
将string
转换成buffer
类型
感谢各位看官老爷耐心看完,如果觉得对看官老爷有帮助,动动手指头点个👍吧!
转载自:https://juejin.cn/post/7246313318545342519