Skip to content

Webpack 核心原理深入

构建流程、模块解析、依赖图、Tapable 钩子系统

一、Webpack 架构设计

1.1 核心对象

Compiler

typescript
class Compiler {
  options: WebpackOptions
  hooks: {
    beforeRun: SyncHook
    run: AsyncSeriesHook
    emit: AsyncSeriesHook
    done: SyncHook
    // ... 更多钩子
  }
  
  run(callback: Callback) {
    // 开始编译
  }
}

Compilation

typescript
class Compilation {
  modules: Set<Module>
  chunks: Set<Chunk>
  assets: Record<string, Source>
  hooks: {
    buildModule: SyncHook
    succeedModule: SyncHook
    finishModules: AsyncSeriesHook
    // ... 更多钩子
  }
}

关系

Compiler (全局唯一)
  ↓ 创建
Compilation (每次编译创建)
  ↓ 包含
Modules, Chunks, Assets

1.2 构建流程详解

完整流程

typescript
// 1. 初始化
const compiler = webpack(config)

// 2. 开始编译
compiler.run((err, stats) => {
  // 编译完成
})

内部流程

1. 初始化阶段
   ├─ 读取配置
   ├─ 创建 Compiler
   ├─ 注册插件
   └─ 触发 environment 钩子

2. 编译阶段
   ├─ 触发 beforeCompile 钩子
   ├─ 创建 Compilation
   ├─ 触发 make 钩子
   ├─ 从 Entry 开始构建
   ├─ 调用 Loader 转换模块
   ├─ 解析依赖
   ├─ 递归构建依赖模块
   └─ 完成模块编译

3. 生成阶段
   ├─ 触发 seal 钩子
   ├─ 生成 Chunk
   ├─ 优化 Chunk
   ├─ 生成 Hash
   ├─ 生成 Assets
   └─ 触发 emit 钩子

4. 输出阶段
   ├─ 写入文件系统
   └─ 触发 done 钩子

二、模块解析机制

2.1 模块类型

NormalModule

typescript
class NormalModule extends Module {
  request: string // 模块路径
  userRequest: string // 用户请求路径
  rawRequest: string // 原始请求
  loaders: LoaderItem[] // 应用的 Loader
  parser: Parser // 解析器
  generator: Generator // 生成器
  
  build(options, compilation, resolver, fs, callback) {
    // 构建模块
  }
}

ContextModule

typescript
// 动态导入
import(`./locale/${language}.js`)

// 生成 ContextModule
class ContextModule extends Module {
  options: {
    regExp: RegExp
    recursive: boolean
  }
}

ExternalModule

typescript
// 外部依赖
externals: {
  jquery: 'jQuery'
}

class ExternalModule extends Module {
  request: string
  externalType: string
}

2.2 依赖解析

Resolver 配置

typescript
resolve: {
  // 查找模块的目录
  modules: ['node_modules', path.resolve(__dirname, 'src')],
  
  // 尝试的扩展名
  extensions: ['.js', '.json', '.jsx', '.ts', '.tsx'],
  
  // 别名
  alias: {
    '@': path.resolve(__dirname, 'src'),
    'components': path.resolve(__dirname, 'src/components')
  },
  
  // 主文件
  mainFiles: ['index'],
  
  // package.json 的字段
  mainFields: ['browser', 'module', 'main']
}

解析流程

typescript
// 1. 解析模块路径
import Button from '@/components/Button'

// 2. 应用 alias
'@/components/Button''/project/src/components/Button'

// 3. 尝试扩展名
'/project/src/components/Button.js'
'/project/src/components/Button.json'
'/project/src/components/Button.jsx'

// 4. 返回绝对路径
'/project/src/components/Button.jsx'

2.3 依赖图构建

示例代码

javascript
// index.js
import { add } from './math.js'
import './style.css'

console.log(add(1, 2))

// math.js
export function add(a, b) {
  return a + b
}

// style.css
body { color: red; }

依赖图

index.js (Entry)
  ├─ math.js (NormalModule)
  └─ style.css (NormalModule)

构建过程

