基本打包过程

0. 配置文件

1
2
3
4
5
6
7
module.exports = {
entry: './src/index.ts',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'bundle.js'
}
}

1. 解析入口文件

1.1 解析入口文件得到 AST

1
2
3
4
// const fs = require("fs");
const fileBuffer = fs.readFileSync(filename, "utf-8");
// const parser = require("@babel/parser);
const ast = parser.parse(fileBuffer, { sourceType: "module" });

1.2 遍历 AST 收集依赖

1
2
3
4
5
6
7
8
9
const deps = {}; // 依赖
traverse(ast, {
ImportDeclaration({ node }) {
// const path = require('path');
const dirname = path.dirname(filename);
const absPath = "./" + path.join(dirname, node.source.value);
deps[node.source.value] = absPath;
},
});

1.3 babel 编译

1
2
3
4
5
// 代码转换
const { code } = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"],
});
const moduleInfo = { filename, deps, code };

2. 获取模块依赖图

将 1.0 的步骤封装为 parse 函数,返回值是最后的 moduleInfo

1
2
3
4
5
6
7
8
const entry = parse('入口文件'); // 返回 { filename, deps, code }
const temp = [entry];
// 遍历模块的依赖
for (const key in entry.deps) {
if (deps.hasOwnProperty(key)) {
temp.push(parse(entry.deps[key]));
}
}

3. 生成最终执行的代码 (打包产物是一个 IIFE)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
generate(graph, entry) {
// 是一个立即执行函数
return `(function(graph){
function require(file) {
var exports = {};
function absRequire(relPath){
return require(graph[file].deps[relPath])
}
(function(require, exports, code){
eval(code)
})(absRequire, exports, graph[file].code)
return exports
}
require('${entry}')
})(${graph})`;
}

4. 打包输出

1
2
3
4
5
6
const { path: dirPath, filename } = output // 配置里的output;
const outputPath = path.join(dirPath, filename);
if(!fs.existsSync(dirPath)){
fs.mkdirSync(dirPath)
}
fs.writeFileSync(outputPath, code, 'utf-8')

以上就完成了一个基础的打包。

Loader

每个 loader 会链式地顺序执行,
每个 loader 只秉承单一职责并且独立,
loader 输入与输出均为字符串,
loader 本质上是一个函数。

写一个把 .txt 文件内容转为大写的 loader

1
2
3
4
5
// ./loaders/txt-uppercase-loader.js
module.exports = function (src) {
src = src.toUpperCase();
return src;
}

配置文件:

1
2
3
4
5
6
7
8
9
10
11
module: {
rules: [
{
test: /\.txt$/,
exclude: /node_modules/,
use: [
'./loaders/txt-uppercase-loader.js'
],
},
];
}

Plugin

webpack 构建生命周期是可以通过它提供多一些 api 获取到的。

  • 初始化参数
  • 开始编译
  • 确定入口
  • 编译模块
  • 完成模块编译
  • 输出资源
  • 输出完成
  • 完整生命周期函数

plugin 的作用就是基于事件流机制工作,监听 webpack 打包过程中的某些事件,修改打包结果。

  • plugin 是一个具有 apply 方法的 JavaScript 对象。(apply 方法会被 webpack compiler 调用,并且在整个编译生命周期都可以访问 compiler 对象。)
  • plugin 应当是一个 class 或是 构造函数。
    1
    2
    3
    4
    5
    6
    7
    class MyCoolWebpackPlugin {
    // compiler 对象代表了完整的 webpack 环境配置,这个对象在启动 webpack 时被一次性建立
    // 可以使用它来访问 webpack 的主环境
    apply(compiler) {
    // ...
    }
    }

实现一个创建 HTML 文件并引入打包后的 js 的 Plugin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const pluginName = "MyHtmlWebpackPlugin";

class MyHtmlWebpackPlugin {
apply(compiler) {
const filename = compiler.options.output.filename;
compiler.hooks.emit.tapAsync(pluginName, (compilation, callback) => {
const content = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Webpack</title>
<script defer src="./${filename}"></script>
</head>
<body>
</body>
</html>
`;
// 将这个文件作为一个新的文件资源,插入到 webpack 构建中:
compilation.assets["index.html"] = {
source: function () {
return content;
},
size: function () {
return content.length;
},
};
callback();
});
}
}
module.exports = MyHtmlWebpackPlugin;

在配置中:

1
2
3
4
module.exports = {
// ...
plugins: [new MyHtmlWebpackPlugin()],
};