Webpack知识体系(下)
这是我参与第五届青训营 伴学笔记创作活动的第15天。
课程重点
- 理解loader
- 理解插件
详细知识点
理解loader
因为webpack只能处理标准的js资源,为了处理非标准js资源,设计出资源翻译模块————Loader,用于将资源翻译为标准js。 下面介绍一个使用Loader处理less的案例:
- 安装loader:
npm i less-loader css-loader style-loader -D - 添加module处理less
module: {
rules: [
{
// 用来匹配 .css 结尾的文件
test: /.css$/,
// use 数组里面 Loader 执行顺序是从右到左
use: ["style-loader", "css-loader","less-loader"],
},
],
},
链式调用
- less-loader: 实现less => css的转换
- css-loader: 将css包装成类似module.exports = "${css}"的内容,包装后的内容符合javascript语法。
- style-loader:将css模块包进require语句,并在运行时调用injectStyle等函数将内容注入到页面的style标签。
其它特性
- 链式执行
- 支持异步执行
- 分normal、pitch两种模式
手动实现一个简单的loader
loader接受的参数:
- content: 源文件内容
- map: SourceMap数据
- meta: 数据,可以是任何内容
// loaders/loader1.js
module.exports = function loader1(content) {
console.log("hello loader");
return content;
};
在module中配置
{
test: /\.less$/,
use: [
path.resolve(__dirname,"src/loaders/loader1.js"),//自定义的loader
"style-loader", //使用多个loader
path.resolve(__dirname,"src/loaders/loader1.js"),//自定义的loader
"css-loader",
path.resolve(__dirname,"src/loaders/loader1.js"),//自定义的loader
"less-loader",
path.resolve(__dirname,"src/loaders/loader1.js")//自定义的loader
],
//loader:'xxx',//只能使用一个loader
},
理解插件
通过插件我们可以扩展 webpack,加入自定义的构建行为,使 webpack 可以执行更广泛的任务,拥有更强的构建能力。
plugin的工作原理 webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。 插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。webpack 通过 Tapable 来组织这条复杂的生产线。 webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。 webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。 ——「深入浅出 Webpack」
站在代码逻辑的角度就是:webpack 在编译代码过程中,会触发一系列 Tapable 钩子事件,插件所做的,就是找到相应的钩子,往上面挂上自己的任务,也就是注册事件,这样,当 webpack 构建的时候,插件注册的事件就会随着钩子的触发而执行了。
钩子的本质 就是:事件。为了方便我们直接介入和控制编译过程,webpack 把编译过程中触发的各类关键事件封装成事件接口暴露了出来。这些接口被很形象地称做:
hooks(钩子)。开发插件,离不开这些钩子。
钩子的核心信息:
- 时机:
compier.hooks.compilation编译过程的特定节点,Webpack会以钩子形式通知插件此刻正在发生什么事情; - 上下文:通过tapable提供的回调机制,以参数方式传递上下文信息;
- 交互:在上下文参数对象中附带了很多存在side effect的交互接口,插件可以通过这些接口改变
Tapable 为 webpack 提供了统一的插件接口(钩子)类型定义,它是 webpack 的核心功能库。
Tapable 还统一暴露了三个方法给插件,用于注入不同类型的自定义构建行为:
tap:可以注册同步钩子和异步钩子。tapAsync:回调方式注册异步钩子。tapPromise:Promise 方式注册异步钩子。
Plugin构建对象
compiler 对象中保存着完整的 Webpack 环境配置,每次启动 webpack 构建时它都是一个独一无二,仅仅会创建一次的对象。
这个对象会在首次启动 Webpack 时创建,我们可以通过 compiler 对象上访问到 Webapck 的主环境配置,比如 loader 、 plugin 等等配置信息。 它有以下主要属性:
compiler.options可以访问本次启动 webpack 时候所有的配置文件,包括但不限于 loaders 、 entry 、 output 、 plugin 等等完整配置信息。compiler.inputFileSystem和compiler.outputFileSystem可以进行文件操作,相当于 Nodejs 中 fs。compiler.hooks可以注册 tapable 的不同种类 Hook,从而可以在 compiler 生命周期中植入不同的逻辑。
compilation 对象代表一次资源的构建,compilation 实例能够访问所有的模块和它们的依赖。
一个 compilation 对象会对构建依赖图中所有模块,进行编译。 在编译阶段,模块会被加载(load)、封存(seal)、优化(optimize)、 分块(chunk)、哈希(hash)和重新创建(restore)。 它有以下主要属性:
compilation.modules可以访问所有模块,打包的每一个文件都是一个模块。compilation.chunkschunk 即是多个 modules 组成而来的一个代码块。入口文件引入的资源组成一个 chunk,通过代码分割的模块又是另外的 chunk。compilation.assets可以访问本次打包生成所有文件的结果。compilation.hooks可以注册 tapable 的不同种类 Hook,用于在 compilation 编译模块阶段进行逻辑添加以及修改。
下面让我们试着开发一个插件
class TestPlugin {
constructor() {
console.log("TestPlugin constructor()");
}
// 1. webpack读取配置时,new TestPlugin() ,会执行插件 constructor 方法
// 2. webpack创建 compiler 对象
// 3. 遍历所有插件,调用插件的 apply 方法
apply(compiler) {
console.log("TestPlugin apply()");
}
}
module.exports = TestPlugin;
注册hook
class TestPlugin {
constructor() {
console.log("TestPlugin constructor()");
}
// 1. webpack读取配置时,new TestPlugin() ,会执行插件 constructor 方法
// 2. webpack创建 compiler 对象
// 3. 遍历所有插件,调用插件的 apply 方法
apply(compiler) {
console.log("TestPlugin apply()");
// 从文档可知, compile hook 是 SyncHook, 也就是同步钩子, 只能用tap注册
compiler.hooks.compile.tap("TestPlugin", (compilationParams) => {
console.log("compiler.compile()");
});
// 从文档可知, make 是 AsyncParallelHook, 也就是异步并行钩子, 特点就是异步任务同时执行
// 可以使用 tap、tapAsync、tapPromise 注册。
// 如果使用tap注册的话,进行异步操作是不会等待异步操作执行完成的。
compiler.hooks.make.tap("TestPlugin", (compilation) => {
setTimeout(() => {
console.log("compiler.make() 111");
}, 2000);
});
// 使用tapAsync、tapPromise注册,进行异步操作会等异步操作做完再继续往下执行
compiler.hooks.make.tapAsync("TestPlugin", (compilation, callback) => {
setTimeout(() => {
console.log("compiler.make() 222");
// 必须调用
callback();
}, 1000);
});
compiler.hooks.make.tapPromise("TestPlugin", (compilation) => {
console.log("compiler.make() 333");
// 必须返回promise
return new Promise((resolve) => {
resolve();
});
});
// 从文档可知, emit 是 AsyncSeriesHook, 也就是异步串行钩子,特点就是异步任务顺序执行
compiler.hooks.emit.tapAsync("TestPlugin", (compilation, callback) => {
setTimeout(() => {
console.log("compiler.emit() 111");
callback();
}, 3000);
});
compiler.hooks.emit.tapAsync("TestPlugin", (compilation, callback) => {
setTimeout(() => {
console.log("compiler.emit() 222");
callback();
}, 2000);
});
compiler.hooks.emit.tapAsync("TestPlugin", (compilation, callback) => {
setTimeout(() => {
console.log("compiler.emit() 333");
callback();
}, 1000);
});
}
}
module.exports = TestPlugin;
通过调试查看 compiler 和 compilation 对象数据情况:
node --inspect-brk ./node_modules/webpack-cli/bin/cli.js
总结
以上内容简单介绍了webpack中loader和plugin原理以及使用,下图总结了webpack的一些常用的配置项和常用的loader,plugin,最好能掌握它们并熟练使用。