typescript
// 1. 从 Entry 开始
const entryModule = compilation.addEntry(context, entry)

// 2. 构建模块
entryModule.build()

// 3. 解析依赖
const dependencies = parser.parse(entryModule.source)

// 4. 递归构建依赖
dependencies.forEach(dep => {
  const module = compilation.addModule(dep)
  module.build()
})

// 5. 生成依赖图
const dependencyGraph = {
  'index.js': {
    dependencies: ['math.js', 'style.css'],
    code: '...'
  },
  'math.js': {
    dependencies: [],
    code: '...'
  },
  'style.css': {
    dependencies: [],
    code: '...'
  }
}

三、Loader 原理

3.1 Loader 本质

Loader 是一个函数

typescript
// 同步 Loader
function loader(source: string): string {
  // 转换源代码
  return transformedSource
}

// 异步 Loader
function loader(source: string): void {
  const callback = this.async()
  
  doAsyncWork(source, (err, result) => {
    callback(err, result)
  })
}

Loader Context

typescript
interface LoaderContext {
  // 当前处理的文件路径
  resourcePath: string
  
  // 异步回调
  async(): (err: Error | null, content: string) => void
  
  // 添加依赖
  addDependency(file: string): void
  
  // 获取查询参数
  query: any
  
  // 缓存
  cacheable(flag: boolean): void
  
  // Source Map
  sourceMap: boolean
}

3.2 Loader 执行顺序

配置

javascript
{
  test: /\.css$/,
  use: ['style-loader', 'css-loader', 'postcss-loader']
}

执行顺序

源文件

postcss-loader (Pitch)

css-loader (Pitch)

style-loader (Pitch)

读取文件内容

style-loader (Normal)

css-loader (Normal)

postcss-loader (Normal)

转换后的代码

Pitch 和 Normal

typescript
// Loader 有两个阶段
loader.pitch = function(remainingRequest, precedingRequest, data) {
  // Pitch 阶段(从左到右)
  // 可以提前返回结果,跳过后续 Loader
}

function loader(source) {
  // Normal 阶段(从右到左)
  return transformedSource
}

3.3 手写 Loader

示例 1:替换文本 Loader

javascript
// replace-loader.js
module.exports = function(source) {
  const options = this.getOptions()
  
  return source.replace(
    new RegExp(options.search, 'g'),
    options.replace
  )
}

使用

javascript
{
  test: /\.js$/,
  use: {
    loader: path.resolve(__dirname, 'loaders/replace-loader.js'),
    options: {
      search: 'console.log',
      replace: '// console.log'
    }
  }
}

示例 2:Markdown Loader

javascript
// markdown-loader.js
const marked = require('marked')

module.exports = function(source) {
  // 标记为可缓存
  this.cacheable && this.cacheable()
  
  // 转换 Markdown 为 HTML
  const html = marked(source)
  
  // 返回 ES Module
  return `export default ${JSON.stringify(html)}`
}

使用

javascript
{
  test: /\.md$/,
  use: path.resolve(__dirname, 'loaders/markdown-loader.js')
}
javascript
// 代码中使用
import content from './README.md'
document.body.innerHTML = content

示例 3:异步 Loader

javascript
// async-loader.js
module.exports = function(source) {
  const callback = this.async()
  
  // 异步处理
  setTimeout(() => {
    const result = source.toUpperCase()
    callback(null, result)
  }, 1000)
}

四、Plugin 原理

4.1 Tapable 钩子系统

钩子类型

typescript
// 同步钩子
const { SyncHook, SyncBailHook, SyncWaterfallHook } = require('tapable')

// 异步钩子
const { AsyncSeriesHook, AsyncParallelHook } = require('tapable')

使用示例

typescript
class Car {
  constructor() {
    this.hooks = {
      // 同步钩子
      accelerate: new SyncHook(['speed']),
      
      // 异步串行钩子
      brake: new AsyncSeriesHook(['speed']),
      
      // 异步并行钩子
      calculateRoutes: new AsyncParallelHook(['source', 'target'])
    }
  }
  
  setSpeed(speed) {
    // 触发钩子
    this.hooks.accelerate.call(speed)
  }
}

