模块打包工具是如何工作的?
打包工具核心流程
1. 入口解析 → 2. 依赖图构建 → 3. 转换编译 → 4. 代码生成 → 5. 输出1. 入口解析
从配置的 entry 开始,读取入口文件:
// webpack.config.js
entry: './src/index.js'
// vite.config.js
build.rollupOptions.input: 'src/index.html'2. 依赖图构建
递归分析 import/require,构建模块依赖图:
index.js
├── import { add } from './math.js'
│ └── import { calculator } from './utils.js'
├── import React from 'react' → node_modules
└── import './styles.css'简化实现:
function createDependencyGraph(entry) {
const graph = {}
function visit(filepath) {
if (graph[filepath]) return // 已访问
const code = fs.readFileSync(filepath, 'utf-8')
const deps = parseImports(code) // 提取 import/require
graph[filepath] = { code, deps }
deps.forEach(dep => visit(resolve(dep, filepath)))
}
visit(entry)
return graph
}3. 转换编译
对每个模块应用 loader/plugin:
// babel 转译
// .vue SFC 编译
// CSS → JS 模块
// 图片 → URL/Base644. 代码生成
将模块图打包为可在浏览器运行的代码:
Webpack 的模块包装:
// Webpack 将每个模块包装为函数
__webpack_modules__ = {
'./src/math.js': function(module, exports, __webpack_require__) {
exports.add = function(a, b) { return a + b }
},
'./src/index.js': function(module, exports, __webpack_require__) {
const { add } = __webpack_require__('./src/math.js')
console.log(add(1, 2))
}
}
// 运行时
function __webpack_require__(moduleId) {
var module = { exports: {} }
__webpack_modules__[moduleId](module, module.exports, __webpack_require__)
return module.exports
}
// 执行入口
__webpack_require__('./src/index.js')5. 代码分割
// 动态 import() 触发代码分割
import('./dashboard.js').then(module => {
module.render()
})
// Webpack 生成:
// main.js(入口 chunk)
// dashboard.[hash].js(异步 chunk)
// 运行时按需加载 dashboard chunkWebpack vs Rollup vs esbuild
| 特性 | Webpack | Rollup | esbuild |
|---|---|---|---|
| 模块格式 | CJS + ESM | ESM | CJS + ESM |
| 代码分割 | ✅ 强大 | ✅ 基础 | ✅ 基础 |
| Tree Shaking | ✅ | ✅ 优秀 | ✅ |
| HMR | ✅ | ❌ | ✅(Vite 中) |
| 速度 | 慢 | 中 | 极快 |
| 适合 | 应用 | 库 | 开发 |
Vite 的独特方式
开发阶段:不打包,浏览器原生 ESM 按需加载
// 浏览器直接请求 /src/main.js
// Vite 拦截请求,按需编译单个模块
// 浏览器通过 import 链自动加载依赖生产构建:用 Rollup 打包
// vite build → Rollup 打包
// 输出 dist/ 目录简化打包器实现
const fs = require('fs')
const path = require('path')
const { parse } = require('acorn')
function bundle(entry) {
const modules = {}
function analyze(filepath) {
const code = fs.readFileSync(filepath, 'utf-8')
const ast = parse(code, { sourceType: 'module' })
const deps = []
ast.body.forEach(node => {
if (node.type === 'ImportDeclaration') {
deps.push(node.source.value)
}
})
modules[filepath] = { code, deps }
deps.forEach(dep => {
const depPath = path.resolve(path.dirname(filepath), dep)
if (!modules[depPath]) analyze(depPath)
})
}
analyze(entry)
// 生成打包代码...
return generateOutput(modules, entry)
}打包优化手段
- Tree Shaking:移除未使用代码
- 代码分割:按路由/功能拆分 chunk
- 压缩:Terser/esbuild minify
- Scope Hoisting:合并模块到同一作用域
- 持久化缓存:文件名 contenthash