Skip to content

构建工具

作者:青见春山
发表于:2026-07-29
字数统计:7500 字
预计阅读26分钟

涵盖 Webpack vs Vite 对比、ES Module 三阶段原理、Vite 冷启动与 HMR 原理、Vite 插件系统、Tree Shaking、Source Map。

一、Webpack vs Vite 速览

维度WebpackVite
开发模式打包后启动(冷启动慢)浏览器原生 ESM(启动快)
HMR慢(全量打包)快(按需编译)
生产模式打包(Tree Shaking、Code Splitting)用 Rollup 打包
配置复杂度
生态成熟、插件多新但增长快
大项目冷启动慢(几十秒~几分钟)快(毫秒级)
HMR 速度受项目规模影响与项目规模几乎无关
产物兼容性更好现代浏览器(生产用 Rollup 兼容性好)

二、Webpack 核心概念

1. 五大核心概念

Plain
1. Entry(入口):打包的起点,从哪个文件开始构建依赖图
2. Output(输出):打包产物的输出位置和文件名
3. Loader(加载器):把非 JS 文件(CSS、图片、TS 等)转换为 JS 模块
4. Plugin(插件):扩展 Webpack 能力(压缩、HTML 生成、环境变量注入等)
5. Mode(模式):development / production,决定默认优化

2. 基础配置

JavaScript
// webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = {
  mode: 'development',

  entry: {
    main: './src/index.js',
    vendor: './src/vendor.js'
  },

  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js',
    chunkFilename: '[name].[contenthash].chunk.js',
    publicPath: '/',
    clean: true
  },

  resolve: {
    extensions: ['.js', '.ts', '.vue', '.json'],
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  },

  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      },
      {
        test: /\.ts$/,
        use: 'ts-loader',
        exclude: /node_modules/
      },
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader']
      },
      {
        test: /\.(png|svg|jpg|gif)$/,
        type: 'asset/resource'
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      }
    ]
  },

  plugins: [
    new HtmlWebpackPlugin({ template: './index.html' }),
    new MiniCssExtractPlugin({ filename: '[name].[contenthash].css' })
  ],

  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendor',
          chunks: 'all'
        }
      }
    },
    runtimeChunk: 'single'
  },

  devServer: {
    hot: true,
    port: 3000,
    historyApiFallback: true  // SPA
  }
}

3. 构建流程

Plain
Webpack 构建流程:
1. 初始化参数:从配置文件 + CLI 参数 + 默认值合并
2. 开始编译:用上一步得到的参数初始化 Compiler 对象
3. 确定入口:从 entry 开始找到所有依赖模块
4. 编译模块:从入口开始调用 Loader 编译每个模块
5. 完成编译:得到编译后的模块和依赖关系
6. 输出资源:根据入口和模块依赖,组装成多个 Chunk
7. 输出完成:根据 output 配置写入文件系统

4. 常见 Loader 与 Plugin

Plain
常用 Loader:
- babel-loader     ES6+ / TS → ES5
- ts-loader        TypeScript
- vue-loader       .vue 文件
- css-loader       CSS → JS 模块
- style-loader     把 CSS 注入 DOM
- postcss-loader   PostCSS 处理
- url-loader       小文件转 base64
- file-loader      文件输出
- raw-loader       加载文件为字符串

常用 Plugin:
- HtmlWebpackPlugin    生成 HTML
- MiniCssExtractPlugin 提取 CSS 到独立文件
- CleanWebpackPlugin   清理 dist
- CopyWebpackPlugin    复制静态文件
- DefinePlugin         定义全局变量
- TerserPlugin        JS 压缩
- CompressionPlugin    Gzip 压缩

三、Vite 核心原理

1. 为什么 Vite 快?

Plain
传统打包器(Webpack)的问题:
- 开发模式下也要打包所有模块
- 项目越大,启动越慢(几百 MB 的项目要打包几十秒~几分钟)
- HMR 也要重新打包

Vite 的解决方案:
- 开发模式:利用浏览器原生 ES Module,按需编译
- 不打包,启动极快
- HMR 精准更新(只更新改动的文件)
- 生产模式:用 Rollup 打包(产物更紧凑)

