Webpack迁到Vite第六节:构建配置详解(vite.config.js)

Webpack迁到Vite第六节:构建配置详解(vite.config.js)

适用版本 :Vite 7.3.6 | @vitejs/plugin-vue2 1.1.2 | @vitejs/plugin-legacy 7.2.1 | vite-plugin-compression 0.5.1 | rollup-plugin-visualizer 6.0.11

本篇定位 :逐段解读每个配置字段「是什么」(静态字典)。这些字段在一次开发请求或一次生产构建中如何按序协作(动态流转),见第十节:vite.config.js 开发与构建流转全链路。

思维导图

复制代码
vite.config.js --- 构建管线控制中心
├── 1.1 整体架构:defineConfig 函数式配置
│   ├── 函数形式的作用:根据 mode 动态切换行为
│   └── 构建参数读取(npm_config_* 环境变量)
├── 1.2 plugins --- 编译管线
│   ├── vue():SFC 编译,模板中 require() → import 自动转换
│   ├── legacy():ES5 双构建,nomodule 产物兼容旧浏览器(详见第七节(五))
│   ├── svg-sprite:virtual module 插件,编译时合并 86 个 SVG 为单个模块
│   ├── serve-pdfjs-cmaps:configureServer 中间件 + closeBundle 复制
│   ├── viteCompression:构建时预生成 .gz 文件,配合 nginx gzip_static
│   └── visualizer:条件启用构建分析,替代 webpack-bundle-analyzer
├── 1.3 resolve --- 模块解析
│   ├── alias:@ → src/ 路径别名,moment 精确匹配 shim
│   └── extensions:省略扩展名时的自动补全顺序
├── 1.4 base --- 部署路径
│   ├── 控制所有静态资源的 URL 前缀
│   └── 与 PATH_PREFIX 的分工:静态资源路径 vs 业务路由路径
├── 1.5 define --- 编译时常量注入
│   ├── 源码文本替换机制:构建时将标识符替换为字面量
│   ├── JSON.stringify 的作用:确保替换后是合法 JS 表达式
│   └── PATH_PREFIX 的空字符串逻辑:避免协议相对 URL 陷阱
├── 1.6 css --- Less 编译配置
│   ├── additionalData:自动注入变量文件
│   ├── modifyVars:运行时覆盖 Less 变量
│   ├── math: 'always':Less 4.x 除法兼容
│   └── plugins:Less 预处理器,处理 webpack ~ 前缀
├── 1.7 server --- 开发服务器
│   ├── port:监听端口
│   └── proxy:API 请求代理,解决开发时跨域
├── 1.8 build --- 生产构建
│   ├── minify:esbuild 压缩,mode 条件控制
│   ├── commonjsOptions:CJS 混合模块兼容(antd 1.x 残留 require)
│   ├── manualChunks:Rollup 代码分割,按库拆分 chunk
│   └── esbuild.drop:生产环境去除 console
├── 1.9 optimizeDeps --- 依赖预构建(仅开发模式)
│   ├── 原理:Vite 自动扫描入口 import 并预构建 node_modules 依赖
│   └── include:兜底机制,已删除(所有依赖已被 Vite 自动扫描覆盖)
├── 1.10 预压缩(vite-plugin-compression)
│   └── 构建时生成 .gz,nginx gzip_static 直接返回,省去实时压缩 CPU 开销
└── 1.11 构建分析(rollup-plugin-visualizer)
    └── 条件启用,分析打包产物体积组成

1.1 整体架构:defineConfig 与构建参数

javascript 复制代码
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue2'
import path from 'path'
import { readFileSync, cpSync } from 'fs'
import { execSync } from 'child_process'

const pkg = JSON.parse(readFileSync('./package.json', 'utf-8'))
const buildDate = JSON.stringify(new Date().toLocaleString())

let gitHash = 'unknown'
try { gitHash = execSync('git rev-parse --short HEAD').toString().trim() } catch (e) {}

const brandColor = process.env.npm_config_brand_color || '#FF8300'
const customPath = process.env.npm_config_custom_path || '/'
const apiPath = process.env.npm_config_api_path || '/api'
const tagName = process.env.npm_config_tag_name || ''

export default defineConfig(({ mode }) => ({
  // ... 配置 ...
}))

defineConfig 的函数形式

defineConfig 接受一个函数,函数接收 { command, mode } 两个参数:

参数 含义 取值
command 当前运行的命令 'serve'(开发)或 'build'(构建)
mode 当前模式 'development'(vite)或 'production'(vite build)

函数形式的核心价值 :让配置能根据运行上下文动态调整。本项目利用 mode 实现了两处条件行为:

javascript 复制代码
// ① 开发模式不压缩(调试更快),生产模式用 esbuild 压缩
minify: mode === 'production' ? 'esbuild' : false,