// 注册监听
const car = new Car()

car.hooks.accelerate.tap('LoggerPlugin', (speed) => {
  console.log(`Accelerating to ${speed}`)
})

car.setSpeed(100)

4.2 Plugin 结构

基本结构

typescript
class MyPlugin {
  apply(compiler) {
    // 注册钩子
    compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
      // 插件逻辑
      console.log('生成文件前')
      callback()
    })
  }
}

module.exports = MyPlugin

使用

javascript
plugins: [
  new MyPlugin()
]

4.3 常用钩子

Compiler 钩子

typescript
compiler.hooks.beforeRun.tap('MyPlugin', (compiler) => {
  // 编译前
})

compiler.hooks.run.tap('MyPlugin', (compiler) => {
  // 开始编译
})

compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
  // 生成文件前
  callback()
})

compiler.hooks.done.tap('MyPlugin', (stats) => {
  // 编译完成
})

Compilation 钩子

typescript
compiler.hooks.compilation.tap('MyPlugin', (compilation) => {
  compilation.hooks.buildModule.tap('MyPlugin', (module) => {
    // 构建模块前
  })
  
  compilation.hooks.succeedModule.tap('MyPlugin', (module) => {
    // 模块构建成功
  })
  
  compilation.hooks.optimize.tap('MyPlugin', () => {
    // 优化阶段
  })
})

4.4 手写 Plugin

示例 1:文件列表插件

javascript
// FileListPlugin.js
class FileListPlugin {
  constructor(options) {
    this.filename = options.filename || 'filelist.md'
  }
  
  apply(compiler) {
    compiler.hooks.emit.tapAsync('FileListPlugin', (compilation, callback) => {
      // 获取所有生成的文件
      const filelist = Object.keys(compilation.assets)
        .map(filename => `- ${filename}`)
        .join('\n')
      
      const content = `# 文件列表\n\n${filelist}`
      
      // 添加新文件到输出
      compilation.assets[this.filename] = {
        source: () => content,
        size: () => content.length
      }
      
      callback()
    })
  }
}

module.exports = FileListPlugin

使用

javascript
plugins: [
  new FileListPlugin({ filename: 'assets.md' })
]

示例 2:清除注释插件

