Webpack原理深析(实践篇)
# Webpack原理深析(实践篇)
# 手写一个简易版 Webpack
这里手写一个简易版的 Webpack,实现ES6转ES5的打包功能~
# 初始化
mkdir my-webpacknpm init -y根目录下新建
src文件,新建index.js、add.js、minus.js文件:
// src/utils/add.js
export default (a, b) => {
return a + b
}
// src/utils/minus.js
export const minus = (a, b) => {
return a - b;
}
// src/index.js
import add from './utils/add.js'
import { minus } from './utils/minus.js'
const sum = add(1, 2)
const division = minus(10, 1)
console.log('=======sum', sum)
console.log('=======division', division)
- 根目录下新建
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>my-webpack</title>
</head>
<body>
<script src="./src/index.js"></script>
</body>
</html>
这时,如果直接在浏览器中打开index.html是会报错的:Uncaught SyntaxError: Cannot use import statement outside a module,因为我们不能在script引入的js文件里,使用es6模块化语法。
接下来参考webpack的打包流程,手写一个简易版的webpack,实现打包之后能够访问到/src/index.js中的代码~
# 实现打包
实现之前,先简单分析下流程~
打包流程分析
- 首先,我们需要读到入口文件里的内容(也就是
index.js的内容) - 其次,分析入口文件,递归的去读取模块所依赖的文件内容,生成依赖图
- 最后,根据依赖图,生成浏览器能够运行的最终代码
# 配置参数和打包命令
- 根目录下新建
webpack文件夹,新建一个index.js作为打包编译的入口:
// webpack/index.js
const Complier = require("./compiler"); // 引入定义的 Compiler 类
const options = require("../webpack.config"); // 引入配置的参数
console.log('====webpack run!!!');
new Complier(options).run(); // 执行打包编译
这里引入了一个Complier类,打包相关的逻辑都在定义在这个类里(稍后会详细讲),开始编译就是执行它的run方法;同时也会传入在webpack.config.js中添加的自定义配置~
先看看webpack.config.js中的代码:
// webpack.config.js
const path = require("path");
module.exports = {
entry: "./src/index.js", // 打包入口
output: {
path: path.resolve(__dirname, "./dist"), // 打包出口
filename: "bundle.js" // 打包输出文件名
}
};
这里面的配置跟我们项目中webpack的配置一样,只是这里只定义了打包入口和打包输出~
- 接下来在
package.json中定义打包命令:
"scripts": {
"build": "node webpack/index.js"
},
当执行npm run build的时候,直接执行webpack/index.js中的代码~
# 定义 Compiler 类
新建文件webpack/compiler.js:
// webpack/compiler.js
// Compiler 编译
const fs = require("fs");
const path = require("path");
const Parser = require("./parser");
// 定义 Compiler 类
class Compiler {
// 初始化
constructor(options) {
// webpack 配置
const { entry, output } = options;
// 入口
this.entry = entry;
// 输出
this.output = output;
// 模块
this.modules = [];
}
// 构建启动
run() {
// 解析入口文件
const info = this.build(this.entry);
// 加入 modules
this.modules.push(info);
// 遍历 modules
// 收集依赖:从入口模块开始根据依赖关系进行递归解析
this.modules.forEach(({ dependecies }) => {
// 判断有依赖对象, 有则递归解析所有依赖项
if (dependecies) {
for (const dependency in dependecies) {
this.modules.push(this.build(dependecies[dependency]));
}
}
});
// 最后将依赖关系构成为依赖图(Dependency Graph)
const dependencyGraph = this.modules.reduce(
(graph, item) => ({
...graph,
[item.filename]: {
dependecies: item.dependecies,
code: item.code
}
}),
{}
);
console.log('=====dependencyGraph', dependencyGraph);
// 最后调用generate方法,生成打包文件
this.generate(dependencyGraph);
}
// 打包
build(filename) {
const { getAst, getDependecies, getCode } = Parser;
const ast = getAst(filename); // 获取ast
const dependecies = getDependecies(ast, filename); // 获取依赖
const code = getCode(ast); // 获取转换后的js
return {
filename, // 文件路径,可以作为每个模块的唯一标识符
dependecies, // 依赖对象,保存着依赖模块路径
code // 文件内容
};
}
// 重写 require函数,输出bundle: 这一步我们需要将刚才编写的执行函数和依赖图合成起来输出最后的打包文件.
generate(depsGraph) {
// 读取配置中传入的输出文件路径和名称
const filePath = path.join(this.output.path, this.output.filename);
// 传入总的依赖图depsGraph,获取打包后的内容bundle
const bundle = `(function(graph){
function require(moduleId){
function localRequire(relativePath){
return require(graph[moduleId].dependecies[relativePath])
}
var exports = {};
(function(require,exports,code){
eval(code)
})(localRequire,exports,graph[moduleId].code);
return exports;
}
require('${this.entry}')
})(${JSON.stringify(depsGraph)})`;
console.log('=====bundle', bundle);
// 如果没有打包输出目录,则新建
!fs.existsSync(this.output.path) && fs.mkdirSync(this.output.path);
// 把文件内容写入到文件系统
fs.writeFileSync(filePath, bundle, 'utf-8');
}
}
module.exports = Compiler;
关于打包的核心逻辑基本都在这里面了,大致流程就是:
- 初始化,引入配置,执行
run方法; - 执行
build方法,解析入口文件,获取编译后的文件; - 收集依赖,递归遍历所有依赖,生成依赖图
dependencyGraph; - 最后执行
generate方法,输出打包后的文件。
先来看看第一步,
build方法中是如何编译源文件的~
# Parser编译
上面Compiler中引入了 Parser:const Parser = require("./parser");,这个对象里面定义一些 js 的编译方法,主要是用于 js 和 ast 树之间互相转换用的~
首先安装第三方编译工具:
npm i @babel/core @babel/parser @babel/preset-env @babel/traverse -D@babel/parser是一个js语法解析工具,将js代码解析成对应的AST;@babel/traverse能对AST节点进行递归遍历;@babel/core提供一些操作AST语法的方法;@babel/preset-env能将ES6语法转换成ES5;
然后新建文件:
webpack/parser.js:
// webpack/parser.js
// parser 解析
const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser"); // js => ast
const traverse = require("@babel/traverse").default; // 遍历 ast
const { transformFromAst } = require("@babel/core"); // ast => js
// 定义 Parser 方法
const Parser = {
// 获取 AST 语法树
getAst: path => {
// 读取文件
const content = fs.readFileSync(path, "utf-8");
// 将文件内容转为AST抽象语法树
return parser.parse(content, {
sourceType: "module" // 表示我们要解析的是ES模块
});
},
// 获取依赖
getDependecies: (ast, filename) => {
const dependecies = {};
// 遍历所有的 import 模块,存入dependecies,
// Babel 提供了@babel/traverse(遍历)方法维护这 AST 树的整体状态,我们这里使用它来帮我们找出依赖模块。
traverse(ast, {
// 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)
ImportDeclaration({ node }) {
const dirname = path.dirname(filename);
// 保存依赖模块路径,之后生成依赖关系图需要用到
const filepath = "./" + path.join(dirname, node.source.value);
dependecies[node.source.value] = filepath;
}
});
return dependecies;
},
// es6转es5
getCode: ast => {
// AST转换为code
const { code } = transformFromAst(ast, null, {
presets: ["@babel/preset-env"]
});
return code;
}
};
module.exports = Parser;
这里面定义的方法会在Compiler的build中用到,用于获取ES6转换成ES5之后的代码~
# 收集依赖
回到Compiler,获取到编译后的文件后,执行了递归生成依赖关系图, 然后执行this.generate(dependencyGraph)方法,根据依赖关系图生成打包后的输出文件:
// webpack/compiler.js
// 构建启动
run() {
// 解析入口文件
const info = this.build(this.entry);
// 加入 modules
this.modules.push(info);
// 遍历 modules
// 收集依赖:从入口模块开始根据依赖关系进行递归解析
this.modules.forEach(({ dependecies }) => {
// 判断有依赖对象, 有则递归解析所有依赖项
if (dependecies) {
for (const dependency in dependecies) {
this.modules.push(this.build(dependecies[dependency]));
}
}
});
// 最后将依赖关系构成为依赖图(Dependency Graph)
const dependencyGraph = this.modules.reduce(
(graph, item) => ({
...graph,
[item.filename]: {
dependecies: item.dependecies,
code: item.code
}
}),
{}
);
console.log('=====dependencyGraph', dependencyGraph);
// 最后调用generate方法,生成打包文件
this.generate(dependencyGraph);
}
先打印dependencyGraph看下输出了什么:
=====dependencyGraph {
'./src/index.js': {
dependecies: {
'./utils/add.js': './src/utils/add.js',
'./utils/minus.js': './src/utils/minus.js'
},
code: '"use strict";\n' +
'\n' +
'var _add = _interopRequireDefault(require("./utils/add.js"));\n' +
'\n' +
'var _minus = require("./utils/minus.js");\n' +
'\n' +
'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' +
'\n' +
'var sum = (0, _add["default"])(1, 2);\n' +
'var division = (0, _minus.minus)(10, 1);\n' +
"console.log('=======sum', sum);\n" +
"console.log('=======division', division);"
},
'./src/utils/add.js': {
dependecies: {},
code: '"use strict";\n' +
'\n' +
'Object.defineProperty(exports, "__esModule", {\n' +
' value: true\n' +
'});\n' +
'exports["default"] = void 0;\n' +
'\n' +
'var _default = function _default(a, b) {\n' +
' return a + b;\n' +
'};\n' +
'\n' +
'exports["default"] = _default;'
},
'./src/utils/minus.js': {
dependecies: {},
code: '"use strict";\n' +
'\n' +
'Object.defineProperty(exports, "__esModule", {\n' +
' value: true\n' +
'});\n' +
'exports.minus = void 0;\n' +
'\n' +
'var minus = function minus(a, b) {\n' +
' return a - b;\n' +
'};\n' +
'\n' +
'exports.minus = minus;'
}
}
可以发现,里面每个路径下都有code和dependecies两个属性,比如./src/index.js路径下的dependecies属性就有值,说明该文件里面有引入其他依赖;code则是经过Parser转换后的代码~
# 输出打包后的文件
接着分析下generate方法:
// webpack/compiler.js
class Compiler {
...
// 构建启动
run() {
...
// 最后调用generate方法,生成打包文件
this.generate(dependencyGraph);
}
// 重写 require函数,输出bundle: 这一步我们需要将刚才编写的执行函数和依赖图合成起来输出最后的打包文件.
generate(depsGraph) {
// 读取配置中传入的输出文件路径和名称
const filePath = path.join(this.output.path, this.output.filename);
// 传入总的依赖图depsGraph,获取打包后的内容bundle
const bundle = `(function(graph){
function require(moduleId){
function localRequire(relativePath){
return require(graph[moduleId].dependecies[relativePath])
}
var exports = {};
(function(require,exports,code){
eval(code)
})(localRequire,exports,graph[moduleId].code);
return exports;
}
require('${this.entry}')
})(${JSON.stringify(depsGraph)})`;
console.log('=====bundle', bundle);
// 如果没有打包输出目录,则新建
!fs.existsSync(this.output.path) && fs.mkdirSync(this.output.path);
// 把文件内容写入到文件系统
fs.writeFileSync(filePath, bundle, 'utf-8');
}
}
module.exports = Compiler;
分析下 bundle:
// 1. 为了避免污染到全局,bundle返回的是一个自执行函数,传入的是打包后的依赖关系图;
(function(graph){
// 2. 在这个自执行函数里定义了一个 require 方法
function require(moduleId){
// 定义一个获取依赖方法:相对路径转化为绝对路径
// 找到对应moduleId的依赖对象,调用require函数,eval执行,拿到exports对象
function localRequire(relativePath){
return require(graph[moduleId].dependecies[relativePath])
}
// 因为转换后的代码会把 ES6 中的 `export default ...` 转为成 `exports.default ...`
// 但因为 ES5 不支持 ESM, exports对象缺失,所以这里定义了一个 exports 对象,传入到下方的自执行函数中
var exports = {};
// require 方法会执行一个自执行函数,会执行当前路径下的code
// 会传入localRequire 方法,返回的也是 require 方法,用于 code 中如果需要引入其他依赖时可以调用 reuqire 方法
// commonjs语法使用module.exports暴露实现,我们传入的exports对象会捕获依赖对象暴露的实现并写入,如:exports.add = add;
(function(require,exports,code){
eval(code) // 通过 eval 来执行代码
})(localRequire,exports,graph[moduleId].code);
// 暴露exports对象,即暴露依赖对象对应的实现
return exports;
}
require('${this.entry}') // 3. 首先执行了 require(this.entry), 传入配置的入口文件路径
})(${JSON.stringify(depsGraph)}) // 传入依赖关系图
# 运行
最后修改index.html中js的引入路径如下:
<script src="./dist/bundle.js"></script>
重新执行打包命令npm run build,会发现新增了dist/bundle.js文件:
// dist/bundle.js
(function(graph){
function require(moduleId){
function localRequire(relativePath){
return require(graph[moduleId].dependecies[relativePath])
}
var exports = {};
(function(require,exports,code){
eval(code)
})(localRequire,exports,graph[moduleId].code);
return exports;
}
require('./src/index.js')
})({"./src/index.js":{"dependecies":{"./utils/add.js":"./src/utils/add.js","./utils/minus.js":"./src/utils/minus.js"},"code":"\"use strict\";\n\nvar _add = _interopRequireDefault(require(\"./utils/add.js\"));\n\nvar _minus = require(\"./utils/minus.js\");\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nvar sum = (0, _add[\"default\"])(1, 2);\nvar division = (0, _minus.minus)(10, 1);\nconsole.log('=======sum', sum);\nconsole.log('=======division', division);"},"./src/utils/add.js":{"dependecies":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports[\"default\"] = void 0;\n\nvar _default = function _default(a, b) {\n return a + b;\n};\n\nexports[\"default\"] = _default;"},"./src/utils/minus.js":{"dependecies":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.minus = void 0;\n\nvar minus = function minus(a, b) {\n return a - b;\n};\n\nexports.minus = minus;"}})
最后重新再浏览器中运行index.html文件,则会发现能执行src/index.js中的代码了~
# 参考
# 手写 Loader
Webpack中的Loader本质上就是一个函数,这个函数会在我们在我们加载一些文件时执行, 比如常见的file-loader、vue-loader、babel-loader等,专门用于打包时解析各种类型的文件。
# 初始化
mkdir my-webpack-loadernpm init -ynpm i webpack webpack-cli -D项目目录结构如下:
── loader
│ ├── arrow-function-loader.js
├── src
│ ├── index.js
├── index.html
├── webpack.config.js
├── package.json
具体代码如下:
loader/arrow-function-loader.js
// 箭头函数转换成普通函数:
// const fn = (a, b) => a + b 转换为 const fn = function(a, b) { return a + b }
// 导出一个函数
module.exports = function (source) {
console.log('======arrow-function-loader', source);
return source
}
src/index.js
const add = (a, b) => {
return a + b;
}
console.log('=====add', add(1,3));
webpack.config.js
const path = require("path");
module.exports = {
mode: 'none',
entry: "./src/index.js", // 打包入口
output: {
path: path.resolve(__dirname, "./dist"), // 打包出口
filename: "bundle.js" // 打包输出文件名
},
// 这里使用resolveLoader配置项,指定loader查找文件路径,这样我们使用loader时候可以直接指定loader的名字
resolveLoader: {
// loader路径查找顺序从左往右
modules: ['node_modules', './loader']
},
module: {
rules: [
{
test: /.js$/,
use: [
'arrow-function-loader' // 引入自定义的loader
]
}
]
}
};
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>my-webpack-loader</title>
</head>
<body>
<div>webpack loader build!!!</div>
<script src="./dist/bundle.js"></script>
</body>
</html>
package.json打包命令:
"scripts": {
"build": "webpack --config webpack.config.js"
},
- 执行打包命令:
npm run build,终端会输出如下内容:
> webpack --config webpack.config.js
======arrow-function-loader
const add = (a, b) => {
return a + b;
}
console.log('=====add', add(1,3));
asset bundle.js 164 bytes [compared for emit] (name: main)
./src/index.js 80 bytes [built] [code generated]
webpack 5.65.0 compiled successfully in 88 ms
这样就说明引入的
loader是生效了的~
同时会生成dist/bundle.js文件:
/******/ (() => { // webpackBootstrap
var __webpack_exports__ = {};
const add = (a, b) => {
return a + b;
}
console.log('=====add', add(1,3));
/******/ })()
;
- 浏览器直接打开
index.html,运行正常说明打包成功,接下来开始完善arrow-function-loader中的逻辑~
# arrow-function-loader
实现箭头函数转换成普通函数~
# 分析 AST 结构
首先在astexplorer (opens new window)上分析 const fn = (a, b) => a + b 和 const fn = function(a, b) { return a + b }看两者语法树的区别:
const fn = (a, b) => a + b:
{
"type": "Program",
"start": 0,
"end": 26,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 26,
"declarations": [
{
"type": "VariableDeclarator",
"start": 6,
"end": 26,
"id": {
"type": "Identifier",
"start": 6,
"end": 8,
"name": "fn"
},
"init": {
"type": "ArrowFunctionExpression", // 箭头函数
"start": 11,
"end": 26,
"id": null,
"expression": true,
"generator": false,
"async": false,
"params": [
{
"type": "Identifier",
"start": 12,
"end": 13,
"name": "a"
},
{
"type": "Identifier",
"start": 15,
"end": 16,
"name": "b"
}
],
"body": {
"type": "BinaryExpression", // 二进制表达式(BinaryExpression)
"start": 21,
"end": 26,
"left": {
"type": "Identifier",
"start": 21,
"end": 22,
"name": "a"
},
"operator": "+",
"right": {
"type": "Identifier",
"start": 25,
"end": 26,
"name": "b"
}
}
}
}
],
"kind": "const"
}
],
"sourceType": "module"
}
const fn = function(a, b) { return a + b }:
{
"type": "Program",
"start": 0,
"end": 42,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 42,
"declarations": [
{
"type": "VariableDeclarator",
"start": 6,
"end": 42,
"id": {
"type": "Identifier",
"start": 6,
"end": 8,
"name": "fn"
},
"init": {
"type": "FunctionExpression", // 普通函数
"start": 11,
"end": 42,
"id": null,
"expression": false,
"generator": false,
"async": false,
"params": [
{
"type": "Identifier",
"start": 20,
"end": 21,
"name": "a"
},
{
"type": "Identifier",
"start": 23,
"end": 24,
"name": "b"
}
],
"body": {
"type": "BlockStatement", // 代码块(BlockStatement)
"start": 26,
"end": 42,
"body": [
{
"type": "ReturnStatement",
"start": 28,
"end": 40,
"argument": {
"type": "BinaryExpression",
"start": 35,
"end": 40,
"left": {
"type": "Identifier",
"start": 35,
"end": 36,
"name": "a"
},
"operator": "+",
"right": {
"type": "Identifier",
"start": 39,
"end": 40,
"name": "b"
}
}
}
]
}
}
}
],
"kind": "const"
}
],
"sourceType": "module"
}
分析总结:
- 变成普通函数之后就不叫箭头函数
ArrowFunctionExpression,而是函数表达式FunctionExpression - 所以首先我们要把 箭头函数表达式(
ArrowFunctionExpression) 转换为 函数表达式(FunctionExpression) - 要把 二进制表达式(
BinaryExpression) 放到一个 代码块中(BlockStatement) - 其实我们要做就是把一棵树变成另外一颗树,说白了其实就是拼成另一颗树的结构,然后生成新的代码,就可以完成代码的转换
# 访问者模式
在 babel 中,我们开发 plugins 的时候要用到访问者模式,就是说在访问到某一个路径的时候进行匹配,然后在对这个节点进行修改,比如说上面的当我们访问到 ArrowFunctionExpression 的时候,对 ArrowFunctionExpression 进行修改,变成普通函数
const babel = require('@babel/core')
const code = `const fn = (a, b) => a + b` // 转换后 const fn = function(a, b) { return a + b }
const arrowFnPlugin = {
// 访问者模式
visitor: {
// 当访问到某个路径的时候进行匹配
ArrowFunctionExpression(path) {
// 拿到节点
const node = path.node
console.log('ArrowFunctionExpression -> node', node)
},
},
}
const r = babel.transform(code, {
plugins: [arrowFnPlugin],
})
console.log(r)
# 修改 AST 结构
在visitor.ArrowFunctionExpression中我们拿到的节点其实就是 ArrowFunctionExpression 的 AST,此时我们要做的是把 ArrowFunctionExpression 的结构替换成 FunctionExpression的结构,但是需要我们组装类似的结构,这么直接写很麻烦,这里需要用到 babel 为我们提供了一个工具叫做 @babel/types。
@babel/types (opens new window)集成了一些快速生成、修改、删除
AST Node的方法~
那么接下来我们就开始生成一个 FunctionExpression,然后把之前的 ArrowFunctionExpression 替换掉;@babel/types提供了functionExpression方法,该方法接受相应的参数即可生成一个 FunctionExpression:
t.functionExpression(id, params, body, generator, async)
id:Identifier (default: null) id 可传递 nullparams:Array<LVal>(required) 函数参数,可以把之前的参数拿过来body:BlockStatement(required) 函数体,接受一个BlockStatement我们需要生成一个generator:boolean(default: false) 是否为generator函数,当然不是了async:boolean(default: false) 是否为async函数,肯定不是了
完整代码如下:
// loader/arrow-function-loader.js
// 箭头函数转换成普通函数:
// const fn = (a, b) => a + b 转换为 const fn = function(a, b) { return a + b }
const babel = require('@babel/core')
const t = require('@babel/types')
// 导出一个函数
module.exports = function (source) {
console.log('======arrow-function-loader', source);
const arrowFnPlugin = {
// 访问者模式
// 在 babel 中,我们开发 plugins 的时候要用到访问者模式,就是说在访问到某一个路径的时候进行匹配,然后在对这个节点进行修改
visitor: {
// 当访问到某个路径的时候进行匹配
// 当我们访问到 ArrowFunctionExpression 的时候,对 ArrowFunctionExpression 进行修改,变成普通函数
ArrowFunctionExpression(path) {
// 拿到节点然后替换节点
const node = path.node
console.log("=====ArrowFunctionExpression====node", node)
// 拿到函数的参数
const params = node.params
let body = node.body
// 判断是不是 blockStatement,不是的话让他变成 blockStatement
if (!t.isBlockStatement(body)) {
body = t.blockStatement([body])
}
// 生成新的 functionExpression
const functionExpression = t.functionExpression(null, params, body)
// 替换原来的函数
path.replaceWith(functionExpression)
}
},
}
// 转换代码
const r = babel.transform(source, {
plugins: [arrowFnPlugin], //plugins 中引入自定义的修改规则
})
console.log(r.code) // const fn = function (a, b) { return a + b; };
return r.code;
}
# 运行
执行打包命令:npm run build,会发现终端输出:
=====r.code const add = function (a, b) {
return a + b;
};
说明箭头函数已经转换成普通函数了;看一下打包之后的文件:dist/bundle.js:
/******/ (() => { // webpackBootstrap
var __webpack_exports__ = {};
const add = function (a, b) {
return a + b;
};
console.log('=====add', add(1, 3));
/******/ })()
;
输出也是ok的,浏览器执行一下,应该也是没问题的~
参考
# 手写 Plugin
plugin通常是在webpack在打包的某个时间节点做一些操作,我们使用plugin的时候,一般都是new Plugin()这种形式使用,所以,首先应该明确的是,plugin应该是一个类。
因为项目配置其实都差不多,这里就继续在上一个my-webpack-loader项目添加plugin了~
- 新建
plugins/test-webpack-plugin.js文件:
class TestWebpackPlugin {
constructor () {
console.log('plugin init')
}
apply (compiler) {
}
}
module.exports = TestWebpackPlugin
在TestWebpackPlugin的构造函数打印一条信息,当我们执行打包命令时,这条信息就会输出,plugin类里面需要实现一个apply方法,webpack打包时候,会调用plugin的apply方法来执行plugin的逻辑,这个方法接受一个compiler作为参数,这个compiler是webpack实例。
plugin的核心在于,apply方法执行时,可以操作webpack本次打包的各个时间节点(hooks,也就是生命周期勾子),在不同的时间节点做一些操作。
关于webpack编译过程的各个生命周期勾子,可以参考Compiler Hooks (opens new window)
// plugins/test-webpack-plugin.js
class TestWebpackPlugin {
constructor () {
console.log('plugin init')
}
apply (compiler) {
// 一个新的编译(compilation)创建之后(同步)
// compilation代表每一次执行打包,独立的编译
compiler.hooks.compile.tap('TestWebpackPlugin', compilation => {
console.log(compilation)
})
// 生成资源到 output 目录之前(异步)
compiler.hooks.emit.tapAsync('TestWebpackPlugin', (compilation, fn) => {
console.log(compilation)
// 打包时候自动生成一个md文档,文档内容是很简单的一句话
compilation.assets['index.md'] = {
// 文件内容
source: function () {
return 'this is a demo for plugin'
},
// 文件尺寸
size: function () {
return 25
}
}
fn()
})
}
}
module.exports = TestWebpackPlugin
webpack.config.js中添加配置:
const TestWebpackPlugin = require('./plugins/test-webpack-plugin')
module.exports = {
...
plugins: [
new TestWebpackPlugin() // 引入自定义插件
]
};
npm run build,打包,会发现dist中生成了index.md文件,说明自定义的插件TestWebpackPlugin生效了~