增加plugins 实际就是发布订阅,使用了tapable
使用tapable增加插件。
插件其实就是打包的时候,在各个过程中,穿插的一些钩子。
使用tapable正好可以实现
前置工作
安装tapable
在webpack开始编译时,new Compiler时,会立即执行构造方法,在这个过程中,会先发布一些任务,待会在编译的过程中,在将这些任务穿插到各个环节中。
安装tapable
shell
yarn add tapable@^1.1.3 -D
写几个不同过程中需要执行的插件
emit钩子时执行
js
// webpack\webpack-dev-3\plugin\P1-entryOption.js
class P {
// compiler是webpack中的编译器,上面有很多钩子
// 规定插件都有一个apply方法
apply(compiler) {
// 假如,在发射文件的时候,我想做什么事
compiler.hooks.emit.tap('emit', () => {
console.log('emit -----');
})
// tap 同步注册任务
}
}
module.exports = P
compile钩子时执行
js
// webpack\webpack-dev-3\plugin\P2-compile.js
class P {
// compiler是webpack中的编译器,上面有很多钩子
// 规定插件都有一个apply方法
apply(compiler) {
// 假如,在发射文件的时候,我想做什么事
compiler.hooks.compile.tap('compile', () => {
console.log('compile -----');
})
// tap 同步注册任务
}
}
module.exports = P
afterCompile钩子时执行
js
// webpack\webpack-dev-3\plugin\P3-afterCompile.js
class P {
// compiler是webpack中的编译器,上面有很多钩子
// 规定插件都有一个apply方法
apply(compiler) {
// 假如,在发射文件的时候,我想做什么事
compiler.hooks.afterCompile.tap('afterCompile', () => {
console.log('afterCompile -----');
})
// tap 同步注册任务
}
}
module.exports = P
修改配置文件
js
// webpack\webpack-dev-3\webpack.config.js
let path = require('path')
// 引入自定义插件
let P1 = require('./plugin/P1-entryOption.js')
let P2 = require('./plugin/P2-compile.js')
let P3 = require('./plugin/P3-afterCompile.js')
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.less$/,
// 引入自己写的loader
use: [
path.resolve(__dirname, 'loader', 'style-loader'),
path.resolve(__dirname, 'loader', 'less-loader')
]
}
]
},
// 增加插件
plugins: [
new P1(),
new P2(),
new P3(),
]
}
修改自定义webpack代码,配置钩子
修改compiler代码
js
// webpack\cheny-pack\lib\Complier.js
let path = require('path')
let fs = require('fs')
let babylon = require('babylon') // 把源码转换为ast语法树
let t = require('@babel/types') // 生成ast节点,可以生成各种类型
let traverse = require('@babel/traverse').default // 遍历ast语法树,替换节点
let generator = require('@babel/generator').default // 根据ast语法树,生成源代码
let ejs = require('ejs') // 模板引擎
// 为了方便 就全部用同步钩子
let { SyncHook } = require('tapable') // 定义一些钩子 同步或异步钩子
class Complier {
constructor(config) {
// 将配置文件挂载到实例上,所有实例都能拿到了
// webpack.config.js
this.config = config
// 1. 保存入口文件的路径
this.entryId; // './src/index.js'
// 2. 需要保存所有模块的依赖
// 解析文件的依赖,变成webpack打包传递的参数,key value的形式
// key 是路径名,value就是模块代码
this.modules = {}
this.entry = config.entry // 入口路径
this.root = process.cwd() // 工作路径 运行 npx cheny-pack 时候的路径
// 声明实例时 增加一些钩子
this.hooks = {
entryOption: new SyncHook(), // 获取完配置文件,配置文件传参给compiler编译器,有了实例之后执行
compile: new SyncHook(), // 开始编译之前的钩子,还没有开始编译
afterCompile: new SyncHook(),
afterPlugins: new SyncHook(),
run: new SyncHook(),
emit: new SyncHook(),
done: new SyncHook()
}
// 如果配置文件需要加载一些插件,就先把这些任务注册一下
let plugins = this.config.plugins
if (Array.isArray(plugins)) { // 如果配置的有插件,就依次执行插件的apply方法
plugins.forEach(plugin => {
plugin.apply(this) // 传递的参数 是这个编译器 this
// 通过这个this 就可以拿到各种钩子,然后在写插件的时候,想让在哪个环节执行,就调用对应的钩子
})
}
// 所有的插件都已经注册完成的钩子
this.hooks.afterPlugins.call()
}
run() {
// 开始执行编译的钩子
this.hooks.run.call()
// 开始编译之前执行的钩子
this.hooks.compile.call()
// 执行,并且创建模块的依赖关系
// 从入口开始执行
this.buildModule(path.resolve(this.root, this.entry), true) // true 表示解析的是主模块
// 编译之后的钩子
this.hooks.afterCompile.call()
// 发射一个文件 打包后的文件
this.emitFile()
// 发射文件之后的钩子
this.hooks.emit.call()
// 发射文件结束的钩子
this.hooks.done.call()
}
// 创建模块的依赖关系
buildModule(modulePath, isEntry) {
// 模块的key是相对路径 value是模块中的代码,不过需要做一些替换
// require都变成了webpack_require等
let source = this.getSource(modulePath) // 模块内容
// 模块id是一个相对路径 总路径 减去 rootPath
// 总路径modulePath F:\code\note_code\webpack\webpack-dev-3\src\index.js
// 工作路径this.root F:\code\note_code\webpack\webpack-dev-3
let moduleName = './' + path.relative(this.root, modulePath) // ./src/index.js 模块id
if (isEntry) { // 如果是主入口的话,记录一下主入口的id
this.entryId = moduleName // 保存入口的名字
}
// path.dirname(moduleName)获取父路径 ./src/index.js -> ./src
// 解析模块代码,替换相应的内容
let { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName))
// 把相对路径和模块中的内容对应起来
this.modules[moduleName] = sourceCode
// 在这里需要递归继续解析依赖项
dependencies.forEach(dep => { // 加载附模块
// 参数是一个绝对路径
this.buildModule(path.join(this.root, dep), false) // false 表示不是主入口文件
})
}
// 解析文件
// 需要把source源码进行改造 返回一个依赖列表
// 因为每一个模块里面还可能会引另外的模块,所以需要一个依赖列表
parse(source, parentPath) { // AST 解析语法树
let ast = babylon.parse(source) // 源码先解析成ast语法树
// https://astexplorer.net/
let dependencies = [] // 存放当源代码依赖的数组,即当前文件引入的第三方模块
// 遍历ast语法树
traverse(ast, {
CallExpression(p) { // 调用表达式,比如 a() require() ...
let node = p.node // 拿到对应的节点
// 如果是require 就改为 __webpack_require__
if (node.callee.name === 'require') {
node.callee.name = '__webpack_require__'
let moduleName = node.arguments[0].value // 取到的就是模块的引用名字 let a = require('./a') -> a
// 有时候我们引用的时候,没有带后缀,我们先把后缀加上
// path.extname('./a') === '' path.extname('./a.js') === '.js'
moduleName = moduleName + (path.extname(moduleName) ? '' : '.js') // ./a.js
// webpack的模板所有的路径都需要加上 ./src 所以需要把 ./a.js 变成 ./src/a.js
moduleName = './' + path.join(parentPath, moduleName) // ./src\a.js
// 把依赖的第三方模块 塞进依赖数组中
dependencies.push(moduleName)
// 替换原来的ast节点
node.arguments = [t.stringLiteral(moduleName)]
}
}
})
// 生成新的代码
let sourceCode = generator(ast).code
return { sourceCode, dependencies }
}
// 发射文件
emitFile() {
// 用转换后的数据,渲染模板
// 拿到输出路径
let main = path.join(this.config.output.path, this.config.output.filename)
// 获取到模板资源
let templateStr = this.getSource(path.join(__dirname, 'main.ejs'))
// 根据处理过的模块 渲染新代码
let code = ejs.render(templateStr, {
entryId: this.entryId,
modules: this.modules
})
// 如果是多入口打包,所以声明一个assets
this.assets = {}
this.assets[main] = code
// 生成打包结果
this.writeFile(main, this.assets[main])
}
// 生成打包结果
writeFile(filePath, code) {
// filePath: F:\code\note_code\webpack\webpack-dev-3\dist\bundle.js
// 先判断文件夹是否存在,不存在的话先创建文件夹
// path.dirname(filePath) F:\code\note_code\webpack\webpack-dev-3\dist
let outputFolder = path.dirname(filePath)
if (!fs.existsSync(outputFolder)) {
fs.mkdirSync(outputFolder) // 没有文件夹先创建文件夹
}
// 输出打包文件
fs.writeFileSync(filePath, code)
}
// 获取模块代码
// 我们是通过路径来读取代码的,实际上可能这个代码会匹配到自己配置的loader
// 比如 .less .js 等
getSource(modulePath) {
let content = fs.readFileSync(modulePath, 'utf8') // 读取到的源文件
// 看看是否需要用自定义的loader处理
let rules = this.config.module.rules // 拿到配置的所有loader
// 如果路径匹配上了,就用自己的loader去加载文件
for (let i = 0; i < rules.length; i++) {
let rule = rules[i] // 配置的每一种规则 比如 /\.js$/ /\.css$/ /\.less$/ 等
let { test, use } = rule // 将规则和配置的loader解构出来
// loader是倒着执行,拿到最后的索引
let count = use.length - 1
if (test.test(modulePath)) { // 匹配到当前这个模块需要使用loader来处理
// 倒着依次执行loader
function normalLoader() {
let loader = require(use[count]) // 配置loader时使用的是绝对路径
// 递归调用loader
content = loader(content)
count-- // 倒着挨个执行loader
if (count >= 0) {
normalLoader()
}
}
// 刚进来 先执行一次,然后就递归执行loader
normalLoader()
}
}
return content
}
}
module.exports = Complier
修改cheny-pack代码
js
#! /usr/bin/env node
// webpack\cheny-pack\bin\cheny-pack.js
// 最顶部的注释表示,这个文件的使用node运行
// 1. 需要找到当前执行名的路径,拿到webpack.config.js
let path = require('path')
// process.cwd() 可以拿到执行命令时的路径 npx cheny-pack
let config = require(path.resolve(process.cwd(), 'webpack.config.js'))
// 2. 声明一个类,来编译配置文件
let Compiler = require('../lib/Complier.js')
let complier = new Compiler(config)
// 调用钩子 entryOption 传参之后执行
complier.hooks.entryOption.call() // tapable 使用 call方法执行之前注册的任务
complier.run() // 标识运行编译
打包一下,看下效果 npx cheny-pack
我们编写的三个插件,都在打包过程中执行了,成功。
总结
webpack中的插件机制,其实就是一个发布订阅。
使用tapable来实现。
每一个插件都是一个类,里面规定有一个apply方法。
在webpack开始编译时,new Compiler时,会立即执行构造方法,在这个过程中,会先发布一些任务,待会在编译的过程中,在将这些任务穿插到各个环节中。这些任务是我们自己配置的插件。
配置一些钩子,在构造编译器时,就将这些钩子挂载到了编译器的实例上,接下来的过程随时可以访问这些钩子
js// webpack\cheny-pack\lib\Complier.js // 部分代码 // 为了方便 就全部用同步钩子 let { SyncHook } = require('tapable') // 定义一些钩子 同步或异步钩子 class Complier { constructor(config) { // 声明实例时 增加一些钩子 this.hooks = { entryOption: new SyncHook(), // 获取完配置文件,配置文件传参给compiler编译器,有了实例之后执行 compile: new SyncHook(), // 开始编译之前的钩子,还没有开始编译 afterCompile: new SyncHook(), afterPlugins: new SyncHook(), run: new SyncHook(), emit: new SyncHook(), done: new SyncHook() } // 如果配置文件需要加载一些插件,就先把这些任务注册一下 let plugins = this.config.plugins if (Array.isArray(plugins)) { // 如果配置的有插件,就依次执行插件的apply方法 plugins.forEach(plugin => { plugin.apply(this) // 传递的参数 是这个编译器 this // 通过这个this 就可以拿到各种钩子,然后在写插件的时候,想让在哪个环节执行,就调用对应的钩子 }) } // 所有的插件都已经注册完成的钩子 this.hooks.afterPlugins.call() } } module.exports = Complier
将钩子穿插到编译的各个环节中
jsrun() { // 开始执行编译的钩子 this.hooks.run.call() // 开始编译之前执行的钩子 this.hooks.compile.call() // 执行,并且创建模块的依赖关系 // 从入口开始执行 this.buildModule(path.resolve(this.root, this.entry), true) // true 表示解析的是主模块 // 编译之后的钩子 this.hooks.afterCompile.call() // 发射一个文件 打包后的文件 this.emitFile() // 发射文件之后的钩子 this.hooks.emit.call() // 发射文件结束的钩子 this.hooks.done.call() }
自己写的插件实现apply方法,在需要执行的时机执行
js// webpack\webpack-dev-3\plugin\P1-entryOption.js class P { // compiler是webpack中的编译器,上面有很多钩子 // 规定插件都有一个apply方法 apply(compiler) { // 假如,在发射文件的时候,我想做什么事 compiler.hooks.emit.tap('emit', () => { console.log('emit -----'); }) // tap 同步注册任务 } } module.exports = P
配置文件中加载插件
js// webpack\webpack-dev-3\webpack.config.js // 引入自定义插件 let P1 = require('./plugin/P1-entryOption.js') let P2 = require('./plugin/P2-compile.js') let P3 = require('./plugin/P3-afterCompile.js') module.exports = { // 增加插件 plugins: [ new P1(), new P2(), new P3(), ] }
参考
https://www.bilibili.com/video/BV1a4411e7Bz?p=39&spm_id_from=pageDriver