说说 webpack 的构建流程?
问题解析
这道题考察对 Webpack 内部工作机制的理解。面试官希望看到你能清晰地描述从配置文件读取到最终产物输出的完整流程,包括初始化、编译构建、输出三个主要阶段,以及各阶段的关键操作。
核心概念
Webpack 构建三大阶段
┌─────────────────────────────────────────────────────────────────────┐
│ Webpack 构建流程 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 初始化流程 编译构建流程 输出流程 │
│ ┌─────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 读取配置 │ --> │ compile │ --> │ seal(封装) │ │
│ │ 创建Compiler│ │ make │ │ emit(发射) │ │
│ │ 注册插件 │ │ build-module│ │ 生成文件 │ │
│ └─────────┘ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
核心对象
| 对象 | 作用 | 生命周期 |
|---|---|---|
| Compiler | 编译器对象,控制整个构建流程 | 全局唯一,贯穿始终 |
| Compilation | 单次编译的上下文,包含模块资源 | 每次编译新建 |
| Module | 模块对象,表示一个文件 | 编译时创建 |
| Chunk | 代码块,由多个模块组成 | seal 阶段生成 |
| Bundle | 最终输出的文件 | emit 阶段生成 |
详细解答
第一阶段:初始化流程
1.1 读取配置
// 1. 从命令行参数或配置文件读取配置
// webpack --config webpack.config.js
// 2. 合并配置(命令行 > 配置文件 > 默认配置)
const options = merge(defaultOptions, configFileOptions, cliOptions);
1.2 创建 Compiler 对象
// 伪代码展示 Compiler 创建过程
class Compiler extends Tapable {
constructor(context) {
super();
this.hooks = {
// 定义各种生命周期钩子
entryOption: new SyncBailHook(['context', 'entry']),
beforeRun: new AsyncSeriesHook(['compiler']),
run: new AsyncSeriesHook(['compiler']),
beforeCompile: new AsyncSeriesHook(['params']),
compile: new SyncHook(['params']),
make: new AsyncParallelHook(['compilation']),
afterCompile: new AsyncSeriesHook(['compilation']),
emit: new AsyncSeriesHook(['compilation']),
afterEmit: new AsyncSeriesHook(['compilation']),
done: new AsyncSeriesHook(['stats'])
};
this.options = {}; // 配置选项
this.context = context; // 项目根目录
}
run(callback) {
// 启动构建流程
this.hooks.beforeRun.callAsync(this, err => {
if (err) return callback(err);
this.hooks.run.callAsync(this, err => {
if (err) return callback(err);
this.compile(onCompiled);
});
});
}
}
1.3 注册插件
// 遍历 plugins 数组,调用每个插件的 apply 方法
options.plugins.forEach(plugin => {
plugin.apply(compiler);
});
// 插件通过 Tapable 钩子监听事件
class MyPlugin {
apply(compiler) {
compiler.hooks.emit.tap('MyPlugin', compilation => {
console.log('文件发射时执行');
});
}
}
第二阶段:编译构建流程
2.1 compile - 创建 Compilation
// 伪代码
compile(callback) {
const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {
if (err) return callback(err);
this.hooks.compile.call(params);
// 创建 Compilation 对象(每次编译新建)
const compilation = this.newCompilation(params);
this.hooks.make.callAsync(compilation, err => {
if (err) return callback(err);
// 编译完成后的处理
compilation.finish(err => {
if (err) return callback(err);
compilation.seal(err => {
if (err) return callback(err);
this.hooks.afterCompile.callAsync(compilation, err => {
if (err) return callback(err);
return callback(null, compilation);
});
});
});
});
});
}
2.2 make - 分析入口,创建模块
// 从 entry 开始,递归构建模块依赖图
make(compilation, callback) {
const entry = this.options.entry;
// 处理每个入口
for (const name of Object.keys(entry)) {
const dep = new SingleEntryDependency(entry[name]);
compilation.addEntry(this.context, dep, name, callback);
}
}
2.3 build-module - 编译模块
// 模块构建过程
buildModule(module, callback) {
// 1. 读取文件内容
const source = fs.readFileSync(module.resource);
// 2. 使用 Loader 转换
const loaders = this.getLoaders(module);
const transformedSource = this.runLoaders(loaders, source);
// 3. 使用 Parser 解析(Acorn 解析为 AST)
const ast = this.parser.parse(transformedSource);
// 4. 遍历 AST,收集依赖
const dependencies = [];
this.parser.walkStatements(ast.statements, statement => {
if (statement.type === 'ImportDeclaration') {
dependencies.push(statement.source.value);
}
});
// 5. 存储依赖,递归处理
module.dependencies = dependencies;
// 6. 对每个依赖递归调用 buildModule
async.forEach(dependencies, (dep, done) => {
const newModule = new Module(dep);
this.buildModule(newModule, done);
}, callback);
}
2.4 Loader 执行过程
// Loader 链式执行,从右到左
// use: ['style-loader', 'css-loader', 'sass-loader']
// 执行顺序: sass-loader -> css-loader -> style-loader
function runLoaders(loaders, source) {
let result = source;
// 从最后一个 loader 开始
for (let i = loaders.length - 1; i >= 0; i--) {
const loader = require(loaders[i]);
result = loader(result);
}
return result;
}
// 实际使用 loader-runner 库处理更复杂的场景
// 支持 pitch loader、异步 loader、inline loader 等
第三阶段:输出流程
3.1 seal - 封装,生成 Chunks
// seal 阶段主要工作
seal(callback) {
this.hooks.seal.call();
// 1. 优化模块依赖图
this.hooks.optimizeDependencies.call(this.modules);
// 2. 创建 Chunks
// 每个入口对应一个 Chunk
for (const entryModule of this.entryModules) {
const chunk = new Chunk(entryModule.name);
chunk.addModule(entryModule);
// 递归添加所有依赖模块
this.addChunkModules(chunk, entryModule);
this.chunks.push(chunk);
}
// 3. 优化 Chunks(代码分割、Tree Shaking)
this.hooks.optimizeChunks.call(this.chunks);
// 4. 生成 Chunk 资源
this.createChunkAssets();
callback();
}
3.2 emit - 发射,输出文件
// emit 阶段
emit(compilation, callback) {
const outputPath = this.options.output.path;
this.hooks.emit.callAsync(compilation, err => {
if (err) return callback(err);
// 遍历所有 assets,写入文件系统
for (const file of Object.keys(compilation.assets)) {
const source = compilation.assets[file].source();
const outputFile = path.join(outputPath, file);
// 写入文件
fs.writeFileSync(outputFile, source);
}
this.hooks.afterEmit.callAsync(compilation, callback);
});
}
深入理解
完整流程图
┌─────────────────────────────────────────────────────────────────────────────┐
│ Webpack 完整构建流程 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 初始化阶段 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 1. 读取配置(命令行参数 + 配置文件 + 默认配置) │ │
│ │ 2. 实例化 Compiler(全局唯一) │ │
│ │ 3. 遍历 plugins,执行 plugin.apply(compiler) │ │
│ │ 4. 处理 webpack.config.js 中的其他配置 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 编译阶段 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 5. 执行 compiler.run() │ │
│ │ 6. 触发 beforeRun、run 钩子 │ │
│ │ 7. 创建 Compilation(单次编译上下文) │ │
│ │ 8. 触发 make 钩子,开始构建模块 │ │
│ │ └── addEntry() 添加入口 │ │
│ │ └── 对每个模块: │ │
│ │ ├── 读取文件内容 │ │
│ │ ├── 使用 Loader 转换 │ │
│ │ ├── Parser 解析为 AST │ │
│ │ ├── 遍历 AST 收集依赖 │ │
│ │ └── 递归处理依赖 │ │
│ │ 9. 触发 finishModules 钩子 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 封装阶段 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 10. 调用 compilation.seal() │ │
│ │ 11. 优化模块依赖(Tree Shaking) │ │
│ │ 12. 创建 Chunks(根据 entry 和动态导入) │ │
│ │ 13. 优化 Chunks(代码分割、合并) │ │
│ │ 14. 生成 Chunk 的 hash │ │
│ │ 15. 为每个 Chunk 生成 assets 资源 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 输出阶段 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 16. 触发 emit 钩子 │ │
│ │ 17. 将 assets 写入输出目录 │ │
│ │ 18. 触发 afterEmit 钩子 │ │
│ │ 19. 触发 done 钩子,构建完成 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
关键钩子(Hooks)执行顺序
// 完整的钩子执行顺序
compiler.hooks.entryOption // 处理 entry 配置
compiler.hooks.beforeRun // 开始构建前
compiler.hooks.run // 开始构建
compiler.hooks.beforeCompile // 编译前
compiler.hooks.compile // 编译开始
compiler.hooks.make // 从 entry 开始构建
compilation.hooks.buildModule // 构建单个模块
compilation.hooks.succeedModule // 模块构建成功
compiler.hooks.finishMake // 完成 make
compilation.hooks.seal // 封装开始
compilation.hooks.optimizeDependencies // 优化依赖
compilation.hooks.optimizeChunks // 优化 chunks
compilation.hooks.beforeChunkAssets // 生成 chunk 资源前
compilation.hooks.additionalAssets // 添加额外资源
compiler.hooks.emit // 输出文件前
compiler.hooks.afterEmit // 输出文件后
compiler.hooks.done // 构建完成
模块依赖图构建示例
// 入口文件 index.js
import a from './a.js';
import b from './b.js';
console.log(a, b);
// a.js
import c from './c.js';
export default 'a' + c;
// b.js
export default 'b';
// c.js
export default 'c';
// 构建的依赖图
const dependencyGraph = {
'./index.js': {
code: '...',
dependencies: ['./a.js', './b.js']
},
'./a.js': {
code: '...',
dependencies: ['./c.js']
},
'./b.js': {
code: '...',
dependencies: []
},
'./c.js': {
code: '...',
dependencies: []
}
};
最佳实践
1. 利用生命周期钩子开发插件
// 自定义插件,统计构建时间
class BuildTimePlugin {
apply(compiler) {
let startTime;
compiler.hooks.beforeCompile.tap('BuildTimePlugin', () => {
startTime = Date.now();
console.log('构建开始...');
});
compiler.hooks.done.tap('BuildTimePlugin', () => {
const duration = Date.now() - startTime;
console.log(`构建完成,耗时: ${duration}ms`);
});
}
}
module.exports = BuildTimePlugin;
2. 分析模块依赖
// 使用 webpack-bundle-analyzer
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false,
reportFilename: 'bundle-report.html'
})
]
};
3. 优化构建速度
module.exports = {
// 1. 缩小 Loader 范围
module: {
rules: [
{
test: /\.js$/,
include: path.resolve(__dirname, 'src'), // 只处理 src 目录
use: 'babel-loader'
}
]
},
// 2. 配置 resolve 优化
resolve: {
modules: [path.resolve(__dirname, 'node_modules')], // 指定模块查找路径
extensions: ['.js', '.jsx', '.ts', '.tsx'], // 省略后缀
alias: {
'@': path.resolve(__dirname, 'src') // 路径别名
}
},
// 3. 使用持久化缓存
cache: {
type: 'filesystem'
}
};
面试要点
回答思路
- 概述:Webpack 构建分为初始化、编译构建、输出三大阶段
- 初始化:读取配置 -> 创建 Compiler -> 注册插件
- 编译构建:compile -> make -> build-module,递归处理依赖,使用 Loader 转换
- 输出:seal 生成 Chunks -> emit 输出文件
- 补充:可以提及 Tapable 钩子系统、Compiler/Compilation 区别
常见追问
Q: Compiler 和 Compilation 的区别?
A: Compiler 是全局唯一的编译器对象,控制整个构建流程;Compilation 是单次编译的上下文,每次构建都会新建,包含当前编译的模块和资源信息。
Q: Loader 是在哪个阶段执行的?
A: Loader 在 build-module 阶段执行,用于将非 JavaScript 资源转换为 Webpack 可处理的模块。
Q: Plugin 是如何工作的?
A: Plugin 通过 Tapable 钩子系统工作,在编译生命周期的各个阶段注册回调函数,扩展 Webpack 功能。
Q: 模块依赖图是如何构建的?
A: 从 entry 开始,读取文件内容,使用 Parser 解析 AST,遍历 AST 收集 import/require 依赖,然后递归处理每个依赖模块,最终形成完整的依赖图。
Q: Tree Shaking 发生在哪个阶段?
A: Tree Shaking 在 seal 阶段进行,通过静态分析 ES Modules 的导出/导入关系,标记未使用的代码,在生成代码时排除。
一句话总结
Webpack 构建流程分为三个阶段:初始化阶段读取配置并创建 Compiler 对象;编译构建阶段从入口开始递归解析模块依赖,使用 Loader 转换代码;输出阶段通过 seal 生成 Chunks,最终通过 emit 将资源写入文件系统。