2. ES Module 的三阶段加载

Plain
Vite 的核心基于浏览器的原生 ESM。当浏览器遇到 import 时,会向服务器发请求:

第一阶段(预构建 / Dep Pre-Bundling):
- 把 CommonJS / UMD 转换为 ESM
- 把多个模块合并(esbuild,速度极快)
- 优化依赖缓存

第二阶段(ESM 按需加载):
- 浏览器请求入口 HTML
- 浏览器解析到 import 时,发请求到 Vite Dev Server
- Vite 把请求的文件编译后返回(按需)
- 浏览器继续解析 import,递归加载

第三阶段(HMR):
- 文件修改后,Vite 通过 WebSocket 推送更新
- 浏览器按需重新加载变化的部分
- 无需全量刷新

3. Vite 配置

JavaScript
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'node:path'

export default defineConfig({
  plugins: [vue()],

  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  },

  server: {
    port: 3000,
    open: true,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: p => p.replace(/^\/api/, '')
      }
    }
  },

  build: {
    outDir: 'dist',
    sourcemap: false,
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['vue', 'vue-router', 'pinia']
        }
      }
    }
  }
})

4. Vite 启动流程

Plain
Vite 启动:
1. 启动 Koa 服务器(Dev Server)
2. 拦截浏览器请求
3. 对于 .vue、.ts、.scss 等文件,按需编译
4. 编译后通过 HTTP 返回给浏览器
5. 浏览器原生 ESM 解析

首次访问 index.html:
- 浏览器解析 HTML,遇到 <script type="module" src="...">
- 浏览器请求 main.js
- Vite 返回编译后的 main.js(包含 import)
- 浏览器解析 import,继续请求依赖
- 每个依赖都是单独的请求
- Vite 按需编译每个文件

相比 Webpack:Vite 不需要先打包所有依赖

5. Vite 预构建(Dep Pre-Bundling)

Plain
为什么需要预构建:
1. CommonJS / UMD 兼容性:很多 npm 包是 CJS,浏览器不支持
2. 减少请求数:lodash 有 600+ 模块,每个请求太慢
3. 缓存:预构建结果缓存在 node_modules/.vite

用什么预构建:
- esbuild(Go 编写,速度极快,比 Babel + Webpack 快 10-100 倍)

触发时机:
- 启动时
- 修改 package.json 后
- 手动:vite --force

6. Vite HMR 原理

Plain
Vite HMR 的工作流程:
1. Vite 通过 WebSocket 与浏览器建立连接
2. 文件修改后,Vite 监听器触发
3. Vite 通过 WebSocket 推送更新消息(仅推送变动的模块)
4. 浏览器收到消息,根据模块类型执行:
   - Vue 组件:调用 component.hot.accept 替换组件
   - 普通 JS:执行 hot.accept 回调
   - CSS:直接替换 style 标签
5. 状态保留:组件更新而非重新加载,状态不丢失

Vite HMR 的优势:
- 只推送变化的模块(不是整个页面)
- ESM 天然支持,无需打包
- 与项目规模无关

7. Vite 插件系统

JavaScript
// 插件本质:带 name 的对象,包含多个生命周期钩子
export default function myPlugin() {
  return {
    name: 'my-plugin',

    // Vite 特有钩子
    configResolved(config) { /* 配置解析后 */ },
    configureServer(server) { /* 配置 Dev Server */ },
    transformIndexHtml(html) { /* 转换 HTML */ },
    handleHotUpdate(ctx) { /* 处理 HMR */ },

    // 兼容 Rollup 钩子
    resolveId(id) { /* 解析模块 ID */ },
    load(id) { /* 加载模块 */ },
    transform(code, id) { /* 转换代码 */ }
  }
}
JavaScript
// 示例:自动注入版本号
export default function injectVersion() {
  return {
    name: 'inject-version',
    transformIndexHtml(html) {
      return html.replace(
        '<body>',
        `<body>
        <script>window.__APP_VERSION__ = "${Date.now()}"</script>`
      )
    }
  }
}

四、Tree Shaking

1. 原理

Plain
Tree Shaking:移除 JavaScript 中未使用的代码(dead code elimination)

