Webpack迁到Vite第六节:构建配置详解(vite.config.js)
适用版本 :Vite 7.3.6 |
@vitejs/plugin-vue21.1.2 |@vitejs/plugin-legacy7.2.1 |vite-plugin-compression0.5.1 |rollup-plugin-visualizer6.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 中的依赖可能有两个问题:
- CJS 格式 :部分包使用
module.exports,浏览器不识别 → esbuild 转 ESM - 碎片文件:部分包有数百个子模块 → 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 异常大,说明需要进一步拆分或优化依赖。