javascript
// RemoveCommentsPlugin.js
class RemoveCommentsPlugin {
  apply(compiler) {
    compiler.hooks.emit.tap('RemoveCommentsPlugin', (compilation) => {
      // 遍历所有生成的文件
      for (const filename in compilation.assets) {
        if (filename.endsWith('.js')) {
          const asset = compilation.assets[filename]
          const source = asset.source()
          
          // 移除注释
          const newSource = source
            .replace(/\/\*[\s\S]*?\*\//g, '')
            .replace(/\/\/.*/g, '')
          
          // 更新文件内容
          compilation.assets[filename] = {
            source: () => newSource,
            size: () => newSource.length
          }
        }
      }
    })
  }
}

module.exports = RemoveCommentsPlugin

示例 3:打包进度插件

javascript
// ProgressPlugin.js
class ProgressPlugin {
  apply(compiler) {
    let lastPercent = 0
    
    compiler.hooks.compilation.tap('ProgressPlugin', (compilation) => {
      compilation.hooks.buildModule.tap('ProgressPlugin', (module) => {
        const percent = Math.floor(
          (compilation.modules.size / compilation._modulesCache.size) * 100
        )
        
        if (percent !== lastPercent) {
          console.log(`构建进度: ${percent}%`)
          lastPercent = percent
        }
      })
    })
  }
}

module.exports = ProgressPlugin

五、Chunk 生成机制

5.1 Chunk 类型

Entry Chunk

javascript
entry: {
  app: './src/app.js',
  admin: './src/admin.js'
}

// 生成两个 Entry Chunk
// app.js, admin.js

Async Chunk

javascript
// 动态导入
import('./module.js').then(module => {
  // ...
})

// 生成 Async Chunk
// 0.js (module.js)

Runtime Chunk

javascript
optimization: {
  runtimeChunk: 'single'
}

// 生成 Runtime Chunk
// runtime.js

5.2 SplitChunks 原理

默认配置

javascript
optimization: {
  splitChunks: {
    chunks: 'async',
    minSize: 20000,
    minRemainingSize: 0,
    minChunks: 1,
    maxAsyncRequests: 30,
    maxInitialRequests: 30,
    enforceSizeThreshold: 50000,
    cacheGroups: {
      defaultVendors: {
        test: /[\\/]node_modules[\\/]/,
        priority: -10,
        reuseExistingChunk: true
      },
      default: {
        minChunks: 2,
        priority: -20,
        reuseExistingChunk: true
      }
    }
  }
}

分割流程

1. 收集所有模块
2. 按 cacheGroups 分组
3. 检查是否满足条件:
   - minSize: 最小体积
   - minChunks: 最少引用次数
   - maxAsyncRequests: 最大异步请求数
   - maxInitialRequests: 最大初始请求数
4. 生成新 Chunk
5. 更新依赖关系

5.3 Chunk 优化

合并 Chunk

javascript
optimization: {
  mergeDuplicateChunks: true
}

移除空 Chunk

javascript
optimization: {
  removeEmptyChunks: true
}

模块连接

javascript
optimization: {
  concatenateModules: true // Scope Hoisting
}

六、打包产物分析

6.1 Bundle 结构

简化的 Bundle

javascript
(function(modules) {
  // 模块缓存
  const installedModules = {}
  
  // require 函数
  function __webpack_require__(moduleId) {
    // 检查缓存
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports
    }
    
    // 创建模块
    const module = installedModules[moduleId] = {
      id: moduleId,
      loaded: false,
      exports: {}
    }
    
    // 执行模块函数
    modules[moduleId].call(
      module.exports,
      module,
      module.exports,
      __webpack_require__
    )
    
    module.loaded = true
    return module.exports
  }
  
  // 加载入口模块
  return __webpack_require__(0)
})([
  // 模块 0: index.js
  function(module, exports, __webpack_require__) {
    const math = __webpack_require__(1)
    console.log(math.add(1, 2))
  },
  
  // 模块 1: math.js
  function(module, exports) {
    exports.add = function(a, b) {
      return a + b
    }
  }
])

6.2 动态导入

源代码

javascript
button.addEventListener('click', () => {
  import('./module.js').then(module => {
    module.default()
  })
})

打包后

javascript
button.addEventListener('click', () => {
  __webpack_require__.e(/* chunkId */ 0).then(
    __webpack_require__.bind(null, /* moduleId */ 1)
  ).then(module => {
    module.default()
  })
})

// __webpack_require__.e 实现
__webpack_require__.e = function(chunkId) {
  return new Promise((resolve, reject) => {
    const script = document.createElement('script')
    script.src = __webpack_require__.p + chunkId + '.js'
    script.onload = resolve
    script.onerror = reject
    document.head.appendChild(script)
  })
}

七、性能优化原理

7.1 Tree Shaking 原理

标记阶段

javascript
// utils.js
export function add(a, b) { return a + b }
export function subtract(a, b) { return a - b }

// index.js
import { add } from './utils.js'
console.log(add(1, 2))

// 标记结果
// add: used
// subtract: unused

删除阶段

javascript
// Terser 压缩时删除未使用的代码
// 最终输出只包含 add 函数

7.2 Scope Hoisting 原理

未优化

javascript
// module-a.js
export const a = 1

// module-b.js
export const b = 2

// index.js
import { a } from './module-a.js'
import { b } from './module-b.js'
console.log(a + b)

// 打包后(3 个模块)

优化后

javascript
// 合并为一个模块
const a = 1
const b = 2
console.log(a + b)

配置

javascript
optimization: {
  concatenateModules: true
}

7.3 持久化缓存原理

缓存结构

.temp_cache/
  ├─ default/
  │   ├─ 0.pack
  │   ├─ 1.pack
  │   └─ index.json
  └─ buildDependencies/
      └─ webpack.config.js

缓存策略

typescript
// 1. 计算模块 Hash
const moduleHash = crypto
  .createHash('md5')
  .update(moduleSource)
  .digest('hex')