// ② 生产模式去除 console,开发模式保留(方便调试)
esbuild: mode === 'production' ? { drop: ['console'] } : {},

静态对象形式也可以达到同样效果(通过 process.env.NODE_ENV 判断),但函数形式中 mode 由 Vite 直接传入,语义更明确------不需要关心 NODE_ENV 是否被正确设置。Webpack 时代的 vue.config.js 同样是静态对象,通过 process.env.NODE_ENV 实现条件逻辑。

process.env.NODE_ENV 在 Vite 中的可用性

vite.config.js 运行在 Node.js 进程中,Vite CLI 启动时自动设置 NODE_ENV:

复制代码
vite         → NODE_ENV=development
vite build   → NODE_ENV=production

vite.config.js 中可以直接读取 process.env.NODE_ENV。而对于浏览器端代码 (src/ 目录下的源码),Vite 同样会自动注入------与 Webpack 的 DefinePlugin 行为一致。Vite 内部在解析配置时(源码 config.js 第 26512-26514 行)自动将其加入 define:

javascript 复制代码
const nodeEnv = process.env.NODE_ENV || config.mode
// 自动注入到 define:
"process.env.NODE_ENV": JSON.stringify(nodeEnv),
"global.process.env.NODE_ENV": JSON.stringify(nodeEnv),
"globalThis.process.env.NODE_ENV": JSON.stringify(nodeEnv)

所以 process.env.NODE_ENV 在浏览器代码中也是可用的 ,开发模式为 "development",生产构建后为 "production"。Vite 另外提供 import.meta.env.MODE 作为 Vite 原生的环境变量访问方式,两者都可以用。

本节说明的是 Vite 对 process.env.NODE_ENV 的自动注入机制(客观事实)。本项目源码中的自定义构建常量 (PATH_PREFIX、API_PATH、BRAND_COLOR 等)已统一采用 import.meta.env.XXX 风格,迁移过程详见第九节 §1.7。

vite.config.js(Node.js 进程) 源码 src/(浏览器)
process.env.NODE_ENV ✅ 可用(Vite CLI 设置) ✅ 可用(Vite 自动注入)
import.meta.env.MODE --- ✅ 可用(Vite 原生)

构建参数读取

process.env.npm_config_* 是 npm 自动设置的环境变量。当运行 npm run build --custom_path=/jtl --brand_color=#FF8300 时,npm 将 -- 后面的参数转换为环境变量:

复制代码
npm run build --custom_path=/jtl
  → npm_config_custom_path = '/jtl'

npm run build --brand_color=#FF8300
  → npm_config_brand_color = '#FF8300'

这些变量在 vite.config.js 执行时(Node.js 进程)读取,然后通过 define 注入到浏览器端代码中。整个数据流:

复制代码
npm CLI 参数 → npm_config_* 环境变量 → vite.config.js 读取 → define 注入 → 浏览器端全局常量

1.2 plugins --- 编译管线

javascript 复制代码
plugins: [
  vue(),
  // 传统浏览器降级:生成 ES5 nomodule 产物,兼容旧 Android WebView 等
  // targets 自动读取 package.json 中的 browserslist,无需重复配置
  legacy({
    additionalLegacyPolyfills: ['regenerator-runtime/runtime'],
  }),
  // SVG Sprite 虚拟模块:编译时合并 50+ 个 SVG 为单个模块,避免开发模式 50+ 次请求(源码注释原文;实际为 86 个)
  {
    name: 'svg-sprite',
    // ... resolveId + load,完整实现见下文「svg-sprite --- SVG 图标虚拟模块」小节
  },
  {
    name: 'serve-pdfjs-cmaps',
    // 开发模式:通过中间件直接从 node_modules 提供 cmaps 文件
    configureServer(server) {
      server.middlewares.use('/cmaps', (req, res) => {
        // connect 挂载 '/cmaps' 会剥离前缀(req.url 为 '/文件名'),path.join 拼接 cmaps 目录
        const filePath = path.join(__dirname, 'node_modules/pdfjs-dist/cmaps', req.url)
        const { readFileSync } = require('fs')
        try { res.end(readFileSync(filePath)) } catch { res.statusCode = 404; res.end() }
      })
    },
    // 生产构建:复制到 dist/cmaps
    closeBundle() {
      cpSync(
        path.resolve(__dirname, 'node_modules/pdfjs-dist/cmaps'),
        path.resolve(__dirname, 'dist/cmaps'),
        { recursive: true },
      )
    },
  },
  viteCompression({ filter: /\.(js|css)$/ }),
  ...(process.env.npm_config_report ? [visualizer({
    open: true, gzipSize: true, filename: 'dist/stats.html',
  })] : []),
],

插件是 Vite 构建管线的核心扩展点。一个 .vue 文件从源码到浏览器可执行代码,经过的管线:

