Webpack 构建流程
Webpack 的初始化、编译、输出三个阶段
问题
描述 Webpack 的构建流程,包括初始化、编译、输出三个阶段分别做了什么。
解答
Webpack 构建流程分为三个阶段:
1. 初始化阶段
读取配置,创建 Compiler 对象,加载插件。
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
// 入口配置
entry: './src/index.js',
// 输出配置
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.[contenthash].js',
},
// 模块处理规则
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader',
exclude: /node_modules/,
},
],
},
// 插件
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
}),
],
};
初始化阶段的主要工作:
// 简化的初始化流程
class Compiler {
constructor(options) {
// 1. 合并配置(命令行参数 + 配置文件)
this.options = this.mergeOptions(options);
// 2. 初始化 hooks(基于 tapable)
this.hooks = {
run: new AsyncSeriesHook(['compiler']),
compile: new SyncHook(['params']),
emit: new AsyncSeriesHook(['compilation']),
done: new AsyncSeriesHook(['stats']),
};
// 3. 加载插件
if (options.plugins) {
options.plugins.forEach(plugin => {
plugin.apply(this);
});
}
}
}
2. 编译阶段
从入口出发,递归解析依赖,构建模块依赖图。
// 简化的编译流程
class Compilation {
constructor(compiler) {
this.compiler = compiler;
this.modules = []; // 所有模块
this.chunks = []; // 代码块
this.assets = {}; // 输出资源
}
// 构建模块
buildModule(modulePath) {
// 1. 读取源代码
let sourceCode = fs.readFileSync(modulePath, 'utf-8');
// 2. 调用 loader 转换
sourceCode = this.runLoaders(modulePath, sourceCode);
// 3. 解析 AST,收集依赖
const ast = parser.parse(sourceCode, {
sourceType: 'module',
});
const dependencies = [];
// 4. 遍历 AST,找出 import/require
traverse(ast, {
ImportDeclaration: ({ node }) => {
dependencies.push(node.source.value);
},
CallExpression: ({ node }) => {
if (node.callee.name === 'require') {
dependencies.push(node.arguments[0].value);
}
},
});
// 5. 转换代码(ES6 -> ES5)
const { code } = transformFromAst(ast, null, {
presets: ['@babel/preset-env'],
});
return {
id: modulePath,
code,
dependencies,
};
}
// 递归构建依赖图
build(entry) {
const entryModule = this.buildModule(entry);
this.modules.push(entryModule);
// 递归处理依赖
for (const module of this.modules) {
module.dependencies.forEach(dep => {
const depPath = this.resolvePath(module.id, dep);
if (!this.modules.find(m => m.id === depPath)) {
const depModule = this.buildModule(depPath);
this.modules.push(depModule);
}
});
}
}
// 运行 loader
runLoaders(modulePath, sourceCode) {
const rules = this.compiler.options.module?.rules || [];
for (const rule of rules) {
if (rule.test.test(modulePath)) {
// loader 从右到左执行
const loaders = Array.isArray(rule.use) ? rule.use : [rule.use];
for (let i = loaders.length - 1; i >= 0; i--) {
const loader = require(loaders[i]);
sourceCode = loader(sourceCode);
}
}
}
return sourceCode;
}
}
3. 输出阶段
将模块组合成 chunk,生成最终文件。
class Compilation {
// 生成 chunk
seal() {
// 根据入口创建 chunk
const chunk = {
name: 'main',
modules: this.modules,
};
this.chunks.push(chunk);
}
// 生成最终代码
createAssets() {
for (const chunk of this.chunks) {
const filename = this.compiler.options.output.filename
.replace('[name]', chunk.name);
// 生成自执行函数包裹的代码
this.assets[filename] = this.generateBundle(chunk);
}
}
// 生成 bundle 代码
generateBundle(chunk) {
const modules = {};
chunk.modules.forEach(module => {
modules[module.id] = module.code;
});
// 生成运行时代码
return `
(function(modules) {
// 模块缓存
const installedModules = {};
// require 函数
function __webpack_require__(moduleId) {
// 检查缓存
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 创建模块并缓存
const module = installedModules[moduleId] = {
exports: {}
};
// 执行模块代码
modules[moduleId].call(
module.exports,
module,
module.exports,
__webpack_require__
);
return module.exports;
}
// 加载入口模块
return __webpack_require__("${chunk.modules[0].id}");
})(${JSON.stringify(modules)});
`;
}
// 写入文件
emitAssets() {
const outputPath = this.compiler.options.output.path;
// 确保目录存在
if (!fs.existsSync(outputPath)) {
fs.mkdirSync(outputPath, { recursive: true });
}
// 写入文件
Object.entries(this.assets).forEach(([filename, content]) => {
const filePath = path.join(outputPath, filename);
fs.writeFileSync(filePath, content);
});
}
}
完整流程串联
class Compiler {
run(callback) {
// 触发 run 钩子
this.hooks.run.callAsync(this, err => {
if (err) return callback(err);
// 开始编译
this.compile((err, compilation) => {
if (err) return callback(err);
// 触发 emit 钩子
this.hooks.emit.callAsync(compilation, err => {
if (err) return callback(err);
// 输出文件
compilation.emitAssets();
// 触发 done 钩子
this.hooks.done.callAsync({ compilation }, callback);
});
});
});
}
compile(callback) {
// 触发 compile 钩子
this.hooks.compile.call();
// 创建 compilation
const compilation = new Compilation(this);
// 构建模块
compilation.build(this.options.entry);
// 生成 chunk
compilation.seal();
// 生成资源
compilation.createAssets();
callback(null, compilation);
}
}
流程图
┌─────────────────────────────────────────────────────────────┐
│ 初始化阶段 │
├─────────────────────────────────────────────────────────────┤
│ 1. 读取配置文件 + 命令行参数 │
│ 2. 创建 Compiler 对象 │
│ 3. 初始化 hooks │
│ 4. 加载插件(调用 plugin.apply) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 编译阶段 │
├─────────────────────────────────────────────────────────────┤
│ 1. 从 entry 开始 │
│ 2. 调用 loader 转换模块 │
│ 3. 解析 AST,收集依赖 │
│ 4. 递归处理依赖模块 │
│ 5. 构建模块依赖图 │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 输出阶段 │
├─────────────────────────────────────────────────────────────┤
│ 1. 根据依赖图生成 chunk │
│ 2. 生成运行时代码 │
│ 3. 触发 emit 钩子(插件可修改输出) │
│ 4. 写入文件到 output.path │
└─────────────────────────────────────────────────────────────┘
关键点
- 初始化:合并配置、创建 Compiler、加载插件、初始化 hooks
- 编译:从入口递归解析,loader 转换代码,AST 分析依赖,构建依赖图
- 输出:依赖图 → chunk → bundle 代码 → 写入文件
- Tapable:整个流程通过 hooks 串联,插件可以在各阶段介入
- Loader:负责文件转换,从右到左执行
- Plugin:通过订阅 hooks 扩展功能,贯穿整个构建流程
目录