// 2. 检查缓存
if (cache.has(moduleHash)) {
  return cache.get(moduleHash)
}

// 3. 构建模块
const result = buildModule(module)

// 4. 写入缓存
cache.set(moduleHash, result)

面试高频题

1: Webpack 的构建流程是什么?

答案

三个阶段

  1. 初始化:读取配置、创建 Compiler、注册插件
  2. 编译:从 Entry 开始、调用 Loader、解析依赖、递归构建
  3. 输出:生成 Chunk、生成 Assets、写入文件

核心对象

  • Compiler:全局唯一,控制整个流程
  • Compilation:每次编译创建,包含模块和资源

2: Loader 和 Plugin 的区别是什么?

答案

Loader

  • 文件转换器
  • 处理单个文件
  • 在模块加载时执行
  • 返回转换后的内容

Plugin

  • 功能扩展器
  • 介入整个构建流程
  • 通过钩子系统工作
  • 可以访问 Compiler 和 Compilation

3: Loader 的执行顺序是什么?

答案

从右到左,从下到上

javascript
{
  test: /\.css$/,
  use: ['style-loader', 'css-loader', 'postcss-loader']
}

// 执行顺序:
// postcss-loader → css-loader → style-loader

原因:函数组合(compose)

javascript
compose(f, g, h)(x) = f(g(h(x)))

4: 如何编写一个 Loader?

答案

javascript
module.exports = function(source) {
  // 1. 获取配置
  const options = this.getOptions()
  
  // 2. 转换源代码
  const result = transform(source, options)
  
  // 3. 返回结果
  return result
}

// 异步 Loader
module.exports = function(source) {
  const callback = this.async()
  
  asyncTransform(source, (err, result) => {
    callback(err, result)
  })
}

5: 如何编写一个 Plugin?

答案

javascript
class MyPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
      // 在生成文件前执行
      console.log('生成文件...')
      callback()
    })
  }
}

module.exports = MyPlugin

关键点

  • 实现 apply 方法
  • 通过 compiler.hooks 注册钩子
  • 使用 taptapAsynctapPromise

6: Webpack 如何实现 Tree Shaking?

答案

原理

  1. 基于 ES6 Module 静态分析
  2. 标记未使用的导出(usedExports
  3. Terser 删除未使用的代码(minimize

配置

javascript
// webpack.config.js
optimization: {
  usedExports: true,
  minimize: true
}

// package.json
{
  "sideEffects": false
}

7: Webpack 如何实现代码分割?

答案

三种方式

  1. 多入口
javascript
entry: {
  app: './src/app.js',
  admin: './src/admin.js'
}
  1. SplitChunksPlugin
javascript
optimization: {
  splitChunks: {
    chunks: 'all'
  }
}
  1. 动态导入
javascript
import('./module.js').then(module => {
  // ...
})

8: Webpack 的模块解析流程是什么?

答案

1. 解析模块路径
   import Button from '@/components/Button'

2. 应用 alias
   '@/components/Button' → '/project/src/components/Button'

3. 尝试扩展名
   Button.js, Button.json, Button.jsx

4. 查找文件
   找到 Button.jsx

5. 返回绝对路径
   '/project/src/components/Button.jsx'

9: Webpack 的 HMR 原理是什么?

答案

流程

1. Webpack Compiler 监听文件变化
2. 重新编译生成新模块
3. Webpack Dev Server 通过 WebSocket 推送更新
4. 客户端通过 JSONP 请求新模块
5. HMR Runtime 替换旧模块
6. 触发 module.hot.accept() 回调

关键

  • WebSocket 通信
  • 模块热替换
  • 保留应用状态

10: Webpack 5 的持久化缓存如何工作?

答案

原理

javascript
cache: {
  type: 'filesystem',
  cacheDirectory: path.resolve(__dirname, '.temp_cache')
}

工作方式

  1. 计算模块内容 Hash
  2. 检查缓存是否存在
  3. 命中缓存直接返回
  4. 未命中则构建并写入缓存

效果:二次构建速度提升 90%+