Vite:HTML 驱动的开发服务器与 Rolldown 生产构建
核验日期:2026-07-08。示例版本来自
examples/build-tools/package.json:vite8.1.3。Vite 官方文档当前显示 v8.1.x,本文以 Vite 8/8.1 的 Rolldown 线为基准,同时说明 Vite 6/7 的 esbuild + Rollup 双引擎边界。
需要掌握的问题
| 主题 | 读者必须回答的问题 | 原理与边界 | 高频错误 | 代码或观察证据 | 来源 |
|---|---|---|---|---|---|
| Vite 双生命周期 | 开发服务器和生产构建为什么不是同一条路径? | 开发时从 HTML 请求进入 module graph,按需 transformRequest 后交给浏览器 ESM;生产时以 HTML entry 进入 Rolldown graph 输出 chunks/assets/manifest | 用 dev 的原生 ESM 请求数推断生产 bundle;只测 vite dev 不测 vite build | dev/prod Mermaid、共享 vite.config.mjs | Getting Started |
module graph 与 transformRequest | 浏览器请求一个模块时 Vite 做了什么? | Vite server 维护 module graph;每个模块请求触发 resolve、load、transform 和 import analysis,并缓存 transform 结果 | 把 Vite 当静态文件服务器,忽略 TS/CSS/env/import analysis 都被转换 | Plugin API 的 plugin container 钩子说明 | Plugin API |
| Plugin container | Vite 插件为什么像 Rollup/Rolldown,又不完全等同? | dev server 创建 plugin container 调用 Rolldown/Rollup 风格的 build hooks;dev 中不会调用完整输出钩子,也不会调用 moduleParsed | 在 dev 插件里依赖 generateBundle 或 moduleParsed | resolveId/load/transform dev 调用表 | Plugin API |
HMR 与 import.meta.hot | Vite 如何在不刷新页面时替换模块? | Vite 通过 HMR 图找受影响模块,客户端用 import.meta.hot.accept、dispose、invalidate 描述更新边界 | accept 写法不被静态分析识别;忘记 dispose 导致重复事件监听 | HMR API 最小代码与错误完整流程 | HMR API |
| env 与 secret boundary | import.meta.env 何时替换,哪些变量能进浏览器? | 内置常量和 VITE_ 前缀变量在 dev 可读、build 时静态替换;值进入 bundle 后不再是秘密 | 把数据库密码写成 VITE_DB_PASSWORD,或把字符串 env 当布尔值 | .env 示例和产物搜索 | Env Variables and Modes |
base 与部署路径 | 为什么本地打开正常,部署到子路径或 CDN 后 404? | base 决定 JS、CSS、asset、HTML 引用的公共路径;./ 适合嵌入式相对部署 | 忘记配置 /repo/、CDN origin 不一致、只测根路径 | 示例 base: './'、Network 404 定位表 | Public Base Path |
生产 build 与 manifest | 生产输出怎样被静态站点或后端模板消费? | vite build 以 <root>/index.html 为入口,Vite 8 用 Rolldown 输出优化后的 chunks/assets;build.manifest 生成 .vite/manifest.json | 服务端手写 hash 文件名、manifest path 拼错、旧 HTML 指向已删 asset | artifact test 读取 .vite/manifest.json 找 src/detail.ts | build.manifest |
| SSR 与 Environment API | SSR、custom runtime 和普通浏览器 build 的边界在哪里? | Vite 6 引入 Environment 概念;Environment API 让框架/运行时映射 client、ssr、worker 等环境,但仍处 RC/实验边界 | 在普通应用里直接依赖底层 Environment API;把 SSR build 当浏览器 build | Environment API 状态和 SSR manifest 边界 | Environment API |
| Vite 6/7 到 Vite 8 | Vite 8 为什么从 esbuild + Rollup 变成 Rolldown 统一引擎? | Vite 6/7 dev 和 build 使用两套转换/打包管线;Vite 8 用 Rolldown 减少双管线差异并提升性能 | 沿用“build 由 Rollup 驱动”的旧认知;插件只在 Vite 7 测过 | Vite 8 release、this.meta.rolldownVersion | Vite 8 release |
| Vite 8.1 bundled dev | 实验 bundled dev 解决什么,为什么不能默认启用? | Vite 8.1 引入 experimental bundled dev mode,目标是大应用减少模块请求并保持 HMR;它仍有插件兼容和功能覆盖限制 | 把实验模式当默认建议,或用它掩盖生产构建问题 | Vite 8.1 release 的实验性说明 | Vite 8.1 release |
| 严重问题与验收 | 怎样证明 Vite 产物能被浏览器加载,而不只是命令退出 0? | 检查 HTML、manifest、JS/CSS、async chunk、Source Map、base 和运行时可见副作用 | base 404、env leak、HMR dispose 不完整、dev 推断 prod、manifest paths 错误 | pnpm --filter @roadmap/build-tools-slice build:vite 和 artifact test | Building for Production |
内容边界说明:
| 分类 | 本文落点 |
|---|---|
| 编程语言与类型系统 | 解释 TS/JSX 只做转译、import.meta.env 静态替换和 ESM/dynamic import;不把 Vite 当类型检查器 |
| Browser APIs 与持久化能力 | 覆盖浏览器 ESM、HMR WebSocket、dynamic import、Source Map、HTTP/CDN/base 路径;不讨论 IndexedDB、Storage 权限模型 |
| UI 框架与元框架 | 说明框架插件、HMR 边界和 SSR/Environment API 接口;组件渲染、Diff、水合由框架专栏负责 |
| 构建工具与运行时 | 完整覆盖 HTML 入口、module graph、transform、插件、HMR、env、manifest、产物和 Vite 8 迁移 |
| 后端、数据与 Agent 技术 | 只覆盖环境变量泄露、SSR manifest、构建插件以开发者权限运行;数据库一致性和 Agent 评测不适用 |
Vite 的双路径:开发按请求,生产按入口
Vite 不是“Rollup 配置包装器”。它有两条生命周期:
- 开发路径:浏览器请求 HTML 和模块,Vite server 按需 transform,再把接近源码形态的 ESM 交给浏览器执行。
- 生产路径:
vite build从 HTML entry 收集依赖,用 Vite 8 的 Rolldown 生成优化后的 chunks、assets 和 manifest。
这两条路径共享插件、配置和源码,但不能互相替代验收。
| 问题 | 开发路径回答 | 生产路径回答 |
|---|---|---|
| 改一行代码后多快反馈? | module graph invalidation、transform cache、HMR 边界 | 不回答 |
| 用户下载哪些文件? | 不稳定,取决于浏览器请求源码模块 | chunks/assets/hash/manifest |
base 或 CDN 是否正确? | 只能粗略观察 | 必须用 build + preview/静态服务验证 |
| Tree Shaking 是否生效? | 通常不是最终结果 | 看生产 bundle 和 minify 后输出 |
| env 是否泄露? | dev 可快速观察 | build 后搜索产物才是最终证据 |
严重问题完整流程:
| 症状 | 根因 | 观察命令 | 修正 |
|---|---|---|---|
| dev 正常,部署后白屏 | dev server 以 root 服务源码,生产 asset path/base 不同 | pnpm --filter @roadmap/build-tools-slice build:vite 后静态预览 dist/vite | 配置 base,验证 build 输出的 HTML 和 Network |
| dev 请求很多就断言生产也很多 | 开发是 unbundled ESM,生产会打包分块 | 比较 dev Network 和 dist/vite/.vite/manifest.json | 用生产产物做部署和性能判断 |
| 类型错误没阻塞 Vite | Vite 对 TS 只做转译,不做类型证明 | 单独跑 tsc --noEmit | 把类型检查放在独立脚本或 CI |
开发路径:HTML 请求 → module graph → transformRequest → 浏览器 ESM → HMR
Vite 把 index.html 当源码入口,而不是放在 public/ 的静态壳。官方文档明确说明 HTML 是 module graph 的一部分,会解析 <script type="module" src="...">,也会处理 CSS 和资源 URL。
共享示例的开发入口在 examples/build-tools/src/index.html,生产配置把 root 指向 src/:
export default defineConfig({
root: fileURLToPath(new URL('../src/', import.meta.url)),
base: './',
build: {
outDir: fileURLToPath(new URL('../dist/vite/', import.meta.url)),
emptyOutDir: true,
sourcemap: true,
manifest: true,
minify: 'esbuild',
cssMinify: false,
target: 'es2022',
},
});开发时的关键过程:
| 阶段 | 输入 | Vite 处理 | 输出 |
|---|---|---|---|
| HTML 请求 | /index.html | 解析 HTML、注入 Vite client、记录 HTML 与 module graph 关系 | 浏览器可执行的 HTML |
| JS/TS 请求 | /src/main.ts | transformRequest 调插件 resolveId/load/transform,做 TS 转译和 import analysis | 浏览器可执行 ESM |
| CSS 请求或导入 | import './styles.css' | CSS transform、URL 处理、HMR 边界 | <style> 注入或 CSS 模块结果 |
| 动态导入 | import('./detail.ts') | 保留浏览器 dynamic import 能力,按需 transform 目标模块 | 点击后请求 detail 模块 |
| 文件变化 | changed file | module graph invalidation,找 HMR 边界 | 局部更新或 full reload |
调试入口:
pnpm --filter @roadmap/build-tools-slice exec vite --config configs/vite.config.mjs --debug如果只需要本仓库已验证的生产构建,运行:
pnpm --filter @roadmap/build-tools-slice build:vite预期结果不是“开发服务器启动”,而是写出 dist/vite/ 下的生产文件。
Plugin container:像 Rolldown/Rollup,但 dev 不跑输出阶段
Vite 插件兼容 Rollup/Rolldown 风格的很多 hooks。关键差异是:开发服务器按请求 transform,不会在每个请求里构建完整 bundle。因此 dev 中不会调用完整输出钩子,官方文档也说明 dev 下不调用 moduleParsed,因为 Vite 为了性能避免完整 AST parse。
| 钩子 | dev server | production build | 用途 |
|---|---|---|---|
options、buildStart | server 启动时调用 | build 启动时调用 | 初始化插件状态 |
resolveId | 每个相关模块请求调用 | 构建图解析时调用 | alias、虚拟模块、external-like 决策 |
load | 每个请求可能调用 | 构建读取模块时调用 | 虚拟模块、非标准资源 |
transform | 每个请求调用,接近 transformRequest 核心 | 每个模块构建转换 | TS/JSX/CSS/env/自定义转换 |
moduleParsed | dev 不调用 | build 中可用 | 依赖完整 AST 的分析 |
renderChunk/generateBundle/writeBundle | dev 不调用 | build 输出阶段调用 | manifest、HTML/asset 输出处理 |
handleHotUpdate | dev 专属 | build 不调用 | 自定义 HMR invalidation |
一个同时考虑 dev/build 的插件:
function buildFlagPlugin() {
return {
name: 'build-flag',
resolveId(id) {
if (id === 'virtual:build-flag') return '\0virtual:build-flag';
return null;
},
load(id) {
if (id === '\0virtual:build-flag') {
return `export const mode = ${JSON.stringify(process.env.NODE_ENV ?? 'development')}`;
}
return null;
},
transform(code, id) {
if (!id.endsWith('.ts')) return null;
return { code, map: null };
},
generateBundle(_, bundle) {
this.emitFile({
type: 'asset',
fileName: 'asset-count.json',
source: JSON.stringify({ count: Object.keys(bundle).length }),
});
},
};
}错误完整流程:
| 症状 | 根因 | 观察命令 | 修正 |
|---|---|---|---|
dev 下插件没生成 asset-count.json | generateBundle 是 build 输出钩子,dev 不调用 | 启动 dev 后搜索内存/磁盘输出 | dev 逻辑放 transform/configureServer,产物逻辑放 build hooks |
| dev 和 build 转换结果不同 | 插件根据 command、ssr、this.meta.rolldownVersion 分支不一致 | 打印插件 env,跑 dev/build fixture | 显式测试两条路径,不共用未经验证的状态 |
| 升 Vite 8 后插件边缘行为变化 | Vite 8 底层切到 Rolldown,兼容面很大但不是“无需回归” | 搜索 this.meta.rolldownVersion,跑插件集成测试 | 插件按 Vite 8/Rolldown 重新验收,避免依赖私有行为 |
HMR 与 import.meta.hot:更新边界必须显式清理副作用
Vite 的 HMR 建立在原生 ESM 和 module graph 上。框架插件通常替你写好边界;手写模块时,需要明确 accept、dispose 或 invalidate。
let timer = setInterval(() => {
console.log('tick');
}, 1000);
if (import.meta.hot) {
import.meta.hot.accept((nextModule) => {
console.log('module updated', nextModule);
});
import.meta.hot.dispose(() => {
clearInterval(timer);
});
}重要边界:
| API | 解决的问题 | 不能解决 |
|---|---|---|
import.meta.hot.accept | 声明当前模块或依赖可热更新 | 自动清理旧事件监听、旧定时器、旧全局状态 |
import.meta.hot.dispose | 在旧模块实例被替换前清理副作用 | 证明新模块一定能无损接管所有状态 |
import.meta.hot.data | 在模块实例之间保存少量状态 | 保存复杂应用状态或替代框架状态管理 |
import.meta.hot.invalidate | 当前模块判断无法安全热更,向上传播或刷新 | 掩盖错误边界;必须仍有 accept 才能继续监听 |
handleHotUpdate | 插件自定义受影响模块或发送自定义事件 | 生产构建行为 |
严重问题完整流程:
| 症状 | 根因 | 观察命令 | 修正 |
|---|---|---|---|
| 改一次代码,点击事件触发两次 | dispose 没移除旧监听 | 浏览器控制台打印 listener 次数 | 在 dispose 里移除事件、定时器、订阅 |
accept 写了但仍 full reload | import.meta.hot.accept( 形态没被静态分析识别,或更新越过边界 | 看终端和浏览器 HMR 日志 | 保持可静态分析写法,必要时接受依赖更新 |
| HMR 正常但生产报错 | HMR 路径没经过 minify/chunk/base/manifest | 同时跑 vite build 和静态预览 | 不用 HMR 成功替代生产验收 |
env、import.meta.env 和 secret boundary
Vite 暴露 import.meta.env。内置常量如 MODE、DEV、PROD、SSR 和 BASE_URL 总是可用。自定义变量默认只有 VITE_ 前缀会进入客户端源码。
VITE_PUBLIC_ANALYTICS_ID=demo
DB_PASSWORD=do-not-shipconsole.log(import.meta.env.VITE_PUBLIC_ANALYTICS_ID); // "demo"
console.log(import.meta.env.DB_PASSWORD); // undefined安全边界是:进入浏览器 bundle 的值不是秘密。Vite build 会静态替换这些表达式,值会永久写进 JS 文件。
| 变量 | 浏览器可见性 | 风险 | 修正 |
|---|---|---|---|
VITE_API_BASE | 可见 | 可以公开的 API 地址通常可接受 | 明确是 public config |
VITE_DB_PASSWORD | 可见 | 严重泄露 | 改为服务端 secret,不进前端构建 |
DB_PASSWORD | 默认不可见 | 插件或自定义 define 仍可能泄露 | 禁止把 secret 写入 define 或日志 |
import.meta.env.PROD | build 时静态替换 | 只适合分支裁剪 | 不承载业务权限判断 |
错误完整流程:
| 症状 | 根因 | 观察命令 | 修正 |
|---|---|---|---|
| 生产 JS 里出现密钥 | 变量用了 VITE_ 前缀,或 define 直接注入 secret | `rg "password | secret |
if (import.meta.env.VITE_FLAG) 永远为真 | env 值都是字符串,"false" 也 truthy | 打印 build 后分支或单元测试 | 显式比较 === "true" |
| SSR 和 client env 混用 | import.meta.env.SSR 分支没隔离 | 跑 client build 和 SSR build | 分清 server-only code,不在 client import server secret |
base、静态资源、hash、CDN 和 manifest
Vite 的 base 决定资源公共路径。默认是 /,部署到子路径时需要改成 /repo/;要支持任意嵌入路径时可以用 ./。共享示例使用:
export default defineConfig({
root: fileURLToPath(new URL('../src/', import.meta.url)),
base: './',
build: {
outDir: fileURLToPath(new URL('../dist/vite/', import.meta.url)),
manifest: true,
},
});build.manifest: true 的预期输出路径是 .vite/manifest.json。它映射非 hash 源文件到 hash 产物,后端模板或测试可用它找正确资源。
严重问题完整流程:
| 症状 | 根因 | 观察命令 | 修正 |
|---|---|---|---|
部署到 /docs/ 后 JS/CSS 404 | base 仍是 / 或 HTML 被错误移动 | DevTools Network 看请求是否从站点根发出 | 设置 base: "/docs/" 或 ./,并用真实子路径预览 |
| 点击后 async chunk 404 | initial assets 路径正确,但 dynamic import base 错 | 点击触发 detail,看 async JS URL | 同时验证初始和动态 chunk,不只看首屏 |
| 后端模板加载旧 hash | 没读 manifest,手写文件名或缓存旧 manifest | 对照 .vite/manifest.json 和 HTML | 每次发布同步 manifest,HTML 短缓存,assets 长缓存 |
| CDN 上 CSS 200 但样式错 | CSS/asset 路径被 CDN rewrite 或 MIME 错 | Network Response Headers 和 CSS sourceMappingURL | CDN 保留路径结构和 MIME;必要时用绝对 base |
manifest 不是浏览器运行时必须文件;它是发布系统和服务端集成的契约。普通静态站点可只用 Vite 生成的 index.html,但后端模板需要 manifest 才能拿到 hash 文件名。
生产 build:HTML entry → Rolldown graph → chunks/assets → manifest
运行命令:
pnpm --filter @roadmap/build-tools-slice build:viteVite 8 的 production build 使用 Rolldown。共享示例的输入是 HTML:
examples/build-tools/src/index.html
examples/build-tools/src/main.ts
examples/build-tools/src/detail.ts
examples/build-tools/src/styles.css预期结果:
examples/build-tools/dist/vite/index.html
examples/build-tools/dist/vite/.vite/manifest.json
examples/build-tools/dist/vite/assets/*.js
examples/build-tools/dist/vite/assets/*.css
examples/build-tools/dist/vite/assets/*.js.map
examples/build-tools/dist/vite/assets/*.css.map验证观察:
find examples/build-tools/dist/vite -type f | sort
node --test --test-name-pattern vite examples/build-tools/test/artifacts.test.mjs预期输出要点:
| 观察点 | 预期结果 |
|---|---|
index.html | 通过相对 script type="module" 加载 initial JS,通过 stylesheet 加载 CSS |
.vite/manifest.json | 包含 src/detail.ts,且该 entry 是 isDynamicEntry: true |
| JS/CSS | 有 hash assets 和外部 Source Map |
| dynamic JS | 不被 HTML 直接加载,而是由运行时 dynamic import 触发 |
| bundled JS | 包含 Babel 和 preserved,不包含 SHOULD_NOT_REACH_BUNDLE |
如果 vite build 退出 0,但 .vite/manifest.json 中没有 src/detail.ts dynamic entry,说明异步边界已经被错误合并、元数据丢失或测试输入不再覆盖动态导入。
SSR 与 Environment API:框架边界,不是普通应用的默认入口
Vite 支持 SSR build,也在 Vite 6 之后 formalize 了 Environment 概念。Environment API 允许框架和 runtime provider 创建 client、ssr、worker、edge 等不同环境,让 dev 更接近生产运行时。官方当前仍把 Environment API 描述为 Release Candidate,并说明部分具体 API 仍是 experimental。
边界表:
| 能力 | 适用对象 | 产物/运行时 | 不适合 |
|---|---|---|---|
| 普通 client build | SPA、静态站点、前端应用 | 浏览器 JS/CSS/assets | 服务端渲染入口 |
| SSR build | 框架或自研 SSR 服务 | server bundle + client manifest/ssr manifest | 直接部署到纯静态 CDN |
| Environment API | 框架作者、runtime provider、复杂插件 | 多环境 dev/build 抽象 | 普通业务应用随手接入底层 API |
vite preview | 本地预览生产 build | 静态预览服务器 | 生产服务器 |
错误完整流程:
| 症状 | 根因 | 观察命令 | 修正 |
|---|---|---|---|
SSR 里访问 window 崩溃 | server environment 执行了 browser-only code | import.meta.env.SSR 分支和 SSR build 日志 | 拆分 client-only import,或动态导入到客户端 |
| 普通应用直接使用 Environment API 后升级困难 | 依赖 RC/experimental 低层接口 | 检查 Vite 版本和 API changelog | 优先使用框架集成;低层 API 留给框架/插件层 |
| SSR manifest 缺 CSS | 只构建 server,没有合并 client assets | 检查 build.ssrManifest 和 client manifest | 按框架文档同时构建 client/server 并合并 |
Vite 6/7 到 Vite 8:重大变化是 Rolldown 统一引擎
Vite 6/7 的常见认知是:dev 侧用 esbuild 做依赖预构建和快速转译,build 侧用 Rollup 输出生产 bundle。这套双引擎长期有效,但也带来两套插件系统和边缘语义对齐成本。
Vite 8 的重大变化是使用 Rolldown 作为底层构建引擎,并让 Vite 插件生态围绕同一方向收敛。官方 Vite 8 发布说明强调 Rolldown 的目标包括性能、兼容 Rollup/Vite 插件 API,以及统一引擎带来的高级能力。
| 维度 | Vite 6/7 | Vite 8 |
|---|---|---|
| 生产构建底层 | Rollup | Rolldown |
| 转换/打包心智模型 | esbuild + Rollup 双管线 | Rolldown 统一管线为核心 |
| 插件兼容 | Rollup 风格 + Vite 特定 hooks | 继续兼容大多数 Vite 插件,但需回归边缘行为 |
| 检测方式 | 没有 Rolldown meta | 插件可读 this.meta.rolldownVersion |
| 性能结论 | 取决于项目、插件和缓存 | 官方报告多家公司 build time 降低,但本文不把它外推到所有项目 |
迁移严重问题完整流程:
| 症状 | 根因 | 观察命令 | 修正 |
|---|---|---|---|
| Vite 7 插件在 Vite 8 build 输出不同 | 插件依赖 Rollup 私有细节或输出 hook 边缘行为 | 跑插件 fixture,检查 this.meta.rolldownVersion | 用公开 API,针对 Vite 8/Rolldown 做回归 |
| 团队文档仍写“Vite build 等于 Rollup build” | 认知停留在 Vite 7 | 查看 vite --version 和官方 release | 更新构建边界说明,避免排查走错文档 |
| 性能升级不明显 | 项目瓶颈在框架插件、类型检查、CSS 或 I/O | benchmark 分开统计 transform/build/typecheck | 拆分瓶颈,不把 Rolldown 当万能加速器 |
Vite 8.1 experimental bundled dev:只作为大应用实验选项
Vite 8.1 引入 experimental bundled dev mode。官方说明它以前叫 Full Bundle Mode,目标是改善超大应用因模块数量过多导致的启动和 full reload 成本,同时保持 HMR 高效。官方也给出初步测试和真实应用反馈。
但它是实验能力,不是默认建议。原因很直接:
| 风险 | 说明 | 决策 |
|---|---|---|
| 插件兼容 | dev 从 unbundled 转为 bundled,会改变插件观察到的请求模型和边界 | 只在插件兼容测试通过后试用 |
| 功能覆盖 | 实验模式可能尚未覆盖所有 Vite dev 特性 | 不作为基础教程默认路径 |
| 问题定位 | bundled dev 更接近生产,但也更难直接观察单源码模块请求 | 保留普通 dev 作为对照结果 |
| 性能外推 | 官方数字来自特定大应用和测试条件 | 本地 benchmark 要写清源码规模、机器、插件和缓存状态 |
适用判断:
| 场景 | 是否考虑 |
|---|---|
| 几百个模块的小应用 | 通常不需要,默认 dev 更简单 |
| 上万模块、首屏请求和 full reload 明显拖慢 | 可开实验分支测试 bundled dev |
| 插件大量依赖 dev 请求粒度 | 先不启用,除非插件作者明确支持 |
| 生产构建失败 | bundled dev 不是修复手段,先修 build |
严重问题完整流程:
| 症状 | 根因 | 观察命令 | 修正 |
|---|---|---|---|
| bundled dev 下某插件不生效 | 插件依赖 unbundled dev 的模块请求粒度 | 对比普通 dev 和 bundled dev 的插件日志 | 保持默认 dev,向插件维护者反馈或等待覆盖 |
| 启用后 HMR 边界变粗 | 打包后的模块归组改变更新传播 | 浏览器 HMR 日志和状态保留情况 | 缩小实验范围,保留 full reload 回退 |
| 团队把实验模式写进模板默认值 | 未验证插件、框架和调试成本 | 新项目 smoke test + 插件兼容测试 | 默认仍用普通 dev,仅大项目 opt-in |
共享示例:Vite 构建 HTML 应用入口
运行命令:
pnpm --filter @roadmap/build-tools-slice build:vite输入:
examples/build-tools/src/index.html
examples/build-tools/src/main.ts
examples/build-tools/src/catalog.ts
examples/build-tools/src/detail.ts
examples/build-tools/src/side-effect.ts
examples/build-tools/src/styles.css预期结果:
examples/build-tools/dist/vite/index.html
examples/build-tools/dist/vite/.vite/manifest.json
examples/build-tools/dist/vite/assets/*.js
examples/build-tools/dist/vite/assets/*.css
examples/build-tools/dist/vite/assets/*.js.map
examples/build-tools/dist/vite/assets/*.css.map本仓库还补了一个 css-source-maps build 插件,因为当前示例需要外部 CSS map 作为统一 artifact 证据:
function cssSourceMaps() {
return {
name: 'css-source-maps',
apply: 'build',
generateBundle(_options, bundle) {
for (const asset of Object.values(bundle)) {
if (asset.type !== 'asset' || !asset.fileName.endsWith('.css')) continue;
this.emitFile({
type: 'asset',
fileName: `${asset.fileName}.map`,
source: JSON.stringify({
version: 3,
sources: ['../../../src/styles.css'],
names: [],
mappings: '...',
}),
});
}
},
};
}这段插件只服务示例验收,不代表所有项目都要手写 CSS map 插件。真实项目按错误平台、源码保密和浏览器调试策略选择 external、hidden 或不发布 Source Map。
验证观察:
| 观察点 | 预期结果 |
|---|---|
index.html | 不包含工作区绝对路径,不直接加载 async detail chunk |
.vite/manifest.json | src/detail.ts 对应 dynamic entry |
| JS source map | 主 chunk 指回 main.ts,async chunk 指回 detail.ts |
| CSS source map | sources 是相对路径并指向真实 styles.css |
| bundled JS | 包含 Babel 和 preserved,不包含 SHOULD_NOT_REACH_BUNDLE |
严重问题总表:不要用“能启动”替代验收
| 严重问题 | 表现 | 根因 | 最小观察 | 修正 |
|---|---|---|---|---|
| base 404 | 首屏或点击后 JS/CSS 404 | base 与部署子路径/CDN 不一致 | DevTools Network、构建后静态预览 | 设置正确 base,同时验证 initial 和 async chunk |
| env leak | 产物包含密码、token 或内部 URL | 使用 VITE_ 或 define 注入 secret | `rg "secret | password |
| HMR dispose 不完整 | 更新后重复事件、重复定时器、状态污染 | 旧模块副作用未清理 | 控制台计数、HMR 日志 | 在 dispose 清理监听、订阅、timer |
| 用 dev 推断 prod | dev 正常但 build 失败或白屏 | dev 和 build 路径不同 | 必跑 vite build 和静态预览 | 生产验收以 build 产物为准 |
| manifest paths 错误 | 后端模板加载不存在 hash 文件 | manifest 路径拼接错或旧 manifest | 对照 .vite/manifest.json 与 dist 文件 | 发布同步 manifest,模板只读 manifest |
| Source Map 泄露 | .map 暴露源码和本机路径 | 发布 external map 无过滤 | `rg "$PWD | file:" dist/vite` |