Webpack迁到Vite第十节:vite.config.js 开发与构建流转全链路

Webpack迁到Vite第十节:vite.config.js 开发与构建流转全链路

本篇定位 :第六节逐段解读了 vite.config.js 的每个配置字段「是什么」------那是一本字典。但字典不回答 「它们如何协作」 :一次开发模式的页面请求,从浏览器发出到模块返回,中间经历了哪些中间件、哪些插件、哪条转换链?一次 npm run build,各插件在哪个钩子执行、产物如何一步步生成?

本篇按时间线(请求发出 → 响应返回 / 构建开始 → 产物落盘)串起全部配置与插件,给出完整流转图。阅读顺序建议:先读第六节(知道每个字段是什么),再读本篇(知道它们怎么动起来)。

思维导图

bash 复制代码
vite.config.js 开发与构建流转全链路
├── 一、配置文件的加载与初始化
│   ├── 1.1 从 vite CLI 到 defineConfig(顶层常量求值 → defineConfig(mode) → 配置对象)
│   ├── 1.2 构建参数的读取链(npm_config_* → brandColor/customPath/apiPath/tagName)
│   └── 1.3 插件注册与执行序(5+ 插件 × 钩子分布表)
├── 二、开发模式:一条请求的完整流转
│   ├── 2.1 启动阶段:依赖扫描与预构建(.vite/deps)
│   ├── 2.2 请求处理:中间件与代理(自定义中间件 → 代理 → 转换管线)
│   ├── 2.3 一次页面请求的完整转换管线(含 HAR 实证)
│   ├── 2.4 Less 编译链(additionalData → modifyVars → ~ 插件 → CSS 注入)
│   ├── 2.5 HMR 热更新链(保存 → 重编译 → 推送 → 局部替换)
│   └── 2.6 开发模式流转小结
├── 三、生产构建:一次 npm run build 的完整流转
│   ├── 3.1 构建主流程(四阶段)与插件参与点
│   ├── 3.2 本项目插件 × 钩子分工表(6 插件矩阵)
│   ├── 3.3 代码转换链:从源码到 chunk(define → tree-shaking → manualChunks → minify)
│   ├── 3.4 legacy 双构建(现代/传统两类产物 + 双脚本注入)
│   └── 3.5 dist/ 产物结构全解
├── 四、开发 vs 构建:同一件事的两种路径(13 项对照总表)
└── 附:与本篇互引的文档

一、配置文件的加载与初始化

1.1 从 vite CLI 到 defineConfig

arduino 复制代码
$ npm run serve   (或 npm run build)
     │
     ▼
vite / vite build(CLI 进程启动)
     │
     ▼ 定位配置文件 vite.config.js → 作为 ESM 模块 import 执行
     │
     ├─① 模块顶层代码先执行(defineConfig 之外)
     │    ├── readFileSync('package.json')  → pkg
     │    ├── new Date().toLocaleString()   → buildDate
     │    ├── execSync('git rev-parse')     → gitHash
     │    └── process.env.npm_config_*      → brandColor / customPath / apiPath / tagName
     │
     ├─② defineConfig(fn) 调用 → 传入 { mode, command }
     │    ├── serve → mode='development',build → mode='production'
     │    └── 返回完整配置对象(plugins/resolve/define/css/server/build/...)
     │
     └─③ Vite 消费配置对象:
          ├── serve 路径 → 创建 dev server(见 §2.1 ~ §2.6)
          └── build 路径 → 启动 Rollup 构建(见 §3.1 ~ §3.5)

关键点 :defineConfig 的函数形式让 mode 参与配置决策------本项目的两处条件配置都依赖它:

javascript 复制代码
minify: mode === 'production' ? 'esbuild' : false,      // build.minify
esbuild: mode === 'production' ? { drop: ['console'] } : {},  // drop console
...(process.env.npm_config_report ? [visualizer({...})] : []),  // --report 参数

1.2 构建参数的读取链(npm_config_* 机制)

arduino 复制代码
npm run serve --brand_color=#DC143C --custom_path=/privatization
     │
     ▼ npm 把 --xxx 解析为自己的配置 → 设环境变量
npm_config_brand_color=#DC143C / npm_config_custom_path=/privatization
     │
     ▼ vite.config.js 顶层读取
brandColor = process.env.npm_config_brand_color || '#FF8300'
customPath = process.env.npm_config_custom_path || '/'
     │
     ├──→ base: customPath                       (部署基础路径)
     ├──→ modifyVars: { '@brand-color': brandColor }  (Less 品牌色,见 2.4)
     └──→ define: 'import.meta.env.BRAND_COLOR' → ...   (源码编译时常量)

