模块打包工具是如何工作的?

打包工具核心流程

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/Base64

4. 代码生成

将模块图打包为可在浏览器运行的代码:

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 chunk

Webpack 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)
}

打包优化手段

  1. Tree Shaking:移除未使用代码
  2. 代码分割:按路由/功能拆分 chunk
  3. 压缩:Terser/esbuild minify
  4. Scope Hoisting:合并模块到同一作用域
  5. 持久化缓存:文件名 contenthash

同分类其他题目