🚀 1. 整体目标
构建一个简单的模块打包器,支持:
- 入口文件分析
- 依赖分析(
import) - 转换 ES6+ 为浏览器可识别代码(使用 Babel)
- 输出一个
bundle.js
🛠️ 实现步骤详解
第一步:准备基本目录结构
bash
复制编辑
mini-webpack/
├── src/
│ └── index.js // 入口文件
├── dist/
├── mini-webpack.js // 你的打包器
└── package.json
第二步:安装依赖
bash
复制编辑
npm init -y
npm install @babel/parser @babel/traverse @babel/core @babel/preset-env
第三步:实现核心逻辑(mini-webpack.js)
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')
let id = 0
function createAsset(filename) {
const content = fs.readFileSync(filename, 'utf-8')
const ast = parser.parse(content, {
sourceType: 'module',
})
const dependencies = []
traverse(ast, {
ImportDeclaration: ({ node }) => {
dependencies.push(node.source.value)
},
})
const { code } = babel.transformFromAstSync(ast, content, {
presets: ['@babel/preset-env'],
})
return {
id: id++,
filename,
dependencies,
code,
}
}
第四步:构建依赖图
js
复制编辑
function createGraph(entry) {
const mainAsset = createAsset(entry)
const queue = [mainAsset]
for (const asset of queue) {
const dirname = path.dirname(asset.filename)
asset.mapping = {}
asset.dependencies.forEach((relativePath) => {
const absolutePath = path.join(dirname, relativePath)
const child = createAsset(absolutePath)
asset.mapping[relativePath] = child.id
queue.push(child)
})
}
return queue
}
第五步:生成最终打包文件
js
复制编辑
function bundle(graph) {
let modules = ''
graph.forEach((mod) => {
modules += `
${mod.id}: [
function(require, module, exports) {
${mod.code}
},
${JSON.stringify(mod.mapping)},
],
`
})
const result = `
(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(0)
})({${modules}})
`
fs.writeFileSync('./dist/bundle.js', result, 'utf-8')
}
第六步:入口执行
js
复制编辑
const graph = createGraph('./src/index.js')
bundle(graph)
运行:
bash
复制编辑
node mini-webpack.js
会生成 dist/bundle.js,可在 HTML 中通过 <script> 引入。
✅ 示例输入文件(src/index.js)
js
复制编辑
import message from './message.js'
console.log(message)
js
复制编辑
// message.js
export default 'Hello from mini-webpack!'
📦 打包结果解析(dist/bundle.js)
会包含一个简化版的模块系统:
- 所有模块代码都被包在一个对象中(以 ID 为键)
- 每个模块使用
function(require, module, exports)形式封装 - 自己实现了一个简化的
require函数
总结:Webpack 核心原理
| 模块 | mini-webpack 实现方式 |
|---|---|
| 入口分析 | createAsset() |
| 依赖收集 | AST 语法分析 + traverse |
| 模块图构建 | createGraph() |
| 模块打包 | bundle() + 自定义 require |
✅ 可选扩展功能(按需添加)
- 支持 CommonJS(
require) - CSS 打包
- 图片/静态资源打包
- 支持热更新(HMR)
- 支持插件系统(类似 Webpack 插件)
- 支持 Loader(编译 Sass、TS 等)
✅ 1. 支持 CommonJS(require)
🔍 原理:
默认我们支持的是 import,如果要兼容 require,需要在 AST 遍历中也识别 CallExpression 类型的 require。
✅ 改动:
js
复制编辑
traverse(ast, {
ImportDeclaration({ node }) {
dependencies.push(node.source.value)
},
CallExpression({ node }) {
if (node.callee.name === 'require') {
dependencies.push(node.arguments[0].value)
}
}
})
✅ 2. CSS 打包
🔍 原理:
遇到 .css 文件,不能转 AST,而是作为文本内容读入并注入页面。
✅ 实现思路:
在 createAsset 中加入判断:
js
复制编辑
if (filename.endsWith('.css')) {
const content = fs.readFileSync(filename, 'utf-8')
return {
id: id++,
filename,
dependencies: [],
code: `
const style = document.createElement('style');
style.innerText = ${JSON.stringify(content)};
document.head.appendChild(style);
`,
}
}
这样在运行 bundle 时,CSS 会注入 <style> 标签。
✅ 3. 图片/静态资源打包
🔍 原理:
遇到 .png、.jpg 等文件,复制到 dist 目录,同时输出资源路径。
✅ 实现方式:
js
复制编辑
if (/.(png|jpg|jpeg|gif)$/.test(filename)) {
const targetPath = path.join('./dist', path.basename(filename))
fs.copyFileSync(filename, targetPath)
return {
id: id++,
filename,
dependencies: [],
code: `module.exports = "./${path.basename(filename)}";`,
}
}
✅ 4. 支持 HMR(Hot Module Replacement)
🔍 原理:
HMR 实现比较复杂,需要:
- devServer(本地服务器,监听文件变化)
- 使用 WebSocket 推送变更
- 模块热替换逻辑
✅ 简化版方案:
- 使用
chokidar监听文件变化 - 手动重新执行
bundle()生成文件 - 浏览器通过
EventSource(或 WebSocket)感知变化并location.reload()
✅ 推荐结构:
bash
复制编辑
mini-webpack/
├── mini-webpack.js
├── dev-server.js <-- 监听文件变更
└── hot-reload.js <-- 客户端 JS 自动 reload
(如果你想实现这部分,我可以给你写一个最小 HMR demo)
✅ 5. 插件系统(模拟 Webpack 插件)
🔍 原理:
暴露生命周期钩子供插件注册,例如:beforeEmit, afterEmit
✅ 简易实现:
js
复制编辑
class Compiler {
constructor(options) {
this.options = options
this.hooks = {
beforeEmit: [],
afterEmit: [],
}
}
applyPlugins(hookName, args) {
this.hooks[hookName].forEach(fn => fn(args))
}
run() {
this.applyPlugins('beforeEmit')
bundle()
this.applyPlugins('afterEmit')
}
use(plugin) {
plugin.apply(this)
}
}
插件结构:
js
复制编辑
class MyPlugin {
apply(compiler) {
compiler.hooks.beforeEmit.push(() => {
console.log('📦 构建即将开始')
})
}
}
✅ 6. 支持 Loader(例如处理 Sass、TS)
🔍 原理:
在模块加载前调用自定义函数对源码进行转换。
✅ Loader 机制:
js
复制编辑
function runLoaders(loaders, content) {
return loaders.reduceRight((code, loader) => loader(code), content)
}
js
复制编辑
const cssLoader = (code) => `const style = document.createElement('style'); style.innerText = ${JSON.stringify(code)}; document.head.appendChild(style);`
const tsLoader = (code) => require('typescript').transpile(code)
加入到 createAsset() 中:
js
复制编辑
let source = fs.readFileSync(filename, 'utf-8')
const ext = path.extname(filename)
if (ext === '.ts') {
source = runLoaders([tsLoader], source)
}
// Babel 转换再继续
你也可以从 webpack.config.js 中配置 loader 路径数组来动态加载。
🎁 Bonus:loader + plugin 的完整设计
| 功能 | 建议实现方法 |
|---|---|
| Loader | runLoaders 按规则匹配后缀 |
| Plugin | 生命周期 hooks + apply() |
| HMR | devServer + WebSocket + 客户端 reload.js |
| 静态资源 | 识别扩展名 + copy |
| CSS | 注入 style 标签 |
| CommonJS | AST 分析 CallExpression |
| TS/SASS | loader 转换再走 Babel |
✅ 总结
你可以像这样逐步增强 mini-webpack:
text
复制编辑
🛠 最小核心:依赖图 + 代码合并
➕ 支持语法:import + require + Babel 转换
🎨 支持资源:CSS + 图片静态资源
🔥 开发优化:热更新 HMR
🧩 插件机制:hooks + apply
🎛 Loader 系统:loader 队列处理源码