loader和plugin它们两者在webpack内部是如何进行工作的呢?
让我们手写一个loader和plugin来看看他的内部原理,以便加深对webpack的理解
手写loader
看过一些webpack文档的人应该都知道,loader是链式传递的,将文件资源从上一个传递到下一个,并且loader的处理也遵循从下往上的顺序,现在我们简单了解一下loader的开发原则
-
单一原则:每个loader只做一件事情,简单应用,便于维护
-
链式原则:webpack会按顺序链式调用每个loader
-
统一原则:遵循
webpack制定的设计规则和结构,输入输出均为字符串,各个loader完全独立,即插即用现在我们尝试写一个
less-loader和style-loader,将less文件处理后通过style标签的方式渲染到页面上去
同步loader
loader其实就是一个函数,接受匹配到的资源字符串和soueceMap,我们可以修改文件内容字符串后再返回给下一个loader处理,因此一个最简单的loader如下
module.exports = function(source, soueceMap){
return source
}
导出的
loader函数不能使用箭头函数,因为很多loader内部的属性和方法都需要通过this进行调用,比如this.cacheable()来进行缓存、this.sourceMap判断是否需要生成sourceMap等。
我们在项目中创建一个loader文件夹,用来存放我们自己写的loader,然后新建我们自己的style-loader。
//loader/style-loader.js
function loader(source, sourceMap) {
let style = `
let style = document.createElement('style');
style.innerHTML = ${JSON.stringify(source)};
document.head.appendChild(style)
`;
return style;
}
module.exports = loader;
这里的source可以看做是处理后的css文件字符串,我们通过style标签的形式将他插入到head中,这时可以发现最后返回的是一个JS代码的字符串,webpack会将返回的字符串打包到模块中。
异步loader
上面的style-loader都是同步操作,我们在处理source时,有时候会进行异步操作,一种方法是通过async/await,阻塞操作执行;另一种方法可以通过loader本身提供的回调函数callback。
//loader/less-loader
const less = require("less");
function loader(source) {
const callback = this.async();
less.render(source, function (err, res) {
let { css } = res;
callback(null, css);
});
}
module.exports = loader;
callback的详细传参方法如下:
callback({
//当无法转换原内容时,给 Webpack 返回一个 Error
error: Error | Null,
//转换后的内容
content: String | Buffer,
//转换后的内容得出原内容的Source Map(可选)
sourceMap?: SourceMap,
//原内容生成 AST语法树(可选)
abstractSyntaxTree?: AST
})
加载本地loader
loader文件准备好了之后,我们需要将它们加载到webpack配置中去;我们加载第三方的loader只需要安装后在loader属性中写loader名称即可,现在加载本地loader需要把loader的路径配置上。
module.exports = {
module: {
rules: [{
test: /\.less/,
use: [
{
loader: './loader/style-loader.js',
},
{
loader: path.resolve(__dirname, "loader", "less-loader"),
},
],
}]
}
}
我们可以在loader中配置本地loader的相对路径或者绝对路径,但是这样写起来比较繁琐,我们可以利用webpack提供的resolveLoader属性,来告诉webpack应该去哪里解析本地loader。
module.exports = {
module: {
rules: [{
test: /\.less/,
use: [
{
loader: 'style-loader',
},
{
loader: 'less-loader',
},
],
}]
},
resolveLoader:{
modules: [path.resolve(__dirname, 'loader'), 'node_modules']
}
}
处理参数
我们在配置loader时,经常会给loader传递参数进行配置,一般是通过options属性来传递的,也有像url-loader通过字符串来传参。
{
test: /\.(jpg|png|gif|bmp|jpeg)$/,
use: 'url-loader?limt=1024&name=[hash:8].[ext]'
}
手写plugin
在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过Webpack提供的API改变输出结果。和手写loader一样,我们先来写一个简单的plugin:
class MyPlugin {
//编写一个构造器
constructor(options) {
console.log(options);
this.options = options
}
apply(compiler) {
console.log(this.options)
compiler.hooks.compile.tap("CopyrightWebpackPlugin", () => {
console.log("compiler");
});
}
}
module.exports = MyPlugin;
plugin的本质是类;我们在定义plugin时,其实是在定义一个类;定义好plugin后就可以在webpack配置中使用这个插件:
//webpack.config.js
const MyPlugin = require('./plugins/MyPlugin')
module.exports = {
plugins: [
new MyPlugin()
],
}
我们在构建插件时就能通过options获取配置信息,对插件做一些初始化的工作。在构造函数中我们发现多了一个apply函数,它会在webpack运行时被调用,并且注入compiler对象;其工作流程如下:
- webpack启动,执行new myPlugin(options),初始化插件并获取实例
- 初始化complier对象,调用myPlugin.apply(complier)给插件传入complier对象
- 插件实例获取complier,通过complier监听webpack广播的事件,通过complier对象操作webpack
我们可以通过apply函数中注入的compiler对象进行注册事件。
class MyPlugin {
apply(compiler) {
//注册完成的钩子
compiler.hooks.done.tap("MyPlugin", (compilation) => {
console.log("compilation done");
});
}
}
compiler不仅有同步的钩子,通过tap函数来注册,还有异步的钩子,通过tapAsync和tapPromise来注册:
class MyPlugin {
apply(compiler) {
compiler.hooks.run.tapAsync("MyPlugin", (compilation, callback) => {
setTimeout(()=>{
console.log("compilation run");
callback()
}, 1000)
});
compiler.hooks.emit.tapPromise("MyPlugin", (compilation) => {
return new Promise((resolve, reject) => {
setTimeout(()=>{
console.log("compilation emit");
resolve();
}, 1000)
});
});
}
}
compiler和compilation的区别在于:
- compiler代表了整个webpack从启动到关闭的生命周期,而compilation只是代表了一次新的编译过程
- compiler和compilation暴露出许多钩子,我们可以根据实际需求的场景进行自定义处理
手写FileListPlugin
了解了compiler和compilation的区别,我们就来尝试一个简单的示例插件,在打包目录生成一个filelist.md文件,文件的内容是将所有构建生成文件展示在一个列表中:
class FileListPlugin {
apply(compiler){
compiler.hooks.emit.tapAsync('FileListPlugin', (compilation, callback)=>{
var filelist = 'In this build:\n\n';
// 遍历所有编译过的资源文件,
// 对于每个文件名称,都添加一行内容。
for (var filename in compilation.assets) {
filelist += '- ' + filename + '\n';
}
// 将这个列表作为一个新的文件资源,插入到 webpack 构建中:
compilation.assets['filelist.md'] = {
source: function() {
return filelist;
},
size: function() {
return filelist.length;
}
};
callback();
})
}
}
module.exports = FileListPlugin