四个参数的完整消费路径见第六节 §1.1 与第七节(四)§1.4;此处只强调时序 :它们在配置对象构造之前就已确定,因此可以被 plugins / css / define / server 各段同时消费。

1.3 插件注册与执行序

plugins 数组有 5+ 个成员(含内联对象插件),数组顺序 = 同名钩子的执行顺序:

javascript 复制代码
plugins: [
  vue(),                  // ① @vitejs/plugin-vue2 ------ SFC 编译
  legacy({...}),          // ② @vitejs/plugin-legacy ------ ES5 降级(生产)
  { name: 'svg-sprite' }, // ③ 内联插件 ------ SVG 虚拟模块
  { name: 'serve-pdfjs-cmaps' }, // ④ 内联插件 ------ cmaps 供给
  viteCompression({...}), // ⑤ 预压缩
  ...[visualizer({...})], // ⑥ 构建分析(--report 时)
],

各插件的钩子分布(后面两章展开):

插件 钩子 生效模式
@vitejs/plugin-vue2 configResolved(注入 vue 别名)、transform(SFC 拆解)、load(?vue&type= 子模块)、handleHotUpdate dev + build
@vitejs/plugin-legacy config(改 build.target)、renderChunk、generateBundle、transformIndexHtml 仅 build
svg-sprite(内联) resolveId + load(虚拟模块) dev + build
serve-pdfjs-cmaps(内联) configureServer(dev 中间件)+ closeBundle(生产复制) dev / build 各一半
vite-plugin-compression closeBundle(生成 .gz) 仅 build
rollup-plugin-visualizer generateBundle(生成 stats.html) 仅 build

二、开发模式:一条请求的完整流转

本章涉及的配置段:server(host / port / proxy)、css.preprocessorOptions.less、plugins(svg-sprite / serve-pdfjs-cmaps / vue2)

2.1 启动阶段:依赖扫描与预构建

yaml 复制代码
vite(dev)启动
  │
  ├──① 解析配置(§1.1 ~ §1.3)→ 创建中间件与代理(见 §2.2)
  │
  ├──② optimizeDeps 依赖扫描:
  │     config 中没有显式 include(本项目已删除该字段)
  │     → Vite 自动扫描入口 HTML 与 JS 引用
  │     → 发现 node_modules 依赖(vue/vuex/vue-router/axios/moment/lodash-es/antd/vant...)
  │
  ├──③ esbuild 预构建 → 写入 node_modules/.vite/deps/:
  │     ├── 目的①:CJS→ESM 转换(moment 是 UMD,浏览器不认)
  │     ├── 目的②:碎片合并(lodash-es 数百个子模块 → lodash-es.js 单文件)
  │     └── 产出形如:vue.js、lodash-es.js、chunk-xxxxx.js(共享分块)
  │
  └──④ 启动 HTTP 服务(vite.config.js 配置 port: 8080、host: true 监听所有网卡;Vite 自身默认端口为 5173)

一条实用结论 :预构建产物不经过插件转换管线------这正是第八节 §1.5「transform 插件为何失败」的机制根源。

2.2 请求处理:中间件与代理

一个 HTTP 请求进入 dev server 后,与本项目配置相关的处理路径有三层:

