Webpack4 源码分析
约 1452 字大约 5 分钟
webpack源码分析原理
2026-04-15
Webpack 核心概念
Compiler 和 Compilation
Compiler:Webpack 运行的核心对象,从启动到结束都会存在。负责整个构建流程。
// Compiler 的主要属性
class Compiler {
constructor(context) {
this.hooks = {
run: new SyncHook(),
compile: new SyncHook(),
emit: new AsyncSeriesHook(),
done: new SyncHook()
};
this.options = options;
this.context = context;
this.inputFileSystem = inputFileSystem;
this.outputFileSystem = outputFileSystem;
}
}Compilation:一次新的编译过程,包含当前的模块、编译资源、变化文件等信息。
class Compilation {
constructor(compiler) {
this.compiler = compiler;
this.modules = [];
this.chunks = [];
this.assets = {};
this.hooks = {
buildModule: new SyncHook(),
succeedModule: new SyncHook(),
finishModules: new SyncHook()
};
}
}Module 和 Chunk
Module:代表一个模块文件,可以是 JS、CSS、图片等。
class Module {
constructor(type, factoryInfo) {
this.type = type;
this.context = factoryInfo.context;
this.resource = factoryInfo.resource;
this.dependencies = [];
this.blocks = [];
}
}Chunk:代码块,由多个 Module 组成,最终输出为一个 bundle 文件。
class Chunk {
constructor(name) {
this.name = name;
this.id = null;
this.modules = [];
this.files = [];
this.parents = [];
this.children = [];
}
}构建流程源码分析
整体流程
webpack(config)
↓
创建 Compiler 对象
↓
调用 compiler.run()
↓
创建 Compilation 对象
↓
make 阶段(构建模块)
↓
seal 阶段(封装 chunk)
↓
emit 阶段(输出文件)
↓
done 回调初始化阶段
// webpack.js
const webpack = (options, callback) => {
// 1. 参数处理
const options = new WebpackOptionsDefiner().processOptions(webpackOptions);
// 2. 创建 Compiler
const compiler = new Compiler(options.context);
compiler.options = options;
// 3. 加载插件
new NodeEnvironmentPlugin({ infrastructureLogging: options.infrastructureLogging })
.apply(compiler);
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === 'function') {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}
// 4. 加载环境配置
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
// 5. 解析配置
new WebpackOptionsApply().process(options, compiler);
return compiler;
};编译阶段(make 流程)
// Compiler.run()
run(callback) {
const startTime = Date.now();
// 触发 run 钩子
this.hooks.run.callAsync(err => {
// 编译
this.compile(onCompiled);
});
}
// Compiler.compile()
compile(callback) {
// 创建 compilation
const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {
// 触发 make 钩子
this.hooks.make.callAsync(compilation, err => {
compilation.finish(err => {
compilation.seal(err => {
this.hooks.afterCompile.callAsync(compilation, err => {
callback(null, compilation);
});
});
});
});
});
}make 钩子的处理
// SingleEntryPlugin.js
compiler.hooks.make.tapAsync(
'SingleEntryPlugin',
(compilation, callback) => {
compilation.addEntry(
context,
entry,
name,
callback
);
}
);
// Compilation.addEntry()
addEntry(context, entry, name, callback) {
// 解析入口文件
this._addModuleChain(context, entry, (module) => {
this.entries.push(module);
}, (err, module) => {
callback(err, module);
});
}懒加载原理
代码分割机制
Webpack 通过 import() 或 require.ensure 实现代码分割。
// 动态 import
import('./module').then(module => {
// 使用模块
});
// 编译后生成独立的 chunk 文件Chunk 加载流程
// webpack 生成的 runtime 代码
__webpack_require__.e = function(chunkId) {
var promises = [];
var installedChunkData = installedChunks[chunkId];
if (installedChunkData !== 0) {
if (installedChunkData) {
promises.push(installedChunkData[2]);
} else {
// 创建 Promise
var promise = new Promise(function(resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
promises.push(installedChunkData[2] = promise);
// 创建 script 标签
var script = document.createElement('script');
script.src = __webpack_require__.p + "" + chunkId + ".js";
document.head.appendChild(script);
}
}
return Promise.all(promises);
};webpackJsonp 机制
// JSONP 加载 chunk
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([
[chunkId],
{
moduleId: function(module, exports, __webpack_require__) {
// 模块代码
}
},
[[moduleId]]
]);
// 处理 push 的数据
function webpackJsonpCallback(data) {
var chunkIds = data[0];
var moreModules = data[1];
// 标记 chunk 已加载
var resolves = [];
for (var i = 0; i < chunkIds.length; i++) {
var chunkId = chunkIds[i];
if (installedChunks[chunkId]) {
resolves.push(installedChunks[chunkId][0]);
}
installedChunks[chunkId] = 0;
}
// 添加模块
for (moduleId in moreModules) {
modules[moduleId] = moreModules[moduleId];
}
// 执行 resolve
while (resolves.length) {
resolves.shift()();
}
}热更新原理(HMR)
HMR 工作流程
文件修改
↓
Webpack 监听到变化
↓
重新编译生成新模块
↓
通过 WebSocket 发送 hash 给浏览器
↓
浏览器请求新的 hot-update.json
↓
请求新的 hot-update.js
↓
执行 module.hot.accept 回调
↓
替换旧模块WebSocket 通信机制
// webpack-dev-server 客户端
var socket = new WebSocket('ws://localhost:8080/sockjs-node');
socket.onmessage = function(e) {
var message = JSON.parse(e.data);
if (message.action === 'building') {
console.log('开始编译...');
} else if (message.action === 'built') {
// 接收新的 hash
hotCheck(true).then(updatedModules => {
// 应用更新
hotApply(updatedModules);
});
}
};HotModuleReplacementPlugin 核心逻辑
// HotModuleReplacementPlugin.js
compiler.hooks.compilation.tap(
'HotModuleReplacement',
(compilation, { normalModuleFactory }) => {
// 注入 HMR runtime
compilation.hooks.additionalTreeRuntimeRequirements.tap(
'HotModuleReplacement',
(chunk, runtimeRequirements) => {
runtimeRequirements.add(RuntimeGlobals.hmrDownloadUpdateHandlers);
}
);
}
);webpack_hot_module 实现
// 模块内部的热更新 API
module.hot = {
// 接受更新
accept: function(dependencies, callback) {
// 注册接受更新的回调
module.hot._acceptedDependencies[dependency] = callback;
},
// 使模块失效
dispose: function(callback) {
module.hot._disposeHandlers.push(callback);
},
// 检查更新
check: function(autoApply) {
return hotCheck(autoApply);
},
// 应用更新
apply: function(options) {
return hotApply(options);
}
};HMR 与 LiveReload 的区别
| 特性 | HMR | LiveReload |
|---|---|---|
| 刷新方式 | 只更新变化的模块 | 刷新整个页面 |
| 状态保持 | 是 | 否 |
| 速度 | 快 | 慢 |
| 适用场景 | 开发 SPA | 开发多页面应用 |
手写简易 Webpack
核心架构设计
class SimpleWebpack {
constructor(options) {
this.options = options;
this.root = process.cwd();
this.modules = {};
}
// 运行
run() {
const entry = this.options.entry;
this.buildModule(entry, true);
this.emitFiles();
}
// 构建模块
buildModule(modulePath, isEntry) {
// 1. 读取文件内容
let source = this.getSource(modulePath);
// 2. 转换代码(Babel)
source = this.transform(source);
// 3. 分析依赖
const { dependencies, code } = this.parse(source);
// 4. 记录模块
const moduleName = this.getModuleName(modulePath);
this.modules[moduleName] = {
code,
dependencies
};
// 5. 递归处理依赖
dependencies.forEach(dep => {
this.buildModule(dep, false);
});
}
// 输出文件
emitFiles() {
const output = this.generateBundle();
fs.writeFileSync(this.options.output.path, output);
}
}完整实现
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const { transformFromAst } = require('@babel/core');
class SimpleWebpack {
constructor(options) {
this.options = options;
this.root = process.cwd();
this.modules = {};
}
getSource(modulePath) {
return fs.readFileSync(modulePath, 'utf-8');
}
parse(source) {
const ast = parser.parse(source, {
sourceType: 'module',
plugins: ['jsx']
});
const dependencies = [];
traverse(ast, {
ImportDeclaration({ node }) {
dependencies.push(node.source.value);
}
});
const { code } = transformFromAst(ast, null, {
presets: ['@babel/preset-env']
});
return { dependencies, code };
}
buildModule(modulePath, isEntry) {
const absolutePath = path.resolve(this.root, modulePath);
const source = this.getSource(absolutePath);
const { dependencies, code } = this.parse(source);
const moduleName = './' + path.relative(this.root, absolutePath);
this.modules[moduleName] = {
code,
dependencies: dependencies.map(dep => {
const dir = path.dirname(absolutePath);
return './' + path.relative(this.root, path.resolve(dir, dep));
})
};
// 递归处理依赖
dependencies.forEach(dep => {
const depPath = path.resolve(path.dirname(absolutePath), dep);
if (!this.modules['./' + path.relative(this.root, depPath)]) {
this.buildModule(depPath, false);
}
});
}
generateBundle() {
let modules = '';
for (const moduleName in this.modules) {
modules += `'${moduleName}': function(module, exports, require) {
${this.modules[moduleName].code}
},`;
}
return `(function(modules) {
var installedModules = {};
function require(moduleName) {
if (installedModules[moduleName]) {
return installedModules[moduleName].exports;
}
var module = installedModules[moduleName] = {
exports: {}
};
modules[moduleName](module, module.exports, require);
return module.exports;
}
return require('${this.options.entry}');
})({${modules}})`;
}
run() {
this.buildModule(this.options.entry, true);
this.emitFiles();
}
emitFiles() {
const output = this.generateBundle();
fs.writeFileSync(this.options.output.path, output, 'utf-8');
console.log('打包完成!');
}
}
module.exports = SimpleWebpack;调试技巧和学习路径
调试技巧
使用 VSCode 调试
- 在 node_modules/webpack 中打断点
- 配置 launch.json
添加 console.log
- 在关键流程中添加日志
使用 webpack 的 debug 模式
node --inspect-brk node_modules/.bin/webpack
学习路径
- 第一阶段:理解 Compiler、Compilation、Module、Chunk
- 第二阶段:理解构建流程(make、seal、emit)
- 第三阶段:理解 Tapable 事件机制
- 第四阶段:阅读 Plugin 源码
- 第五阶段:手写简易 Webpack
常见问题
1. chunkhash 和 contenthash 的区别?
- chunkhash:根据 chunk 内容生成
- contenthash:根据提取的内容生成(常用于 CSS)
2. 如何实现按需加载?
使用动态 import() 语法,Webpack 会自动分割代码。
3. HMR 不生效怎么办?
- 确认已添加 HotModuleReplacementPlugin
- 确认 devServer.hot 为 true
- 模块中已添加 module.hot.accept
