Webpack 深入原理
Webpack 架构、编译流程、插件系统深度解析
六、Webpack 深入原理
6.1 完整编译流程
javascript
/*
┌─────────────────────────────────────────────────────────────────┐
│ Webpack 编译流程详解 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 初始化阶段 │
│ │ │
│ ├─ 读取配置文件 (webpack.config.js) │
│ ├─ 合并 CLI 参数 │
│ ├─ 创建 Compiler 对象 │
│ └─ 加载所有配置的 Plugin │
│ │
│ 2. 编译阶段 (make) │
│ │ │
│ ├─ 从 entry 开始 │
│ ├─ 调用 Loader 转换模块 │
│ ├─ 使用 acorn 解析为 AST │
│ ├─ 遍历 AST 找到 require/import │
│ ├─ 递归处理依赖模块 │
│ └─ 构建模块依赖图 (ModuleGraph) │
│ │
│ 3. 生成阶段 (seal) │
│ │ │
│ ├─ 根据依赖图生成 Chunk │
│ ├─ 对 Chunk 进行优化 │
│ │ ├─ Tree Shaking │
│ │ ├─ Scope Hoisting │
│ │ └─ Code Splitting │
│ └─ 生成最终代码 │
│ │
│ 4. 输出阶段 (emit) │
│ │ │
│ ├─ 生成文件内容 │
│ ├─ 写入文件系统 │
│ └─ 触发 done 钩子 │
│ │
└─────────────────────────────────────────────────────────────────┘
*/6.2 Tapable 钩子系统
javascript
// Webpack 的插件系统基于 Tapable
// Tapable 提供各种钩子类型
const {
SyncHook, // 同步串行
SyncBailHook, // 同步串行,返回非 undefined 时终止
SyncWaterfallHook, // 同步串行,上一个返回值传给下一个
AsyncParallelHook, // 异步并行
AsyncSeriesHook // 异步串行
} = require('tapable');
// Compiler 核心钩子
class Compiler {
constructor() {
this.hooks = {
// 编译开始
run: new AsyncSeriesHook(['compiler']),
// 编译完成
done: new AsyncSeriesHook(['stats']),
// 编译失败
failed: new SyncHook(['error']),
// 输出前
emit: new AsyncSeriesHook(['compilation']),
// 输出后
afterEmit: new AsyncSeriesHook(['compilation']),
};
}
}
// 插件注册方式
class MyPlugin {
apply(compiler) {
// 同步钩子用 tap
compiler.hooks.done.tap('MyPlugin', (stats) => {
console.log('编译完成');
});
// 异步钩子用 tapAsync 或 tapPromise
compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
setTimeout(() => {
console.log('异步操作完成');
callback();
}, 1000);
});
}
}6.3 Loader 执行机制
javascript
// Loader 链式调用 (从右到左,从下到上)
module: {
rules: [{
test: /\.scss$/,
use: [
'style-loader', // 3. 将 CSS 注入 DOM
'css-loader', // 2. 处理 CSS 中的 @import 和 url()
'sass-loader' // 1. 将 SCSS 编译为 CSS
]
}]
}
// Loader 执行流程
/*
scss 源码
│
▼ sass-loader (normal)
│
css 代码
│
▼ css-loader (normal)
│
JS 模块代码 (包含 CSS 依赖)
│
▼ style-loader (normal)
│
最终 JS 代码 (运行时注入样式)
*/
// Pitch 阶段 (从左到右)
// 可以跳过后续 loader
module.exports = function(source) {
return transform(source);
};
module.exports.pitch = function(remainingRequest) {
// 如果 pitch 返回值,跳过后续 loader
if (shouldSkip) {
return 'module.exports = "cached content"';
}
};6.4 手写简易 Webpack
javascript
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const babel = require('@babel/core');
// 1. 解析单个模块
function parseModule(filePath) {
const content = fs.readFileSync(filePath, 'utf-8');
// 解析为 AST
const ast = parser.parse(content, {
sourceType: 'module'
});
// 收集依赖
const dependencies = [];
traverse(ast, {
ImportDeclaration({ node }) {
dependencies.push(node.source.value);
}
});
// 转换代码
const { code } = babel.transformFromAstSync(ast, null, {
presets: ['@babel/preset-env']
});
return { filePath, dependencies, code };
}
// 2. 构建依赖图
function buildDependencyGraph(entry) {
const entryModule = parseModule(entry);
const modules = [entryModule];
const moduleMap = new Map();
// BFS 遍历所有依赖
for (const module of modules) {
moduleMap.set(module.filePath, module);
const dirname = path.dirname(module.filePath);
module.dependencies.forEach(dep => {
const depPath = path.resolve(dirname, dep);
if (!moduleMap.has(depPath)) {
modules.push(parseModule(depPath));
}
});
}
return modules;
}
// 3. 生成 Bundle
function generateBundle(modules, entry) {
let modulesCode = '';
modules.forEach(module => {
modulesCode += `
"${module.filePath}": function(require, module, exports) {
${module.code}
},
`;
});
return `
(function(modules) {
const cache = {};
function require(moduleId) {
if (cache[moduleId]) {
return cache[moduleId].exports;
}
const module = cache[moduleId] = { exports: {} };
modules[moduleId](require, module, module.exports);
return module.exports;
}
require("${entry}");
})({${modulesCode}});
`;
}
// 使用
const modules = buildDependencyGraph('./src/index.js');
const bundle = generateBundle(modules, './src/index.js');
fs.writeFileSync('./dist/bundle.js', bundle);