bash 复制代码
HTTP 请求
  │
  ├─ ① 自定义中间件(plugins 的 configureServer 钩子注册)
  │      └─ 本项目:serve-pdfjs-cmaps 插件的 /cmaps 中间件
  │           匹配 /cmaps/* → 读 node_modules/pdfjs-dist/cmaps 直接返回
  │           不匹配 → 放行给下一层
  │
  ├─ ② 代理(server.proxy 配置)
  │      └─ 本项目:/api 前缀 → 转发到后端服务器
  │           rewrite 去掉 /api 前缀;changeOrigin 改写 Origin 头
  │
  └─ ③ Vite 转换管线(其余请求)
        .vue / .js / .less 等源码模块 → 编译后返回(见 2.3)

顺序要点 :/cmaps 中间件先于 /api 代理被匹配------两者路径前缀不同、互不冲突;且自定义中间件位于代理之前,意味着它可以拦截本会走到代理的请求(这也是 /cmaps 必须放行 next() 的原因)。

2.3 一次页面请求的完整转换管线

以浏览器访问 http://localhost:8080/login 加载 src/views/pc/user/login.vue 为例:

bash 复制代码
浏览器                    dev server                          说明
  │
  │── GET /login ─────────▶ 返回处理后的 index.html
  │                           └─ 注入 <script type="module" src="/@vite/client">(HMR 客户端)
  │
  │── GET /@vite/client ───▶  HMR 客户端运行时(dev 模式固定开销)
  │                              └─ 其内部再请求 /node_modules/vite/dist/client/env.mjs(环境变量注入)
  │
  │── GET /src/main.js ────▶  resolve.alias 解析 → 编译返回:
  │                           ├─ esbuild 转译(仅语法层)
  │                           └─ 改写裸导入:'vue' → '/node_modules/.vite/deps/vue.js?v=xxx'
  │                              (main.js 本身不注入 HMR 边界代码;HMR 注入仅出现在
  │                               引用 import.meta.hot 的模块,如 SFC 编译产物)
  │
  │── GET /node_modules/.vite/deps/vue.js?v=xxx ──▶ 预构建产物直接返回(不进转换管线)
  │
  │  ...... 模块瀑布:main.js 的每条 import 级联触发下一轮请求 ......
  │
  │── GET /src/views/pc/user/login.vue ──▶ 编译返回:
  │                           ├─ alias 解析(@/ → src/)
  │                           ├─ plugin-vue2 拆解 SFC:
  │                           │    ├─ <template> → render 函数
  │                           │    ├─ <script> → JS
  │                           │    ├─ <style> → 独立 CSS 子请求(仅当 SFC 含 <style> 时,如
  │                           │    │      login.vue?vue&type=style&...&lang.less;App.vue 无 style 块则无)
  │                           │    └─ render 函数 + script + HMR 包裹 → 返回 JS
  │                           └─ 返回转换后模块
  │
  │── GET /src/assets/svgIcons/index.js ──▶
  │                           ├─ import 'virtual:svg-icons'
  │                           ├─ svg-sprite 插件 resolveId:'virtual:svg-icons' → 返回同一标识(认领虚拟模块)
  │                           ├─ load 钩子:读 86 个 SVG → 拼 symbol →
  │                           │   生成「DOM 注入代码」模块(含 inline sourcemap)
  │                           └─ 返回
  │
  └── (并行)GET /api/xxx ──▶ 中间件栈 6:proxy → 转发后端 → 返回

HAR 实证 (本项目真实首屏记录):PC 端首屏 127 个请求 = 组件/工具模块逐步请求 + 预构建依赖 + 转换请求。其中 /node_modules/.vite/deps/ant-design-vue.js(约 2MB)与 antd.less(约 680KB)是最大的两个请求------前者是预构建产物、后者是 Less 编译后的 CSS 内容。

2.4 Less 编译链(CSS 请求内部)

任何 .less 文件(无论是 .vue 内的 <style lang="less"> 还是独立 .less 导入)在 dev 模式下按下述链路编译:

typescript 复制代码
.less 源文(如 App.vue?vue&type=style&lang.less)
  │
  ├─① additionalData 注入(css.preprocessorOptions.less.additionalData)
  │     在源文末尾追加:@import "src/styles/default.less"
  │     → 所有 Less 文件都拿到全部主题变量(含 @primary-color: @brand-color)
  │
  ├─② Less 编译器启动(less 4.x,javascriptEnabled: true,math: 'always')
  │
  ├─③ modifyVars 以最高优先级注入 @brand-color: <构建参数>
  │     → 覆盖 default.less 中的硬编码值
  │
  ├─④ pre-processor 插件:正则替换 @import '~xxx' → @import 'xxx'
  │     (递归处理所有 @import 文件,包括 node_modules 中的 vant/antd 样式)
  │
  ├─⑤ Less 编译输出 CSS → Vite 包装为 JS 模块(dev 模式 CSS 经 JS 注入 <style>)
  │
  └─⑥ 浏览器执行模块 → style 标签插入 → 样式生效

关键 :①③④ 三步都发生在 Less 编译器内部,而不是 Vite 转换层------这正是第三节 §1.3「~ 前缀必须用 Less 插件而非 Vite 插件」的机制原因。

2.5 HMR 热更新链

bash 复制代码
开发者保存 src/views/pc/user/login.vue
  │
  ▼ Vite 侦测到文件变化 → 重新编译受影响模块
  ▼ 通过 WebSocket 推送给浏览器(/@vite/client 建立的连接)
  ▼ @vite/client 收到更新 → plugin-vue2 hmr-runtime 应用:
     ├─ 组件 → 热替换 render 函数(不刷新页面)
     ├─ 样式 → 直接替换 <style> 标签
     └─ 无法热替换的模块(如 store 逻辑)→ 整页 reload

2.6 开发模式流转小结

bash 复制代码
浏览器请求
   → 请求分层(自定义中间件 / 代理 / Vite 转换管线)
   → alias 解析
   → 插件 transform 链(vue2 SFC / esbuild 语法 / HMR 注入)
   → (Less 文件另走 Less 编译链)
   → 模块返回 → 浏览器执行 → 级联请求下一层依赖

每个环节的失败模式与排查方法,见第八节(如 §1.5 预构建先于插件、§1.10 alias 匹配规则)。


三、生产构建:一次 npm run build 的完整流转

本章涉及的全部配置段:build(minify / commonjsOptions / manualChunks)、css、define、esbuild、base 及 plugins 全量

3.1 构建主流程与插件参与点

ini 复制代码
npm run build(vite build)
  │
  ▼ 配置解析(mode='production' → minify: esbuild、drop: ['console'] 生效)
  │
  ▼ 构建主流程(四阶段):
  │
  ├── ① 配置与启动:应用 rollupOptions / manualChunks;
  │        plugin-legacy 调整 build.target
  │
  ├── ② 模块图构建:逐个处理模块------定位(alias / 虚拟模块 / node_modules)
  │        → 读取内容(svg-sprite 在此生成虚拟模块)
  │        → 代码转换(vue2 SFC / esbuild 语法 / define 常量替换)
  │        → 分析依赖关系(tree-shaking 依据)
  │
  ├── ③ 产物生成(内存中):chunk 加工(plugin-legacy 在此产出 legacy 变体)
  │        → bundle 最终形态(visualizer 分析;legacy 生成 polyfills)
  │
  └── ④ 落盘与收尾:写入 dist/ → 收尾
           (serve-pdfjs-cmaps 复制 cmaps;viteCompression 生成 .gz)

3.2 本项目插件 × 钩子分工表

插件 config resolveId/load transform renderChunk generateBundle closeBundle
@vitejs/plugin-vue2 --- --- SFC 拆解编译 --- --- ---
@vitejs/plugin-legacy 改 build.target --- --- 生成 legacy chunk 变体 生成 polyfills-legacy + 注入 ---
svg-sprite --- 虚拟模块生成 --- --- --- ---
serve-pdfjs-cmaps --- --- --- --- --- 复制 cmaps → dist/
vite-plugin-compression --- --- --- --- --- 生成 .gz
rollup-plugin-visualizer --- --- --- --- 生成 stats.html ---

表中各插件的参与阶段均经实际构建与 node_modules 源码双重验证。

3.3 代码转换链:从源码到 chunk

arduino 复制代码
源码(ESM + Vue SFC)
  │
  ├─① define 文本替换(构建早期完成,标记为编译时常量)
  │     import.meta.env.APP_VERSION → "4.3.0"(字面量替换)
  │
  ├─② vue2 SFC 编译(template → render 函数)
  │
  ├─③ Rollup tree-shaking(基于 ESM 静态分析)
  │     ├─ 模块级:未引用的 export 被移除(lodash-es 只留 5 个方法)
  │     └─ 副作用保留:CSS 导入不参与 tree-shaking
  │
  ├─④ chunk 分割(manualChunks)
  │     ├── ant-design-vue chunk(PC 主库)
  │     ├── vant chunk(H5 主库)
  │     ├── vendor chunk(vue/vuex/vue-router/axios/moment)
  │     └── 业务 chunk(路由级动态 import 自动分割)
  │
  ├─⑤ esbuild minify(压缩率优先于速度的选项被设为 esbuild)
  │     └─ esbuild.drop: ['console'] 移除 console 调用
  │
  └─⑥ legacy 变体重建(见 3.4)

3.4 legacy 双构建(@vitejs/plugin-legacy)

bash 复制代码
正常构建(modern 产物)
  │
  ├─ 构建前:build.target 被 plugin-legacy 覆盖为现代浏览器集合
  │     ['es2020','edge79','firefox67','chrome64','safari12'](未传 modernTargets 时的默认)
  │
  ├─ 构建中:每个 chunk 同步产出 legacy 变体(Babel 降至 ES5 语法)
  │
  └─ 构建后:
        ├─ 生成 polyfills-legacy chunk(core-js 按需 + regenerator-runtime)
        ├─ 注入 SystemJS loader(legacy 浏览器没有原生 ESM)
        └─ 在 index.html 注入双脚本:
              <script type="module"> → modern 产物
              <script nomodule>     → legacy 产物加载逻辑

产物命名:
  assets/index-<hash>.js          (modern,.gz 预压缩)
  assets/index-legacy-<hash>.js   (legacy,nomodule 加载)
  assets/polyfills-legacy-<hash>.js

3.5 dist/ 产物结构全解

bash 复制代码
dist/
├── index.html                    ← transformIndexHtml 注入后:
│                                     module / nomodule 双脚本 + modulepreload/stylesheet
├── favicon.ico                   ← public/ 原样复制
├── js/                           ← public/js/ 原样复制(pdf.worker.min.js)
├── cmaps/                        ← closeBundle 从 node_modules/pdfjs-dist/cmaps 复制(169 文件)
├── stats.html                    ← 仅 --report 时(visualizer,位于 dist 根目录)
└── assets/
    ├── index-<hash>.js + .gz     ← 入口 chunk
    ├── ant-design-vue-<hash>.js  ← manualChunks 分块
    ├── vant-<hash>.js
    ├── vendor-<hash>.js
    ├── 业务 chunk(路由页面,现代 JS 约 190 个)
    ├── index-legacy-<hash>.js    ← legacy 变体
    ├── polyfills-legacy-<hash>.js
    └── *.css(antd / vant / 业务样式,从 JS 中提取,约 120 个)

hash 内容寻址:文件内容不变则 hash 不变 → 浏览器强缓存命中。这正是 manualChunks 拆分 vendor 的意义(第七节(三)§1.2)。


四、开发 vs 构建:同一件事的两种路径(对照总表)

事项 开发模式(serve) 生产构建(build)
入口 index.html → /src/main.js 模块瀑布 Rollup 从 index.html + main.js 构建模块图
模块加载 浏览器逐个请求 ESM(127/104 请求/首屏) 打包为 chunk(现代 JS≈190 个、CSS≈120 个,另有 legacy 变体与 .gz)
依赖 optimizeDeps 预构建 → .vite/deps/(不进 transform) Rollup 直接打包进 chunk(tree-shaking)
CJS/UMD esbuild 预构建转 ESM commonjsOptions.transformMixedEsModules 转换
Vue SFC plugin-vue2 按请求编译(+ HMR 包裹) plugin-vue2 构建期编译(无 HMR)
Less 每请求编译(additionalData → modifyVars → ~ 插件 → CSS 经 JS 注入) 一次性编译,CSS 提取为独立文件(+ 全量 minify)
define 编译时文本替换(相同) 编译时文本替换(相同)
虚拟模块 svg load 钩子生成(含 sourcemap,约 560KB) load 钩子生成(tree-shaking 后进 chunk)
压缩 无 esbuild minify + drop console + .gz 预压缩(closeBundle)
polyfill/ES5 无(按现代浏览器运行) legacy 双构建产出 nomodule 产物
cmaps configureServer 中间件实时读(HTTP 200 直接返回) closeBundle 复制到 dist/cmaps
产物 无落盘(内存 + 按需转换) dist/ 全量落盘(见 3.5)
诊断 @vite/client + HMR + 源码直读 stats.html(--report)+ sourcemap 关闭

附:与本篇互引的文档

主题 对应文档
每个配置字段的逐段解读 第六节:构建配置详解
各插件替代了 Webpack 的什么 第七节(一)(三):全家桶移除
ES5 降级的完整方案与决策 第七节(五):ES5 降级方案专题
流转环节的失败案例与排查 第八节:错误排查实录(§1.5 预构建时序 / §1.10 alias / §1.11 混合模块)
流转优化的实战(请求数/体积) 第九节:迁移收尾与优化专题
相关推荐
定栓1 小时前
Webpack迁到Vite第二节:JSX 到 Render 函数
vue.js·webpack·vite
Liu.7742 小时前
Vue3 中如何使用 GET 请求传递复杂参数
vue.js·vue3
定栓2 小时前
Webpack迁到Vite第一节:模块系统迁移:require → import
运维·前端·webpack
todoitbo2 小时前
本来只想给内网开个口子,Natapp 却多给了一道防护
前端·javascript·vue.js·内网穿透·网络防护·natapp
雪芽蓝域zzs3 小时前
JS 项目转 TS 项目(Vue3 + 若依这类后台项目)完整改造清单
前端·vue.js
定栓3 小时前
Webpack迁到Vite第四节:SVG 图标系统
前端·webpack·node.js
定栓3 小时前
Webpack迁到Vite第六节:构建配置详解(vite.config.js)
前端·javascript·webpack
鶴哥只手遮天16 小时前
Vue 3 核心源码深度剖析系列(v3.5.44)-第 1 章 项目全景:monorepo 架构与工程化体系
前端·vue.js·开源