依赖:
1. ES Module 静态结构(import/export 不能动态)
2. 副作用分析
3. 模块打包器支持(Webpack / Rollup / esbuild)

注意:CommonJS 不支持(require 是动态的)

2. 配置

JavaScript
// Webpack
module.exports = {
  mode: 'production',  // 自动启用
  optimization: {
    usedExports: true,  // 标记未使用
    sideEffects: false  // 假设所有模块无副作用
  }
}

// package.json
{
  "sideEffects": false,           // 整个项目无副作用
  "sideEffects": ["*.css"]        // 除 CSS 外无副作用
}
JavaScript
// ❌ 有副作用的代码(会被保留)
let count = 0
export function increment() {
  count++
  return count
}

// ✅ 纯函数(可以被 Tree Shaking)
export function add(a, b) {
  return a + b
}

五、Source Map

1. 概念

Plain
Source Map:把编译后的代码映射回源代码,方便调试

格式:main.js.map
内容:
- version:Source Map 版本
- sources:源文件列表
- names:变量名映射
- mappings:位置映射(VLQ 编码)
- file:生成的 JS 文件名
- sourcesContent:源文件内容(可选)

2. 配置

JavaScript
// Webpack
module.exports = {
  devtool: 'source-map'  // 开发环境推荐
  // 'eval-source-map'    // 开发环境,速度更快
  // 'cheap-module-source-map'  // 生产环境(只映射行号)
  // production 不用 devtool 或用 hidden-source-map
}

// Vite
export default defineConfig({
  build: {
    sourcemap: false,  // 生产环境关闭(或 'hidden')
  }
})

3. Source Map 类型对比

Plain
eval            eval 包装,最快,无 Source Map
cheap-source-map 行级映射,无列,无 loader 映射
source-map      完整 Source Map,最慢
eval-source-map eval + Source Map
inline-source-map Source Map 内联到文件
hidden-source-map 不在文件尾添加注释,但生成 .map

六、其他构建工具对比

Plain
Rollup:
- 适合库(Vue、React 都用)
- 产物简洁
- 适合 ES Module

esbuild:
- Go 编写,速度极快
- 用作 Vite 预构建
- 不支持完整 Source Map(开发体验略差)

Parcel:
- 零配置
- 自动识别依赖
- 适合小型项目

Turbopack:
- Rust 编写,Webpack 作者开发
- Next.js 13+ 默认
- 速度快

SWC:
- Rust 编写
- 替代 Babel
- 与 Next.js、Turbopack 集成

七、面试高频问答

Q1: Webpack 和 Vite 的区别?

维度WebpackVite
原理打包所有模块利用浏览器原生 ESM
启动慢(先打包)快(按需加载)
HMR全量更新精准更新
生产Webpack 打包Rollup 打包
适用大型项目、复杂需求现代项目、新项目

核心区别:Vite 利用了现代浏览器原生支持 ESM 这一特性,跳过了开发时的打包过程,从而实现快速冷启动和精准 HMR。

Q2: Vite 为什么快?

  1. 启动快:不打包所有模块,直接用浏览器 ESM 按需加载
  2. 预构建依赖:用 esbuild 把 CJS 转 ESM + 合并依赖(毫秒级)
  3. HMR 快:只更新修改的模块,不刷新整个页面
  4. 按需编译:浏览器请求哪个文件,Vite 才编译哪个

Q3: 什么是 Tree Shaking?依赖什么?

:Tree Shaking 是移除 JavaScript 中未使用代码的优化技术。

依赖条件:

  1. ES Module 静态结构import / export 必须在编译时确定
  2. 副作用分析:在 package.json 中声明 "sideEffects": false
  3. 打包器支持:Webpack / Rollup / esbuild

注意:CommonJS 不支持 Tree Shaking(require 是动态的)。

Q4: Vite 的预构建是干什么的?

:Vite 启动时会用 esbuild 把依赖包预构建成 ESM 格式,主要解决:

  1. CommonJS 兼容:把 CJS / UMD 转为 ESM
  2. 减少请求数:把 lodash 这种多模块包合并成单文件
  3. 缓存优化:缓存到 node_modules/.vite,二次启动极快

八、关联文档