前言
前面我们学习了webpack的核心功能如何实现源代码的转换以及打包。
- 基于
@babel/parser把字符串转换成AST抽象语法树。 - 对
AST进行修改,既对源代码的修改。 - 使用
babel的API再把修改后的AST转换成新的字符串(期望的代码)。 - 以及了解了
css-loader和style-loader的基本实现。
下面我们学习一下plugin的使用场景以及工作原理。
前置问题
- 一个插件的基本代码结构
- webpack的构建流程
- Tapable自身有什么作用,以及如何把webpack的各个插件串联起来的?
compiler以及compilation对象的作用以及主要的API
插件的基本结构
这里写的是一个简版的md文件转换成html文件的一个案例
使用插件
const {resolve} = require('path')
const MdToHtmlPlugin = require('./plugins/md-to-html-plugin')
module.exports = {
mode: 'development',
entry: resolve(__dirname, 'src/app.js'),
output: {
path: resolve(__dirname, 'dist'),
filename: 'app.js'
},
plugins: [
new MdToHtmlPlugin({
template: resolve(__dirname, 'test.md'),
filename: 'test.html'
})
]
}
定义插件
const fs = require('fs')
const path = require('path')
function mdToHtml(mdStr) {
let html = ''
// 过滤掉空的
let mdArrTemp = mdStr.split('\n').filter(item => item !== "")
//ol 1.
//ul -
//h2 ##
let olReg = /\d\.\s/,
ulReg = /\-\s/,
h2Reg = /\#{2}\s/
mdArrTemp.forEach((item, index) => {
// 没有前一个元素, 或者前一个元素不是当前遍历的元素的时候
let prev = mdArrTemp[index - 1];
let next = mdArrTemp[index + 1];
if (h2Reg.test(item)) {
html += item.replace(h2Reg, "<h2>");
html += "</h2>\n";
} else if (ulReg.test(item)) {
if (!prev || !ulReg.test(prev)) {
html += "<ul>\n";
}
html += item.replace(ulReg, " <li>");
html += "</li>\n";
if (!next || !ulReg.test(next)) {
html += "</ul>\n";
}
} else if (olReg.test(item)) {
if (!prev || !olReg.test(prev)) {
html += "<ol>\n";
}
html += item.replace(olReg, " <li>");
html += "</li>\n";
if (!next || !olReg.test(next)) {
html += "<ol>\n";
}
}
})
return html
}
class MdToHtmlPlugin {
// 在构造函数里面可以传递实例的参数
constructor({template, filename}) {
if(!template) {
throw new Error('"template" must be configured')
}
this.template = template
this.filename = filename
}
// webpack 调用HelloPlugin 实例的apply方法给插件传入compiler对象。
apply(compiler) {
compiler.hooks.emit.tap("md-to-html-plugin", (compilation) => {
let _assets = compilation.assets
// 读取md原始文件。
let mdContent = fs.readFileSync(this.template, "utf-8");
let htmlTemplateContent = fs.readFileSync(path.resolve(__dirname, './template.html')).toString()
let html = mdToHtml(mdContent)
// 读取template模板原始文件。
// 将md文件的格式替换成html。
// 将替换生成的html插入到html template文件里面。
let genHtml = htmlTemplateContent.replace("<!--md-->", html);
fs.writeFileSync(path.resolve(__dirname, '../../dist/'+this.filename), genHtml, 'utf-8')
});
}
}
module.exports = MdToHtmlPlugin;
插件是如何工作的?
- 在读取webpack配置的过程中我们先使用
new MdToHtmlPlugin(options)初始化了一个MdToHtmlPlugin实例 - 也在其他代码中初始化了
compiler对象,调用了MdToHtmlPlugin.apply(compiler)方法,给插件传入compiler对象。 - 插件获取到
compiler对象,就可以通过调用compiler.hooks.<hook name>.call。具体的事件在文档中有体现。
node events && tabable
node
events 模块只提供了一个对象: events.EventEmitter。EventEmitter 的核心就是事件触发与事件监听器功能的封装。
var EventEmitter = require('events').EventEmitter;
var event = new EventEmitter();
event.on('some_event', function() {
console.log('some_event 事件触发');
});
setTimeout(function() {
event.emit('some_event');
}, 1000);
tapable
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
实现一个简单的tapable
class Hook{
constructor(args){
this.taps = []
this._args = args
}
tap(name,fn){
this.taps.push({name,fn})
}
}
class SyncHook extends Hook{
call(name,fn){
try {
this.taps.forEach(tap => tap.fn(name))
fn(null,name)
} catch (error) {
fn(error)
}
}
}
// 使用
let $ = new SyncHook()
$.tap('xx', () => {console.log('xx')})
$.call('xx', () => {console.log('xx called')})
理解Sync类型的钩子
1、SyncHook
const {SyncHook} = require('tapable')
// 创建实例
const syncHook = new SyncHook(['name', 'age'])
//注册事件
syncHook.tap('1', (name, age) => {
console.log('1' ,name , age)
})
syncHook.tap("2", (name, age) => {
console.log("2", name, age);
});
syncHook.tap("3", (name, age) => {
console.log("3", name, age);
});
// 触发事件 ,让回调函数执行
syncHook.call('天明', 10)
理解Async类型的钩子
1、AsyncSeriesHook
const { AsyncSeriesHook } = require("tapable");
// 创建实列
const asyncSeriesHook = new AsyncSeriesHook(["name", "age"]);
// 注册事件
asyncSeriesHook.tapAsync("1", (name, age, done) => {
setTimeout(() => {
console.log("1", name, age, new Date());
done();
}, 1000);
});
asyncSeriesHook.tapAsync("2", (name, age, done) => {
setTimeout(() => {
console.log("2", name, age, new Date());
done();
}, 2000);
});
asyncSeriesHook.tapAsync("3", (name, age, done) => {
setTimeout(() => {
console.log("3", name, age, new Date());
done();
}, 3000);
});
// 触发事件,让监听函数执行
asyncSeriesHook.callAsync("月儿", 10, () => {
console.log("执行完成");
});
tapable和webpack是如何关联起来的
Compiler.js
const { AsyncSeriesHook ,SyncHook } = require("tapable");
//创建类
class Compiler {
constructor(options) {
// 从options上面取出plugin 并执行apply方法,把this传入
this.hooks = {
run: new SyncHook(["run"]), //同步钩子
compile: new AsyncSeriesHook(["name", "age"]), //异步钩子
done: new SyncHook(['done'])
};
options.plugins[0].apply(this);
}
run() {
//执行异步钩子
this.hooks.run.call();
this.compile()
}
compile() {
//执行同步钩子 并传参
this.hooks.compile.callAsync("月儿", 10, (err) => {
this.done();
});
}
done() {
this.hooks.done.call();
}
}
module.exports = Compiler
- MyPlugin.js
const Compiler = require("./Compiler");
class MyPlugin {
apply(compiler) {
// 这边就是写好钩子,
// 等待webpack 内部在一定的时机触发钩子的回调函数即可,加入自己的逻辑在里面
//接受 compiler参数
compiler.hooks.run.tap("MyPlugin", () => console.log("开始编译..."));
// 触发异步钩子
compiler.hooks.compile.tapAsync("MyPlugin", (name, age, done) => {
setTimeout(() => {
console.log(`编译中...收到参数name:${name}-age:${age}编译中...`);
done();
}, 3000);
});
//同步钩子
compiler.hooks.done.tap("MyPlugin", () => console.log("结束编译..."));
}
}
//这里类似于webpack.config.js的plugins配置
//向 plugins 属性传入 new 实例
const myPlugin = new MyPlugin();
const options = {
plugins: [myPlugin],
};
let compiler = new Compiler(options);
compiler.run();
运行完上面这段代码,会得到以下的输出。
顺便吐槽一下原文中的代码,看了好几个版本的代码。
webpack构建流程
- 校验配置文件 :读取命令行传入或者
webpack.config.js文件,初始化本次构建的配置参数 - 生成
Compiler对象:执行配置文件中的插件实例化语句new MyWebpackPlugin(),为webpack事件流挂上自定义hooks - 进入
entryOption阶段:webpack开始读取配置的Entries,递归遍历所有的入口文件 run/watch:如果运行在watch模式则执行watch方法,否则执行run方法compilation:创建Compilation对象回调compilation相关钩子,依次进入每一个入口文件(entry),使用loader对文件进行编译。通过compilation我可以可以读取到module的resource(资源路径)、loaders(使用的loader)等信息。再将编译好的文件内容使用acorn解析生成AST静态语法树。然后递归、重复的执行这个过程, 所有模块和和依赖分析完成后,执行compilation的seal方法对每个 chunk 进行整理、优化、封装__webpack_require__来模拟模块化操作.emit:所有文件的编译及转化都已经完成,包含了最终输出的资源,我们可以在传入事件回调的compilation.assets上拿到所需数据,其中包括即将输出的资源、代码块Chunk等等信息。
// 修改或添加资源
compilation.assets['new-file.js'] = {
source() {
return 'var a=1';
},
size() {
return this.source().length;
}
};
复制代码
afterEmit:文件已经写入磁盘完成done:完成编译 还是的配一张图
compiler(负责编译)
Compiler 模块是 webpack 的主要引擎,它通过 CLI 或者 Node API 传递的所有选项创建出一个 compilation 实例。 它扩展(extends)自 Tapable 类,用来注册和调用插件。 大多数面向用户的插件会首先在 Compiler 上注册。
在为 webpack 开发插件时,你可能需要知道每个钩子函数是在哪里调用的。想要了解这些内容,请在 webpack 源码中搜索 hooks.<hook name>.call。
参考webpack中文文档plugins
compilation(负责创建bundles)
简单来说,Compilation的职责就是构建模块和Chunk,并利用插件优化构建过程。
常用API
compilation.hooks.optimizeChunkAssets.tapAsync(
'MyPlugin',
(chunks, callback) => {
chunks.forEach((chunk) => {
chunk.files.forEach((file) => {
compilation.assets[file] = new ConcatSource(
'/**Sweet Banner**/',
'\n',
compilation.assets[file]
);
});
});
callback();
}
);
参考文献