手写自己的 Webpack4
约 1348 字大约 4 分钟
webpack手写实现源码
2026-04-15
Webpack 核心原理回顾
打包工具的核心职责
Webpack 的核心功能:
- 模块解析:找到所有依赖的模块
- 代码转换:将各种语言转换为浏览器可执行的代码
- 依赖图构建:建立模块之间的依赖关系
- 资源打包:将所有模块打包成一个或多个文件
模块依赖分析
// index.js
import { add } from './math.js';
console.log(add(1, 2));
// math.js
export function add(a, b) {
return a + b;
}
// 依赖关系
// index.js → math.js架构设计
Compiler 类设计
class Compiler {
constructor(options) {
this.options = options;
this.root = process.cwd();
this.modules = {};
this.chunks = [];
}
// 运行
run() {
const entry = this.options.entry;
this.buildModule(entry, true);
this.emitFiles();
}
}
module.exports = Compiler;配置解析
// 默认配置
const defaultOptions = {
mode: 'production',
entry: './src/index.js',
output: {
path: './dist/main.js',
filename: 'main.js'
},
module: {
rules: []
},
resolve: {
extensions: ['.js', '.jsx', '.json']
}
};
// 合并配置
function normalizeOptions(userOptions) {
return {
...defaultOptions,
...userOptions,
output: {
...defaultOptions.output,
...userOptions.output
}
};
}核心实现
第一步:读取入口文件
const fs = require('fs');
const path = require('path');
class Compiler {
getSource(modulePath) {
const absolutePath = path.resolve(this.root, modulePath);
if (!fs.existsSync(absolutePath)) {
throw new Error(`模块不存在: ${absolutePath}`);
}
return fs.readFileSync(absolutePath, 'utf-8');
}
}第二步:解析模块依赖(使用 AST)
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
class Compiler {
getDependencies(source) {
// 解析代码生成 AST
const ast = parser.parse(source, {
sourceType: 'module',
plugins: ['jsx', 'typescript']
});
const dependencies = [];
// 遍历 AST 节点,找到 import 语句
traverse(ast, {
ImportDeclaration({ node }) {
dependencies.push(node.source.value);
},
CallExpression({ node }) {
// 处理 require() 和 import()
if (
node.callee.name === 'require' ||
node.callee.type === 'Import'
) {
const arg = node.arguments[0];
if (arg.type === 'StringLiteral') {
dependencies.push(arg.value);
}
}
}
});
return dependencies;
}
}第三步:转换代码(使用 Babel)
const { transformFromAst } = require('@babel/core');
class Compiler {
transform(source, modulePath) {
// 获取该模块对应的 loader
const loaders = this.getLoaders(modulePath);
// 从右到左执行 loader
let result = source;
for (let i = loaders.length - 1; i >= 0; i--) {
result = loaders[i](result);
}
// 如果没有 loader,使用 babel 转换
if (loaders.length === 0) {
const ast = parser.parse(source, {
sourceType: 'module'
});
const { code } = transformFromAst(ast, null, {
presets: ['@babel/preset-env']
});
result = code;
}
return result;
}
getLoaders(modulePath) {
const rules = this.options.module.rules;
const matchedLoaders = [];
for (const rule of rules) {
const test = rule.test;
if (test && test.test(modulePath)) {
const loaders = Array.isArray(rule.use)
? rule.use
: [rule.use];
matchedLoaders.push(...loaders);
}
}
return matchedLoaders;
}
}第四步:生成依赖图
class Compiler {
buildModule(modulePath, isEntry) {
// 1. 读取文件
const source = this.getSource(modulePath);
// 2. 转换代码
const transformedSource = this.transform(source, modulePath);
// 3. 解析依赖
const dependencies = this.getDependencies(transformedSource);
// 4. 获取模块名称
const moduleName = this.getModuleName(modulePath);
// 5. 保存模块
this.modules[moduleName] = {
source: transformedSource,
dependencies: dependencies.map(dep => {
const dir = path.dirname(modulePath);
return this.resolveModule(dep, dir);
})
};
// 6. 递归处理依赖
this.modules[moduleName].dependencies.forEach(dep => {
if (!this.modules[dep]) {
this.buildModule(dep, false);
}
});
}
getModuleName(modulePath) {
return './' + path.relative(this.root, modulePath);
}
resolveModule(modulePath, dir) {
const absolutePath = path.resolve(dir, modulePath);
return './' + path.relative(this.root, absolutePath);
}
}第五步:生成打包文件
class Compiler {
generateBundle() {
let modules = '';
// 生成所有模块的代码
for (const moduleName in this.modules) {
const module = this.modules[moduleName];
modules += `'${moduleName}': function(module, exports, require) {
${module.source}
},`;
}
// 生成最终的打包代码
const bundle = `(function(modules) {
// 模块缓存
var installedModules = {};
// require 函数
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}})`;
return bundle;
}
emitFiles() {
const output = this.generateBundle();
const outputPath = path.resolve(
this.root,
this.options.output.path || 'dist/main.js'
);
// 确保输出目录存在
const outputDir = path.dirname(outputPath);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// 写入文件
fs.writeFileSync(outputPath, output, 'utf-8');
console.log(`打包成功!输出文件: ${outputPath}`);
}
}完整实现
// simple-webpack.js
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) {
const absolutePath = path.resolve(this.root, modulePath);
return fs.readFileSync(absolutePath, 'utf-8');
}
getDependencies(source) {
const ast = parser.parse(source, {
sourceType: 'module',
plugins: ['jsx']
});
const dependencies = [];
traverse(ast, {
ImportDeclaration({ node }) {
dependencies.push(node.source.value);
}
});
return dependencies;
}
transform(source) {
const ast = parser.parse(source, {
sourceType: 'module'
});
const { code } = transformFromAst(ast, null, {
presets: ['@babel/preset-env']
});
return code;
}
buildModule(modulePath, isEntry) {
const absolutePath = path.resolve(this.root, modulePath);
const source = this.getSource(modulePath);
const transformedSource = this.transform(source);
const dependencies = this.getDependencies(transformedSource);
const moduleName = './' + path.relative(this.root, absolutePath);
this.modules[moduleName] = {
source: transformedSource,
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);
const depModuleName = './' + path.relative(this.root, depPath);
if (!this.modules[depModuleName]) {
this.buildModule(depPath, false);
}
});
}
generateBundle() {
let modules = '';
for (const moduleName in this.modules) {
modules += `'${moduleName}': function(module, exports, require) {
${this.modules[moduleName].source}
},`;
}
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}})`;
}
emitFiles() {
const output = this.generateBundle();
const outputPath = path.resolve(
this.root,
this.options.output.path || 'dist/main.js'
);
const outputDir = path.dirname(outputPath);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
fs.writeFileSync(outputPath, output, 'utf-8');
console.log('打包完成!');
}
run() {
this.buildModule(this.options.entry, true);
this.emitFiles();
}
}
module.exports = SimpleWebpack;进阶功能
支持多入口
class SimpleWebpack {
buildMultipleEntries(entries) {
for (const name in entries) {
this.buildModule(entries[name], true);
}
}
}支持代码分割
// 处理动态 import
traverse(ast, {
Import({ node }) {
// 创建新的 chunk
const chunkName = node.arguments[0].value;
chunks.push({
name: chunkName,
entry: chunkName
});
}
});测试和使用
创建测试项目
mkdir test-project && cd test-project
npm init -y
npm install @babel/core @babel/parser @babel/traverse @babel/preset-env配置文件
// webpack.config.js
module.exports = {
entry: './src/index.js',
output: {
path: './dist/main.js'
}
};运行
const SimpleWebpack = require('./simple-webpack');
const config = require('./webpack.config');
const compiler = new SimpleWebpack(config);
compiler.run();与真实 Webpack 的差距
| 功能 | 简易实现 | 真实 Webpack |
|---|---|---|
| 模块解析 | 基础路径解析 | 完整 resolve 配置 |
| Loader | 简单转换 | 链式调用、pitch |
| Plugin | 无 | Tapable 事件系统 |
| 代码分割 | 无 | 智能 chunk 分割 |
| Tree Shaking | 无 | 完整支持 |
| HMR | 无 | 完整支持 |
| 缓存 | 无 | 多层缓存 |
学习意义和后续方向
学习意义
通过手写 Webpack,你可以:
- 理解模块打包的本质
- 掌握 AST 的使用
- 深入了解依赖解析
- 为阅读源码打基础
后续方向
- 添加 Loader 支持
- 添加 Plugin 机制
- 实现代码分割
- 实现 HMR
- 阅读 Webpack 源码
总结
手写 Webpack 是理解打包原理的最佳方式,虽然功能简单,但涵盖了核心流程:
- 读取入口文件
- 解析依赖
- 转换代码
- 生成依赖图
- 输出打包文件
