"你每天都在用Webpack,却不知道它把代码塞进bundle的黑魔法? 当面试官问起模块加载原理,你是否只能支支吾吾?今天,我将用 200行代码 撕开Webpack的神秘面纱,带你手写一个核心打包器!无需Loader和插件,仅凭纯JavaScript实现依赖分析、AST解析、闭包封装三大核心机制。读完本文,你不仅能吊打打包原理面试题,还能亲手改造Webpack!"
一、暴力拆解Webpack
在动手编码前,我们先理解Webpack的核心工作流程:
- 入口分析:从配置的入口文件开始解析
- 依赖收集:构建模块依赖图(Module Graph)
- 代码转换:通过Loader处理不同资源
- 打包输出:生成可在浏览器运行的bundle
本次实现的mini-webpack将实现最核心的前两步和最后一步,重点展示依赖分析和打包原理。
图解4大核心阶段(入口→依赖收集→转换→输出)
以下是Webpack 核心工作原理流程图,直观展示其工作流程和关键环节:
流程图说明:
-
入口分析(起点) :
- 从配置的入口文件(Entry)开始解析
- 绿色块表示起始点
-
依赖收集阶段:
-
解析文件生成 AST 抽象语法树
-
识别所有 import/require 语句
-
递归构建完整的模块依赖图
-
代码转换阶段
- 调用匹配的 Loader 处理不同资源
- 转换操作:ES6→ES5、SASS→CSS、图片优化等
- 打包输出阶段:
-
将模块封装为 Chunk(代码块)
-
执行 Tree Shaking、代码压缩等优化
-
生成可在浏览器运行的 Bundle 文件
-
橙色块表示最终产物
关键特性标注:
-
依赖图(Module Graph) :递归收集形成的模块依赖关系网
-
Loader 系统:处理非 JS 资源的转换管道
-
Chunk 生成:根据分包策略(SplitChunks)生成代码块
-
Bundle:包含运行时代码的最终输出文件
二、mini_webpack项目结构设计
创建以下文件结构:
mini-webpack/
├── src/
│ ├── compiler.js # 编译器核心
│ └── parser.js # 模块解析器
├── example/
│ ├── entry.js # 示例入口文件
│ ├── a.js # 示例模块
│ └── b.js # 示例模块
├── index.js # 入口文件
└── package.json
三、核心代码实现
1. 模块解析器 (parser.js)
const fs = require('fs');
const path = require('path');
const babylon = require('babylon'); // 用于生成AST
const traverse = require('babel-traverse').default; // 用于遍历AST
let ID = 0; // 为每个模块分配唯一ID
// 创建模块信息
function createModuleInfo(filePath) {
// 读取文件内容
const content = fs.readFileSync(filePath, 'utf-8');
// 使用Babylon生成AST(抽象语法树)
const ast = babylon.parse(content, {
sourceType: 'module',
});
// 收集模块的依赖
const dependencies = [];
// 遍历AST,找到所有import声明
traverse(ast, {
ImportDeclaration: ({ node }) => {
dependencies.push(node.source.value);
},
CallExpression: ({ node }) => {
if (node.callee.name === 'require') {
dependencies.push(node.arguments[0].value);
}
}
});
// 为模块分配唯一ID
const id = ID++;
// 返回模块信息对象
return {
id,
filePath,
dependencies,
code: content,
};
}
// 构建依赖关系图
function createDependencyGraph(entry) {
// 从入口文件开始解析
const entryInfo = createModuleInfo(entry);
// 依赖关系图队列
const graphQueue = [entryInfo];
// 遍历所有模块,广度优先
for (const module of graphQueue) {
module.mapping = {};
const dirname = path.dirname(module.filePath);
module.dependencies.forEach(relativePath => {
// 将相对路径转为绝对路径
const absolutePath = path.join(dirname, relativePath);
// 防止重复处理
if (!graphQueue.some(m => m.filePath === absolutePath)) {
const childInfo = createModuleInfo(absolutePath);
module.mapping[relativePath] = childInfo.id;
graphQueue.push(childInfo);
}
});
}
return graphQueue;
}
module.exports = { createDependencyGraph };
代码思路(模块解析器 (parser.js) 工作流程):
关键数据结构:
{
id: 0, // 模块唯一ID
filePath: '/src/entry.js', // 绝对路径
dependencies: ['./a.js','./b.js'], // 依赖列表
code: '原始代码内容', // 模块源代码
mapping: {} // 路径到ID的映射(后续添加)
}
2. 编译器核心 (compiler.js)
const fs = require('fs');
const path = require('path');
const { createDependencyGraph } = require('./parser');
class MiniWebpack {
constructor(options) {
this.options = options;
}
// 打包方法
bundle() {
const { entry, output } = this.options;
// 创建依赖图
const graph = createDependencyGraph(path.resolve(entry));
// 生成模块对象字符串
const modules = graph.map(module => {
return `
${module.id}: [
function(require, module, exports) {
${module.code}
},
${JSON.stringify(module.mapping)}
],
`;
}).join('');
// 生成最终bundle内容
const bundleContent = `
(function(modules) {
// 缓存已加载模块
const installedModules = {};
// require函数实现
function require(id) {
// 检查模块是否已加载
if (installedModules[id]) {
return installedModules[id].exports;
}
// 创建新模块
const [fn, mapping] = modules[id];
const module = {
exports: {}
};
// 缓存模块
installedModules[id] = module;
// 处理路径映射
function localRequire(relativePath) {
return require(mapping[relativePath]);
}
// 执行模块函数
fn(localRequire, module, module.exports);
// 返回模块导出
return module.exports;
}
// 从入口模块开始执行
require(0);
})({
${modules}
})
`;
// 确保输出目录存在
const outputDir = path.dirname(output.path);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// 写入bundle文件
fs.writeFileSync(
path.resolve(output.path, output.filename),
bundleContent
);
}
}
module.exports = MiniWebpack;
3. 入口文件 (index.js)
const MiniWebpack = require('./src/compiler');
const path = require('path');
const config = {
entry: './example/entry.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
}
};
const compiler = new MiniWebpack(config);
compiler.bundle();
console.log('打包完成!');
四、运行与效果
示例文件:
entry.js
const message = require('./a.js');
const count = require('./b.js');
console.log(message);
console.log('计数结果:', count.add(5, 3));
a.js
module.exports = '你好,我是模块A!';
b.js
module.exports = {
add: (a, b) => a + b,
subtract: (a, b) => a - b
};
运行我们的mini-webpack:
node index.js
在dist目录下生成bundle.js:
(function(modules) {
// ... (runtime代码)
})({
0: [
function(require, module, exports) {
const message = require('./a.js');
const count = require('./b.js');
console.log(message);
console.log('计数结果:', count.add(5, 3));
},
{"./a.js":1,"./b.js":2}
],
1: [
function(require, module, exports) {
module.exports = '你好,我是模块A!';
},
{}
],
2: [
function(require, module, exports) {
module.exports = {
add: (a, b) => a + b,
subtract: (a, b) => a - b
};
},
{}
],
})
在浏览器中运行这个bundle,控制台将输出:
你好,我是模块A!
计数结果: 8
五、浏览器运行bundle原理解析
既然明白了webpack如何打包源码,接下来可以再了解下浏览器识别打包后代码后如何正确运行~
1. 依赖图构建
通过AST解析技术(使用Babylon):
- 识别
require/import语句 - 递归收集所有依赖
- 为每个模块创建唯一ID
- 建立模块间的映射关系
2. 运行时机制
生成的自执行函数包含:
- 模块缓存:避免重复加载
- require函数:实现模块加载
- 模块执行环境:提供module/exports对象
3. 模块加载过程
-
require 函数执行流程:
总结与结尾:
"当你亲手用200行代码让破碎的模块在浏览器中复活时,Webpack就不再是黑箱! 本文实现的迷你打包器虽未包含Loader和插件系统,却赤裸裸揭露了工程化核心:AST解析依赖 → 闭包封装作用域 → 运行时递归加载。下次面试被问"bundle.js为什么能运行?",你大可冷笑一声:"不过是个自执行函数配个require"!