复制代码
.vue 源文件
  → vue() 插件:解析 SFC,编译 template 为 render 函数,处理 script 和 style
  → Vite 核心:解析 import,处理 CSS 预处理器,HMR 注入
  → esbuild/Rollup:语法转换、打包、压缩
  → 自定义插件:额外处理(如 closeBundle 复制文件)
  → 最终产物

vue() --- SFC 编译

@vitejs/plugin-vue2 接管 .vue 文件的处理:

  • <template> → 调用 vue/compiler-sfc 编译为 render() 函数
  • 模板中的 require('@/assets/xxx.png') → 自动转换为 import 语句
  • <script> → 交给 Vite 核心处理(esbuild 语法转换)
  • <style lang="less"> → 交给 Vite 的 CSS 管线(Less 编译 + PostCSS + 注入 DOM)

legacy() --- 旧浏览器 ES5 降级

@vitejs/plugin-legacy 用 Babel + core-js 对打包产物做二次转译,生成 ES5 传统产物(<script nomodule>)供旧浏览器(旧 Android WebView 等)加载:

javascript 复制代码
legacy({
  additionalLegacyPolyfills: ['regenerator-runtime/runtime'],
}),
  • 双构建:现代产物(ESM)给现代浏览器,传统产物(ES5)给旧浏览器,浏览器二选一加载,现代浏览器不会增加下载体积
  • targets :自动读取 package.json 中的 browserslist(> 1%, last 2 versions, not ie <= 10),降级目标与 Webpack 时期一致,无需重复配置
  • additionalLegacyPolyfills :额外注入 regenerator-runtime/runtime,覆盖传统产物中 async/await 转译所需的 regenerator 运行时

方案选型论证、产物结构与构建实测数据详见第七节(五):全家桶移除 --- ES5 降级方案专题。

svg-sprite --- SVG 图标虚拟模块

