Webpack 是一个前端资源构建工具,一个模块打包器,核心是处理字符串。Webpack 通过配置找到入口及其所有的依赖文件,然后经过各种 Loader 的转换处理,并且通过 Plugin 的方式广播出特定的事件,Plugin 监听事件并执行相应的逻辑,而且可以调用 Webpack 提供的 api 改变 Webpack 最终产出的代码。
Webpack 打包流程
loader
loader(模块转换器):用于把模块原内容按照需求转换成新内容
在实际开发过程中,webpack 默认只能打包处理以 js 后缀名结尾的模块。其他非 js 后缀名结尾的模块,webpack 默认处理不了,需要调用 loader 加载器才可以正常打包,否则会报错!
loader加载器的作用:协助 webpack 打包处理特定的文件模块。比如:
- css-loader 可以打包处理.css相关的文件
- less-loader 可以打包处理.less相关的文件
- babel-loader 可以打包处理 webpack 无法处理的高级 JS 语法
调用过程
webpack 入口文件 webpack.js ,根据配置文件 设置配置的options
options = new WebpackOptionsDefaulter().process(options);compiler = new Compiler(options.context);compiler.options = options;
WebpackOptionsDefaulter 加载 默认配置
// WebpackOptionsDefaulter.js
this.set("module.defaultRules", "make", options => [
{
type: "javascript/auto",
resolve: {}
},
{
test: /\.mjs$/i,
type: "javascript/esm",
resolve: {
mainFields: options.target === "web" ||
options.target === "webworker" ||
options.target === "electron-renderer" ?
["browser", "main"] :
["main"]
}
},
{
test: /\.json$/i,
type: "json"
},
{
test: /\.wasm$/i,
type: "webassembly/experimental"
}
]);
//...
this.set("optimization.splitChunks.cacheGroups.default", {
automaticNamePrefix: "",
reuseExistingChunk: true,
minChunks: 2,
priority: -20
});
this.set("optimization.splitChunks.cacheGroups.vendors", {
automaticNamePrefix: "vendors",
test: /[\\/]node_modules[\\/]/,
priority: -10
});
其中,test 表示匹配的文件类型,use 表示对应要调用的 loader
注意:
- use 数组中指定的 loader 顺序是固定的
- 多个 loader 的调用顺序是:从后往前调用
一组 loader 的执行有两个阶段:Pitching 阶段 和 Normal 阶段,类似于js中的事件捕获、冒泡。
webpack 的 loader-runner 会按正序(从左到右) require 每个 loader,把这个 loader 的模块导出函数 和 pitch函数都存到 loaderContext 对象上,然后执行该 loader 的 pitch 方法(如果有的话);如果一组 loader 的 pitch 都没有返回值,就开始
Normal阶段:**反向(从右到左)**执行 loader 的导出函数,依次进行模块源码的转换,直到拿到最后的处理结果;
但是当 Pitching 阶段某个 loader 的 pitch 有返回值,那么就会跳过剩余未读取的 loader,直接进入执行 loader 的环节。从前一个 require 的 loader 开始执行,pitch 的返回值即是传入的第一个参数。除了 pitch 有返回的那个 loader,倒序执行已经 require 的每个 loader。
原理可参考:浅析 webpack 打包流程(原理) 二 之【执行 loader 阶段,初始化模块 module,并用 loader 倒序转译】部分
实现一个loader
loader承担的是翻译官的职责,利用其弥补了让webpack只能理解JavaScript和JSON文件的问题,从而可以处理其它类型的文件,所以loader对webpack的重要性不言而喻,所以学习构建一个loader是学习webpack的必经之路。在学习编写一个loader之前,要明确一下loader的职责:其职责是单一的,只需要完成一种转换。下面将逐步阐述选择loader开发中的几个关键点并实现一个loader。
同步LOADER
同步loader指的是同步的返回转换后的内容。由于是在Node.js这样的单线程环境,所以转换过程会阻塞整个构建,构建缓慢,不适用于耗时较长的环境中。对于同步loader,主要有两种方法返回转换后的内容:return和this.callback.
利用return可直接返回转换后结果
module.exports = function (source, map, meta) {
// ...
// output为处理后结果
return output;
}
this.callback
该方法相比于return更加灵活,其参数主要有四个
this.callback(
err: Error | null,
content: string | Buffer,
sourceMap ? : SourceMap,
meta ? : any
);
1)第一个参数为无法转换原内容,Webpack会返回一个Error。
(2)第二个参数即为经过转换后的内容(为输出的内容)。
(3)指与编译后代码所映射的源代码,便于调试。为了在此loader中获取该sourceMap,则需要在创建的webpack做一下配置(以js为例,babel-loader会将基础ES6语法进行转换为ES5,通过devtool可以开启source-map):
// webpack.config.js
module.exports = {
// ...
module: {
rules: [
{
test: /\.js$/,
use: [
'test-loader', // 该loader即为自己构建的loader
{
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env'
]
}
}
]
}
]
},
devtool: 'eval-source-map',
}
可以是任何东西,输出该参数,即可在下一个loader中获取并使用,例如通过各loader之间共享通用的AST,加速编译时间。
利用this.callback可返回传递多参数的结果。
module.exports = function (source, map, meta) {
// 处理后获得的结果output
const output = dealOperation(source);
this.callback(null, output, map, meta);
}
异步LOADER
同步loader只适合于计算量小,速度快的场景,但是对于计量量大、耗时比较长的场景(例如网络请求),使用同步loader会阻塞整个构建过程,导致构建速度变慢,采用异步loader即可避免该问题。对于异步loader,使用this.async()可以获取到callback函数,该函数参数和同步loader中this.callback参数一致。
module.exports = function (content, map, meta) {
// 获取callback函数
const callback = this.async();
// 用setTimeout模拟该异步过程
setTimeout(() => {
// 处理后获得的结果output
const output = dealOperation(source);
callback(null, output, map, meta);
}, 100)
}