project/
├── dist/
└── bundle.js ← 输出文件
├── src/
└── foo.json
└── index.js
└── message.js
└── mini-webpack-config.js
npm install @babel/parser @babel/traverse @babel/core @babel/preset-env chokidar@2
功能项 |
实现目标 |
1. 入口文件 |
从入口文件出发,递归分析依赖 |
2. AST 转换 |
使用 Babel 解析代码,提取依赖 |
3. 模块依赖图 |
构建依赖图 |
4. Loader 模拟 |
将 .json 文件转换为 JS 对象; 模拟处理图片的 loader,实际情况下图片会被转为 base64 或上传至 CDN。 |
5. Plugin 系统 |
实现生命周期钩子compile、emit,支持tap和tapAsync操作 |
6. Compiler |
负责处理构建生命周期中的各种事件,初始化和启动构建过程,结合插件系统 在相应的生命周期钩子中执行操作 |
7. Compilation |
负责在每次构建中处理模块(模块解析、加载、编译等) |
8. HMR |
通过chokidar,实现在文件发生变化时,生成新的 Compilation 实例 |
9. 模块打包 |
将多个模块打包成一个 bundle |
10. 输出文件 |
生成最终 JS 文件并写入磁盘 |
src/index.js
import message, {val} from './message.js';
import foo from './foo.json';
console.log('Message is:', message, val);
console.log(foo);
src/message.js
export default 'Hello from Mini Webpack!';
export const val = "thank you";
src/foo.json
{
"foo": "suc"
}
mini-webpack-config.js
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const babel = require('@babel/core');
const chokidar = require('chokidar'); // 用于监听文件变化
// 同步执行,按注册顺序立即执行
class SyncHook {
constructor(args) {
this.args = args; // 参数列表(如 ['name', 'age'])
this.taps = []; // 存储注册的回调函数
}
// 注册回调
tap(name, callback) {
this.taps.push({ name, callback });
}
// 触发回调
call(...args) {
// 依次执行所有回调
for (const tap of this.taps) {
tap.callback(...args);
console.log(tap.name, '执行同步钩子', new Date());
}
}
}
// 异步串行执行,按注册顺序依次执行,等待前一个完成
class AsyncSeriesHook {
constructor(args) {
this.args = args;
this.taps = [];
}
// 注册同步钩子
tap(name, fn) {
this.taps.push({ name, type: 'sync', fn });
}
// 注册异步钩子(回调风格)
tapAsync(name, fn) {
this.taps.push({ name, type: 'async', fn });
}
// 触发异步钩子
callAsync(...args) {
const finalCallback = args.pop();
const context = {};
let index = 0;
const next = (err) => {
if (err) return finalCallback(err);
const tap = this.taps[index++];
if (!tap) return finalCallback();
try {
if (tap.type === 'sync') {
tap.fn(...args, context);
console.log(tap.name, '执行同步钩子', new Date());
next();
} else if (tap.type === 'async') {
console.log(tap.name, '执行异步钩子', new Date());
tap.fn(...args, next);
}
} catch (error) {
next(error);
}
};
next();
}
}
// 模拟 JSON loader
function jsonLoader(content) {
return `export default ${content}`;
}
// 模拟图片 loader(这里只是简单地将图片路径作为模块内容)
function imageLoader(content) {
return `export default '${content}'`;
}
// 解析模块
function parseModule(filename) {
let content = fs.readFileSync(filename, 'utf-8');
// 处理不同类型的文件
if (filename.endsWith('.json')) {
content = jsonLoader(content);
} else if (filename.match(/\.(jpg|png|gif)$/)) {
content = imageLoader(content);
}
const ast = parser.parse(content, { sourceType: 'module' });
const dependencies = [];
const mapping = {}; // 这里用来存储模块的依赖关系
traverse(ast, {
ImportDeclaration({ node }) {
dependencies.push(node.source.value);
const relativePath = node.source.value;
// 解析相对路径为绝对路径
const absolutePath = path.resolve(path.dirname(filename), relativePath);
dependencies.push(absolutePath); // 使用绝对路径存储依赖
mapping[relativePath] = absolutePath; // 保存相对路径与绝对路径的映射关系
}
});
const { code } = babel.transformFromAstSync(ast, null, {
presets: ['@babel/preset-env'],
});
return {
filename,
dependencies,
code,
mapping
};
}
// 模拟 Compilation 类:负责模块的解析和构建
class Compilation {
constructor(modules, plugins) {
this.modules = modules; // 存储所有的模块
this.plugins = plugins; // 存储所有插件
}
buildModule(filename) {
// 通过解析模块,构建模块内容
const module = parseModule(filename);
this.modules.push(module);
}
generateOutput() {
// 生成最终的打包代码
let modules = '';
this.modules.forEach(mod => {
modules += `"${mod.filename}": [
function(require, module, exports) {
${mod.code}
},
${JSON.stringify(mod.mapping).replace(/\\\\/g,'\\')},
],`;
});
return `
(function(modules) {
function require(id) {
const [fn, mapping] = modules[id];
function localRequire(name) {
return require(mapping[name]);
}
const module = { exports: {} };
fn(localRequire, module, module.exports);
return module.exports;
}
require("${this.modules[0].filename}");
})({${modules}})
`;
}
}
// 模拟 Compiler 类:负责整体的构建生命周期管理
class Compiler {
constructor({entry, plugins, outputPath}) {
this.entry = entry; // 入口文件
this.plugins = plugins || []; // 插件列表
this.outputPath = outputPath; // 输出路径
this.modules = []; // 存储所有模块
this.hooks = {};
this.initHooks();
}
initHooks() {
this.hooks.compile = new SyncHook();
this.hooks.emit = new AsyncSeriesHook();
}
// 执行插件生命周期钩子
applyPlugins() {
this.plugins.forEach(plugin => plugin.apply(this));
}
// 执行构建过程
run() {
this.applyPlugins();
// 执行compile钩子相关回调
this.hooks.compile.call();
const compilation = new Compilation(this.modules, this.plugins);
// 解析入口模块
compilation.buildModule(path.resolve(this.entry));
// 解析其他模块(依赖)
for (const module of this.modules) {
const dirname = path.dirname(module.filename);
module.dependencies.forEach(relativePath => {
const absPath = path.resolve(dirname, relativePath);
compilation.buildModule(absPath);
});
}
// 执行emit钩子相关回调
this.hooks.emit.callAsync(compilation, () => {
console.log('emit插件执行完成。')
// 最终生成输出文件
const output = compilation.generateOutput();
// 输出到文件
const outDir = this.outputPath;
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir);
fs.writeFileSync(path.join(outDir, 'bundle.js'), output, 'utf-8');
console.log('✅ 构建成功:dist/bundle.js');
});
}
// 监视文件变化,重新构建
watch() {
const watcher = chokidar.watch(this.entry, { persistent: true });
watcher.on('change', (filename) => {
console.log(`文件变化:${filename}`);
// 清空之前的模块,重新执行构建
this.modules = [];
this.run();
});
}
}
// 模拟插件
class ExamplePlugin1 {
apply(compiler) {
// 使用 tap 注册同步钩子
compiler.hooks.compile.tap('ExamplePlugin1', (params) => {
console.log('ExamplePlugin1,同步钩子:编译开始!', new Date());
});
// 使用 tapAsync 注册异步钩子
compiler.hooks.emit.tapAsync('ExamplePlugin1', (compilation, callback) => {
console.log('ExamplePlugin1,异步钩子:开始 emitting 文件...', new Date());
setTimeout(() => {
console.log('ExamplePlugin1,异步操作完成', new Date());
callback(); // 完成异步操作并触发回调
}, 1000);
});
}
}
class ExamplePlugin2 {
apply(compiler) {
// 使用 tap 注册同步钩子
compiler.hooks.compile.tap('ExamplePlugin2', (params) => {
console.log('ExamplePlugin2,同步钩子:编译开始!', new Date());
});
// 使用 tapAsync 注册异步钩子
compiler.hooks.emit.tapAsync('ExamplePlugin2', (compilation, callback) => {
console.log('ExamplePlugin2,异步钩子:开始 emitting 文件...', new Date());
setTimeout(() => {
console.log('ExamplePlugin2,异步操作完成', new Date());
callback(); // 完成异步操作并触发回调
}, 1000);
});
}
}
const compiler = new Compiler({
entry: './src/index.js',
plugins: [new ExamplePlugin1(), new ExamplePlugin2()],
outputPath: path.resolve(__dirname, 'dist')
});
// 启动构建并监视文件变化
compiler.run();
compiler.watch();