javascript 复制代码
{
  name: 'svg-sprite',
  resolveId(id) {
    if (id === 'virtual:svg-icons') return 'virtual:svg-icons'
  },
  load(id) {
    if (id === 'virtual:svg-icons') {
      const { readdirSync, readFileSync } = require('fs')
      const svgDir = path.resolve(__dirname, 'src/assets/svgIcons')
      const symbols = readdirSync(svgDir)
        .filter((f) => f.endsWith('.svg'))
        .map((f) => {
          const raw = readFileSync(path.resolve(svgDir, f), 'utf-8')
          const name = f.replace('.svg', '')
          const viewBoxMatch = raw.match(/viewBox="([^"]+)"/)
          const viewBox = viewBoxMatch ? ` viewBox="${viewBoxMatch[1]}"` : ''
          const inner = raw.replace(/<svg[^>]*>/, '').replace('</svg>', '').trim()
          return `<symbol id="icon-${name}"${viewBox}>${inner}</symbol>`
        }).join('')
      return `
        const el = document.createElement('div')
        el.style.cssText = 'width:0;height:0;position:absolute;overflow:hidden;'
        el.innerHTML = ${JSON.stringify(`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">${symbols}</svg>`)}
        document.body.appendChild(el)
      `
    }
  },
}

resolveId :拦截 virtual:svg-icons 的模块解析请求,返回 \0 前缀的虚拟模块 ID。\0 是 Rollup 约定------表示该模块是虚拟的,不来自文件系统。

load :当 Vite 请求加载虚拟模块时,用 Node.js 的 readdirSync + readFileSync 在编译时扫描并读取所有 SVG 文件,拼接为 SVG Sprite 注入代码。JSON.stringify 确保 SVG 内容中的 " 被正确转义,避免生成的 JS 代码语法错误。

效果:开发模式下 86 个 SVG 请求 → 1 个虚拟模块请求。详见第四节。

serve-pdfjs-cmaps --- 开发/生产一致处理 pdfjs cmaps

javascript 复制代码
{
  name: 'serve-pdfjs-cmaps',
  // 开发模式:通过中间件直接从 node_modules 提供 cmaps 文件
  configureServer(server) {
    server.middlewares.use('/cmaps', (req, res) => {
      // connect 挂载 '/cmaps' 会剥离前缀(req.url 为 '/文件名'),path.join 拼接 cmaps 目录
      const filePath = path.join(__dirname, 'node_modules/pdfjs-dist/cmaps', req.url)
      const { readFileSync } = require('fs')
      try { res.end(readFileSync(filePath)) } catch { res.statusCode = 404; res.end() }
    })
  },
  // 生产构建:复制到 dist/cmaps
  closeBundle() {
    cpSync(
      path.resolve(__dirname, 'node_modules/pdfjs-dist/cmaps'),
      path.resolve(__dirname, 'dist/cmaps'),
      { recursive: true },
    )
  },
},

pdfjs-dist 渲染 PDF 时需要 CMap 文件(字符映射表,用于中文等非拉丁字符,约 1.4MB、169 个文件),运行时通过 cMapUrl: '/cmaps/' 加载。

configureServer :Vite 插件的钩子,开发服务器启动时调用,server 是 Vite 的内部开发服务器实例。server.middlewares 是一个中间件栈,与 Express 的 app.use() 用法一致。

javascript 复制代码
server.middlewares.use('/cmaps', (req, res) => {
  // connect 挂载 '/cmaps' 会剥离前缀(req.url 为 '/文件名'),path.join 拼接 cmaps 目录
  const filePath = path.join(__dirname, 'node_modules/pdfjs-dist/cmaps', req.url)
  const { readFileSync } = require('fs')
  try { res.end(readFileSync(filePath)) } catch { res.statusCode = 404; res.end() }
})

逐行拆解:

代码 作用
server.middlewares.use('/cmaps', ...) 拦截所有 /cmaps/ 开头的请求
req.url connect 会剥离挂载前缀 ,handler 内 req.url 为 /xxx.bcmap(不含 /cmaps/)
path.join(__dirname, 'node_modules/pdfjs-dist/cmaps', req.url) 拼接为 node_modules/pdfjs-dist/cmaps/xxx.bcmap。用 path.join 而非 path.resolve------join 天然忽略 req.url 的前导斜杠;resolve 会把 /xxx.bcmap 视为盘根绝对路径(Windows 下解析为 D:\xxx.bcmap),导致文件读取失败
const { readFileSync } = require('fs'); readFileSync(filePath) 先解构再同步读取文件内容
res.end(...) 将文件内容作为响应体返回
try { ... } catch { res.statusCode = 404; res.end() } 文件不存在时返回 404,防止服务器崩溃

这样开发模式下 /cmaps/xxx.bcmap 请求直接从 node_modules 读取,不复制、零额外磁盘操作,与生产行为一致。

closeBundle :生产构建时,Rollup 写入所有产物后,用 cpSync 将 CMap 文件复制到 dist/cmaps/。

cpSync(src, dest, { recursive: true }) :Node.js 16.7+ 内置的同步复制 API。recursive: true 是递归复制目录的关键参数,等价于 cp -r。

与 Webpack 时期对比 :原 CopyWebpackPlugin 一行配置搞定 dev 和 build------它是成熟插件,内部已处理两种模式。Vite 没有等价的内置机制,public/ 目录只能放项目文件不能放 node_modules,closeBundle 只覆盖 build,所以 dev 模式需要额外写 configureServer 中间件。

如果有多个类似需求 ,可以装 vite-plugin-static-copy,一行配置等价于 CopyWebpackPlugin:

javascript 复制代码
import { viteStaticCopy } from 'vite-plugin-static-copy'

viteStaticCopy({
  targets: [{ src: 'node_modules/pdfjs-dist/cmaps/*', dest: 'cmaps' }],
})

但这样多一个依赖。当前手写方案 15 行代码,零依赖,效果一样。

viteCompression --- 预生成 .gz 文件

构建时对 JS 和 CSS 产物生成对应的 .gz 文件。部署后,nginx 的 gzip_static on 检测到同名 .gz 文件存在时直接返回,跳过实时压缩------零 CPU 开销。

复制代码
构建时:index.js (388KB) → index.js.gz (128KB)
运行时:nginx 收到请求 → 检测到 index.js.gz → 直接返回(无 CPU 消耗)

详见 [1.10 预压缩](#1.10 预压缩)。

visualizer --- 条件启用构建分析

... 展开运算符实现条件插件:npm run build --report 时才启用,普通构建不生成分析报告。详见 [1.11 构建分析](#1.11 构建分析)。


1.3 resolve --- 模块解析

javascript 复制代码
resolve: {
  alias: [
    { find: '@', replacement: path.resolve(__dirname, 'src') },
    { find: /^moment$/, replacement: path.resolve(__dirname, 'src/shims/moment.js') },
  ],
  extensions: ['.mjs', '.js', '.vue', '.ts', '.tsx', '.json'],
},

alias --- 路径别名

@ → src/ :import X from '@/components/X' 等价于 import X from 'src/components/X'。与 Webpack 的 resolve.alias 完全一致。

/^moment$/ → shim :精确匹配 moment(不匹配 moment/locale/zh-cn 等子路径),将其重定向到 shim 文件,补充 isMoment 命名导出。详见第五节 Moment Shimming。

数组格式 :支持 find 为正则表达式,实现精确匹配。对象格式 { moment: 'shim' } 是前缀匹配,会把 moment/locale/zh-cn 也拦截。

extensions --- 扩展名自动补全

import X from './util' 时,按顺序尝试 .mjs → .js → .vue → .ts → .tsx → .json。

.mjs 是什么? mjs = Module JavaScript。Node.js 中 .js 默认是 CommonJS,.mjs 强制标识为 ES Module(对应的 .cjs 强制标识为 CommonJS)。它放在第一位是因为它是明确的 ESM 信号,优先匹配可以减少模块格式歧义。项目中虽然没有 .mjs 文件,但这是一个向前兼容的配置。


1.4 base --- 部署路径

javascript 复制代码
base: customPath,  // 默认 '/',可通过 --custom_path=/jtl 指定子路径

base 控制所有静态资源的 URL 前缀 。构建时,index.html 中的资源引用会被改写:

复制代码
base: '/'     → <script src="/assets/index-xxx.js">
base: '/jtl'  → <script src="/jtl/assets/index-xxx.js">

与 PATH_PREFIX 的分工

base 和 PATH_PREFIX 是两个不同的概念,控制不同的路径:

base PATH_PREFIX
配置位置 vite.config.js 的 base 字段 define 中的全局常量
作用范围 index.html 中的静态资源引用 业务代码中的路由跳转
影响对象 <script>、<link>、url() 等 location.replace()、router base、window.open()
典型值 '/' 或 '/jtl' '' 或 '/jtl'

为什么需要两个 :base 只在构建时作用于 index.html,无法影响运行时的业务逻辑跳转。业务代码中 location.replace()、window.open() 共 50 处,PATH_PREFIX 引用共 69 处,运行时跳转需要通过它拼接路径。

base 的路径拼接规则

Vite 在拼接 base 和资源路径时,自动在中间添加 /:

复制代码
base: '/'     → '/' + '/' + 'assets/xxx.js' → '/assets/xxx.js'
                 ↑ 已以 / 结尾,不需要补

base: '/jtl'  → '/jtl' + '/' + 'assets/xxx.js' → '/jtl/assets/xxx.js'
                 ↑ 不以 / 结尾,Vite 自动补 /

所以 base: '/jtl' 不会变成 /jtlassets/xxx.js。这与 path.join('/jtl', 'assets/xxx.js') 的行为一致------Node.js 的路径拼接自动处理分隔符。

为什么 base 默认是 '/' 而 PATH_PREFIX 默认是 ''

base PATH_PREFIX
默认值 '/' ''(customPath === '/' 时)
为什么 HTML 资源路径必须以 / 开头 业务路径拼接:'' + '/login' = '/login'
如果 PATH_PREFIX 也是 '/' --- '/' + '/login' = '//login' → 协议相对 URL,跳转错误

1.5 define --- 编译时常量注入

javascript 复制代码
define: {
  'import.meta.env.PATH_PREFIX': JSON.stringify(customPath === '/' ? '' : customPath),
  'import.meta.env.API_PATH': JSON.stringify(apiPath),
  'import.meta.env.BRAND_COLOR': JSON.stringify(brandColor),
  'import.meta.env.IS_MOCK': JSON.stringify(false),
  'import.meta.env.APP_VERSION': JSON.stringify(pkg.version),
  'import.meta.env.APP_TAGNAME': JSON.stringify(tagName),
  'import.meta.env.APP_NAME': JSON.stringify(pkg.name),
  'import.meta.env.GIT_HASH': JSON.stringify(gitHash),
  'import.meta.env.BUILD_DATE': buildDate,
},

工作机制:源码文本替换

define 在构建时 将源代码中的标识符直接替换为值。这不是运行时赋值,而是构建时的文本替换:

javascript 复制代码
// 源代码
const apiUrl = import.meta.env.API_PATH
const base = import.meta.env.PATH_PREFIX

// 构建后(API_PATH='/api', PATH_PREFIX='')
const apiUrl = "/api"
const base = ""

替换发生在 esbuild/Rollup 的代码生成阶段,不是运行时。所以这些值被打包进了最终产物,无法在运行时修改------它们是"编译时常量"。

为什么必须用 JSON.stringify

define 做的是源码文本替换 ------构建时把标识符直接替换为 value 的文本内容。问题的根源在于:JS 在解析 define 这个对象时,会先吃掉 value 的引号。

javascript 复制代码
define: { 'import.meta.env.API_PATH': '/api' }
//       ↑ key 的引号告诉 Vite 找什么标识符
//                                  ↑  ↑ 这两个引号是 JS 解析器的语法边界
//                                     JS 解析后,value 只剩三个字符:a p i
//                                     Vite 把这三个字符原样贴进源码 → /api → 正则!

所以 JSON.stringify 做的事情就是在 value 里面再包一层引号------让引号成为字符串内容,不被 JS 解析器吃掉:

javascript 复制代码
define: { 'import.meta.env.API_PATH': JSON.stringify('/api') }
// 等价于
define: { 'import.meta.env.API_PATH': '"/api"' }
//                                 ↑        ↑ 外层引号:JS 解析器吃掉
//                                  ↑      ↑ 内层引号:字符串内容,保留
//                          value = "/api"(六个字符,含双引号)
//                          Vite 贴进源码 → "/api" → 合法字符串 ✅

这不是 Vite 独有的设计 。Webpack 的 DefinePlugin 完全一样------底层都是文本替换,都需要 JSON.stringify。Vue CLI 在内部自动完成了 JSON.stringify,所以 vue.config.js 中不需要显式写出。

JSON.stringify 把任何 JS 值转换为它的字面量表示:

值 JSON.stringify 结果 替换后效果
'/api' '"/api"' "/api"
false 'false' false
42 '42' 42
{a:1} '{"a":1}' {"a":1}
{a:1} '{"a":1}' {"a":1}

PATH_PREFIX 的空字符串逻辑

javascript 复制代码
JSON.stringify(customPath === '/' ? '' : customPath)

当 customPath = '/'(默认部署在根路径)时,PATH_PREFIX = ''(空字符串)。如果设为 '/',会产生双斜杠:

javascript 复制代码
// 如果 PATH_PREFIX = '/'
const url = `${PATH_PREFIX}/login`  // → '//login'
// 浏览器解析 '//login' 为协议相对 URL → 跳转到 http://login(错误!)

// PATH_PREFIX = ''
const url = `${PATH_PREFIX}/login`  // → '/login'
// 浏览器解析 '/login' 为根路径 → 正确跳转

本项目已将 process.env.XXX 全部迁移为 import.meta.env.XXX(语义正确、tree-shaking 友好)。完整迁移过程(define 改造、40 个文件源码批量替换、ESLint 两层配套)见第九节 §1.7。


1.6 css --- Less 编译配置

javascript 复制代码
css: {
  preprocessorOptions: {
    less: {
      additionalData: (source, filePath) => {
        if (filePath.includes('default.less')) return source
        return `${source}\n@import "${path.resolve(__dirname, 'src/styles/default.less')}";`
      },
      javascriptEnabled: true,
      math: 'always',
      modifyVars: { '@brand-color': brandColor },
      plugins: [{
        install(_, pluginManager) {
          pluginManager.addPreProcessor({
            process(src) {
              return src.replace(/@import\s+['"](~[^'"]+)['"]/g,
                (_, p) => `@import "${p.replace(/^~/, '')}"`)
            },
          })
        },
      }],
    },
  },
},

每一项配置的详细解读见第三节(Less 与 CSS 处理)。这里从构建管线角度总结:

配置项 在管线中的作用 影响范围
additionalData 每个 Less 文件编译前,自动追加变量文件内容 所有 .less 文件
modifyVars Less 编译器层面覆盖变量值,优先级最高 所有 Less 文件(含 node_modules)
math: 'always' 恢复 Less 3.x 的除法行为,兼容旧版 antd 代码 Less 编译器
javascriptEnabled: true 允许 Less 中使用 JavaScript 表达式(antd 需要) Less 编译器
plugins Less 预处理器,处理 webpack 的 ~ 前缀 Less 编译前

1.7 server --- 开发服务器

javascript 复制代码
server: {
    host: true,       // 监听所有网卡,终端打印局域网 IP
    port: 8080,
    strictPort: false, // 端口被占用时自动递增(8080→8081→...)
    proxy: {
      '/api': {
        target: 'https://<后端服务地址>/api', // 随开发环境切换(测试/沙箱/演示/本地环境在配置中注释切换)
        changeOrigin: true,
        rewrite: (p) => p.replace(/^\/api/, ''),
      },
    },
},

proxy --- API 代理

开发时前端运行在 localhost:8080,后端 API 在另一台服务器上。浏览器直接请求后端会跨域。Vite 的开发服务器作为代理,转发 /api 开头的请求:

复制代码
浏览器 → localhost:8080/api/user/login
  → Vite 代理:先 rewrite 去掉 /api 前缀 → /user/login
  → 再拼接 target → https://<后端服务地址>/api/user/login
  → 返回响应 → 浏览器

changeOrigin: true 修改请求头中的 Origin 为目标地址,避免后端校验 Origin 导致拒绝。


1.8 build --- 生产构建

javascript 复制代码
build: {
  outDir: 'dist',
  assetsDir: 'assets',
  sourcemap: false,
  minify: mode === 'production' ? 'esbuild' : false,
  // 处理 ant-design-vue 1.x 内部残留的 CJS require() 调用
  commonjsOptions: {
    transformMixedEsModules: true,
    include: [/node_modules/],
  },
  rollupOptions: {
    output: {
      manualChunks: {
        'ant-design-vue': ['ant-design-vue'],
        vant: ['vant'],
        // 框架运行时: 稳定库独立缓存,版本不变时命中浏览器强缓存
        vendor: ['vue', 'vuex', 'vue-router', 'axios', 'moment'],
      },
    },
  },
},
// esbuild 配置独立于 build.minify,即使不压缩也会生效
esbuild: mode === 'production' ? { drop: ['console'] } : {},

outDir、assetsDir、sourcemap --- 输出控制

配置项 值 作用
outDir 'dist' 构建产物输出目录,相对于项目根目录。与 Webpack 的 output.path 对应
assetsDir 'assets' outDir 下的静态资源子目录。JS、CSS、图片等产物放在 dist/assets/ 下
sourcemap false 不生成 .map 文件。生产构建不需要源码映射,减小部署体积。开发模式默认启用

minify --- 生产环境压缩

mode === 'production' 时启用 esbuild 压缩,开发模式不压缩(调试更快)。esbuild 是 Go 语言编写的压缩器,速度远快于 Webpack 时代的 terser。

commonjsOptions --- CJS 模块兼容

@rollup/plugin-commonjs 默认只处理纯 CJS 模块(文件中只有 require 和 module.exports)。但 ant-design-vue 1.x 的部分模块同时包含 import 和 require()(混合模块),插件默认跳过它们,导致 require() 原样保留在输出中,浏览器报 require is not defined。

配置项 作用
transformMixedEsModules: true 转换同时包含 import 和 require() 的混合模块
include: [/node_modules/] 覆盖所有 node_modules 依赖,确保不遗漏

manualChunks --- 代码分割

Rollup 默认将所有代码打包到一个 JS 文件。manualChunks 手动指定哪些模块拆分为独立 chunk:

javascript 复制代码
manualChunks: {
  'ant-design-vue': ['ant-design-vue'],  // 所有 antd 模块 → ant-design-vue-xxx.js
  vant: ['vant'],                        // 所有 vant 模块 → vant-xxx.js
  vendor: ['vue', 'vuex', 'vue-router', 'axios', 'moment'],  // 框架运行时 → vendor-xxx.js
}

为什么拆分 antd 和 vant?

  • 体积大:antd ~1.4MB,vant ~350KB,不拆分会导致主 bundle 超过 2MB
  • 变更少:UI 框架版本固定,chunk hash 几乎不变,浏览器可长期缓存
  • 不重叠:PC 端用 antd,移动端用 vant,分开打包避免用户加载用不到的库

为什么拆分 vendor? :vue/vuex/vue-router/axios/moment 是最稳定的框架运行时------版本升级频率远低于业务代码,单独拆出后命中强缓存的概率最高。(lodash 已改为 lodash-es 按需导入,体积仅 ~8KB,不再值得独立拆包,详见第九节 §1.6)

浏览器缓存效果:修改业务代码后,只有主 chunk 的 hash 变化,antd、vant 和 vendor 的 chunk 命中缓存,用户只需下载变化的业务代码而非全部 2MB。

esbuild.drop --- 生产环境去除 console

javascript 复制代码
esbuild: mode === 'production' ? { drop: ['console'] } : {},

drop: ['console'] 在代码生成阶段删除所有 console.xxx() 调用。它配置在 esbuild 字段而非 build.minify 中------两者独立:minify 控制代码压缩(空格、变量名),drop 控制特定语句的删除。即使 minify: false,drop 仍然生效。开发模式保留 console,方便调试。


1.9 optimizeDeps --- 依赖预构建(仅开发模式)

当前配置

当前 vite.config.js 中已无 optimizeDeps 字段 ------迁移初期曾声明 4 个条目(ant-design-vue、vant、moment、lodash),后验证 Vite 自动扫描已全覆盖,字段最终被完整删除:

javascript 复制代码
// 演进过程(vite.config.js git 历史):
// ① 迁移初始:include: ['ant-design-vue', 'vant', 'moment', 'lodash']
// ② lodash 优化:lodash → lodash-es
// ③ 最终版本:整个 optimizeDeps 字段删除(Vite 自动扫描已全覆盖)

optimizeDeps 解决什么问题

Vite 开发模式不打包,浏览器直接加载 ESM 模块。node_modules 中的依赖可能有两个问题:

  1. CJS 格式 :部分包使用 module.exports,浏览器不识别 → esbuild 转 ESM
  2. 碎片文件:部分包有数百个子模块 → esbuild 合并为单个文件

Vite 的自动扫描

Vite 启动时会扫描入口文件中的 import 语句,自动发现并预构建 所有 node_modules 依赖。预构建产存放在 node_modules/.vite/deps/ 下,浏览器请求时直接返回预构建后的单个 ESM 文件。

复制代码
import { throttle } from 'lodash-es'
  → Vite 自动扫描 → 发现 lodash-es
  → esbuild 预构建 → node_modules/.vite/deps/lodash-es.js
  → 浏览器加载 → 1 次 HTTP 请求

include 的作用

include 是兜底机制------当 Vite 的自动扫描漏掉了某个依赖时手动补充。常见场景:

  • 动态 import() 路径无法静态分析
  • 深层嵌套依赖未被入口文件直接 import
  • 依赖在 Vite 插件中动态生成

本项目中 main.js 直接 import 了所有依赖(antd、vant、moment、lodash-es),Vite 自动扫描已全部覆盖,include 不再需要。实测移除整个 optimizeDeps 字段后,首屏请求数无增加。


1.10 预压缩(vite-plugin-compression)

javascript 复制代码
viteCompression({ filter: /\.(js|css)$/ })

构建时对 JS 和 CSS 产物生成 .gz 文件。部署后,nginx 的 gzip_static on 检测到同名 .gz 文件时直接返回,完全跳过实时压缩:

复制代码
构建产物:index.js (388KB) + index.js.gz (128KB)
运行时:nginx 收到请求 → 检测到 index.js.gz → 直接返回(零 CPU,最快响应)

即使私有化部署,当客户量足够大时,每次请求实时压缩的 CPU 累积开销不可忽略。预压缩让 nginx 的静态文件服务直接返回,是最优方案。

为什么只压缩 JS 和 CSS

dist 产物中其他类型不需要压缩:

类型 是否需要 原因
.js / .css ✅ 文本,压缩率 65-88%,体积大,收益最高
.png / .ico ❌ 二进制格式自身已压缩,再 gzip 可能膨胀
.html ❌ 仅一个文件(~2.2KB,含 legacy 插件注入的脚本),预压缩收益可忽略
.json 暂不需要 项目 i18n 文件(zh.json 14KB、en.json 15KB)通过 import 引入,已打入 JS bundle,不会作为独立文件出现在 dist/ 中。将来若改为动态加载(fetch)且文件变大,改为 `/.(js

filter 也支持函数形式做更精细控制:filter: (file) => /\.(js|css)$/.test(file)。


1.11 构建分析(rollup-plugin-visualizer)

javascript 复制代码
...(process.env.npm_config_report ? [visualizer({
  open: true,          // 构建完成后自动在浏览器中打开分析报告
  gzipSize: true,      // 显示每个 chunk 经 gzip 压缩后的预估大小
  filename: 'dist/stats.html', // 输出文件名,放在 dist/ 中与构建产物生命周期一致
})] : []),

配置项详解

参数 类型 作用
open boolean true 时构建完成后自动用浏览器打开 dist/stats.html,方便立即查看
gzipSize boolean true 时在报告中额外显示 gzip 压缩后的体积。gzip 是 nginx 最常用的压缩格式,这个数据更接近用户实际下载大小
filename string 报告文件名。'dist/stats.html' 生成在 dist/ 目录中(随构建产物一起清理),也可以设为 'stats.json' 输出 JSON 数据

条件启用

... 展开运算符实现条件插件:process.env.npm_config_report 只有运行 npm run build --report 时才为 true。普通 npm run build 时三元表达式返回 [],展开后为空,插件不生效。

bash 复制代码
npm run build              # 普通构建,不生成分析报告
npm run build --report     # 构建 + 自动打开浏览器展示 dist/stats.html

分析报告解读

dist/stats.html 以矩形树图展示每个 chunk 的体积占比。矩形越大,体积越大。鼠标悬停可查看原始大小、gzip 大小。典型分析结果:

复制代码
ant-design-vue chunk: 1.4MB (gzip: 419KB)  ← 最大,但几乎不变更
vant chunk: 350KB (gzip: 89KB)             ← 移动端 UI 库
index chunk: 388KB (gzip: 128KB)           ← 业务代码,频繁变更

如果某个 chunk 异常大,说明需要进一步拆分或优化依赖。

相关推荐
风骏时光牛马1 小时前
从零到精通AI全栈学习路线
前端
Autumn_ing10 小时前
Design.md生成UI怎么验收?举例运营后台的颜色、状态和数据核对
前端·ui·ai·设计系统·paico·ai生成ui·design.md
远离UE410 小时前
UE5 Substrate Thin Film 源码拆析
开发语言·javascript·ue5
keyipatience10 小时前
JavaScript学习(1)书写位置,变量、数据类型、运算符
开发语言·前端·javascript·css·学习
朱涛的自习室11 小时前
我给 Gemini 4 和 DeepSeek 搞了套「末位淘汰」,AI 干活会更猛吗?(Day 3)
android·前端·后端
java1234_小锋13 小时前
【技术专题】HTML5 网页技术 - HTML5简介 & HTML5 文档结构
前端·html·html5
鶴哥只手遮天13 小时前
Vue 3 核心源码深度剖析系列(v3.5.44)-第 1 章 项目全景:monorepo 架构与工程化体系
前端·vue.js·开源
流星。。方13 小时前
小团队官网SEO落地:前端语义化与静态生成的工程实践
前端·状态模式
用户5304297657914 小时前
盘点危化品搬运诊断的6类工具与自处理边界判断
前端