基本打包过程
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 fileBuffer = fs.readFileSync(filename, "utf-8");
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 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('入口文件'); 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 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
| 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 { 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> `; 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()], };
|