phase 5 part 2: vite 8, webpack 5, fe-testing-perf topics with verified examples
This commit is contained in:
1 parent
7b5cae7d24
commit
9eeb10aede
38 files changed
+1823
-37
No files matched your search
@@ -0,0 +1,104 @@
|
||||
---
|
||||
id: frontend.vite.dev-build-config
|
||||
title: Vite 8:开发服务器原理、Rolldown 构建、环境变量、代理(含 SSE)、插件和拆包
|
||||
type: lesson
|
||||
domain: frontend
|
||||
stack: [vite]
|
||||
tags: [Vite, Rolldown, ESM, HMR, 环境变量, 代理, 插件, 虚拟模块, 拆包]
|
||||
level: intermediate
|
||||
prerequisites: [frontend.typescript.type-system]
|
||||
est_hours: 2.5
|
||||
related: [frontend.webpack.build-optimization, frontend.vite.scenario.webpack-to-vite, frontend.vite.interview]
|
||||
week: [7]
|
||||
order: 1
|
||||
---
|
||||
|
||||
# Vite 8:开发服务器原理、Rolldown 构建、环境变量、代理(含 SSE)、插件和拆包
|
||||
|
||||
> 基于 Vite 8.3 编写并实际构建验证。Vite 8 的生产构建改为基于 Rust 编写的 **Rolldown**(Vite 7 及以前:开发时用 esbuild 预构建依赖,生产用 Rollup 打包);`build.rollupOptions` 改名为 `build.rolldownOptions`(旧名仍可用但已标记弃用),拆包推荐 `output.codeSplitting`。以官方文档为准:https://vite.dev/guide/ 、https://rolldown.rs/ 。
|
||||
|
||||
## 解释说明
|
||||
|
||||
### 为什么开发时快
|
||||
|
||||
- **开发**:不打包。浏览器原生支持 ES 模块(`<script type="module">`),请求哪个文件,Vite 就即时转换哪个文件(TS → JS、`.vue` → JS)。第三方依赖会预构建(把 CommonJS 转成 ESM、把很多小文件合成一个),并强缓存。
|
||||
- **热更新(HMR)**:文件修改后只让对应模块失效并重新请求,速度与项目大小基本无关。
|
||||
- **生产构建**:仍然要打包(减少请求数、tree shaking、压缩、拆包)。
|
||||
|
||||
### 常用配置
|
||||
|
||||
| 需求 | 配置 |
|
||||
| --- | --- |
|
||||
| 路径别名 | `resolve.alias` |
|
||||
| 环境变量 | `.env`、`.env.[mode]`;浏览器代码只能读取 `VITE_` 前缀的变量(`import.meta.env.VITE_XXX`);配置文件中用 `loadEnv` |
|
||||
| 开发代理 | `server.proxy`,解决开发时的跨域,支持 `rewrite` |
|
||||
| 插件 | Rollup / Rolldown 兼容的插件钩子(`resolveId`、`load`、`transform`)+ Vite 专有钩子(`configureServer`、`transformIndexHtml`、`handleHotUpdate`) |
|
||||
| 拆包 | Vite 8:`build.rolldownOptions.output.codeSplitting.groups`;动态 `import()` 自动拆包 |
|
||||
| 兼容旧浏览器 | `@vitejs/plugin-legacy` |
|
||||
|
||||
### Monorepo
|
||||
|
||||
pnpm workspace + Vite 是常见组合:多个应用共用组件库和工具包,`workspace:*` 引用本地包。
|
||||
|
||||
## 案例
|
||||
|
||||
AI 客服前端项目:开发时 `/api` 代理到本地 FastAPI,`/stream` 代理 SSE 流式接口;构建时注入版本号(便于排查线上问题对应哪个版本);Vue 运行时单独拆包,长期缓存。
|
||||
|
||||
## 代码示例
|
||||
|
||||
完整项目:[examples/vite-config](examples/vite-config/)。已验证:`tsc` 类型检查通过;`vite build` 输出独立的 `vendor-vue` 和按需加载的 `heavy` chunk;开发服务器代理普通接口(`/api/hello` → 后端 `/hello`)和 SSE(每 0.5 秒一条,逐条到达,没有被缓冲)。
|
||||
|
||||
自定义插件——虚拟模块([vite.config.ts](examples/vite-config/vite.config.ts)):
|
||||
|
||||
```ts
|
||||
function buildInfo(version: string): Plugin {
|
||||
const id = "virtual:build-info";
|
||||
const resolved = "\0" + id;
|
||||
return {
|
||||
name: "build-info",
|
||||
resolveId(source) { return source === id ? resolved : null; },
|
||||
load(moduleId) {
|
||||
if (moduleId !== resolved) return null;
|
||||
return `export default ${JSON.stringify({ version, builtAt: new Date().toISOString() })};`;
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
代理和拆包:
|
||||
|
||||
```ts
|
||||
server: {
|
||||
proxy: {
|
||||
"/api": { target: apiTarget, changeOrigin: true, rewrite: (p) => p.replace(/^\/api/, "") },
|
||||
"/stream": { target: apiTarget, changeOrigin: true },
|
||||
},
|
||||
},
|
||||
build: {
|
||||
rolldownOptions: {
|
||||
output: { codeSplitting: { groups: [{ name: "vendor-vue", test: /node_modules[\\/]@?vue/ }] } },
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
构建输出:
|
||||
|
||||
```text
|
||||
dist/assets/heavy-xxxx.js 0.08 kB
|
||||
dist/assets/index-xxxx.js 2.54 kB
|
||||
dist/assets/vendor-vue-xxxx.js 60.80 kB
|
||||
```
|
||||
|
||||
## 易错点
|
||||
|
||||
- 把密钥放到 `VITE_` 前缀的变量中:会被打包进浏览器代码,任何人都能看到。
|
||||
- 以为开发代理在生产环境也生效:生产环境需要 Nginx 或网关转发。
|
||||
- 开发正常、构建后报错:开发不打包,CommonJS 依赖、动态路径、大小写敏感的路径在构建时才暴露问题,提交前要跑一次 `vite build`。
|
||||
- 照搬旧文章的 `rollupOptions.output.manualChunks`:Vite 8 中仍可用但已弃用,新项目使用 `codeSplitting`。
|
||||
- 拆包拆得太碎:请求数增加,反而变慢。
|
||||
|
||||
## 面试常问
|
||||
|
||||
- Vite 为什么比 webpack 开发时快?生产构建为什么还要打包?
|
||||
- Vite 的环境变量是如何区分浏览器可见和不可见的?
|
||||
- 写过 Vite 插件吗?说说钩子。
|
||||
@@ -0,0 +1,5 @@
|
||||
# 复制为 .env 后使用(.env 已在 .gitignore 中,避免误提交)
|
||||
# 以 VITE_ 开头的变量会被打包进浏览器代码,绝对不要放密钥
|
||||
VITE_APP_TITLE=Vite 配置示例
|
||||
# 不带 VITE_ 前缀的变量只在 vite.config.ts 中可用
|
||||
API_TARGET=http://127.0.0.1:8000
|
||||
@@ -0,0 +1,11 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Vite 配置示例</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "vite-config-demo",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.0",
|
||||
"typescript": "^5.9.0",
|
||||
"vite": "^8.3.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/// <reference types="vite/client" />
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_APP_TITLE: string;
|
||||
}
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
declare module "virtual:build-info" {
|
||||
const info: { version: string; builtAt: string };
|
||||
export default info;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// 不自动运行:被 main.ts 动态 import,构建后会成为单独的 chunk
|
||||
export function summary(): string {
|
||||
return "这个模块在点击按钮时才加载";
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// 不自动运行:浏览器入口,由 vite 构建
|
||||
import { createApp, h } from "vue";
|
||||
import info from "virtual:build-info";
|
||||
|
||||
const App = {
|
||||
setup() {
|
||||
return () =>
|
||||
h("main", [
|
||||
h("h1", import.meta.env.VITE_APP_TITLE),
|
||||
h("p", `版本 ${info.version},构建于 ${info.builtAt},模式 ${import.meta.env.MODE}`),
|
||||
h("button", { onClick: () => import("./heavy").then((m) => alert(m.summary())) }, "按需加载模块"),
|
||||
]);
|
||||
},
|
||||
};
|
||||
createApp(App).mount("#app");
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"skipLibCheck": true,
|
||||
"types": ["vite/client", "node"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// 不自动运行:需要先 npm install,然后 npm run dev / npm run build
|
||||
// Vite 8 配置示例:环境变量、开发代理(含 SSE)、自定义插件(虚拟模块)、拆包。以官方文档为准:https://vite.dev/config/
|
||||
import { defineConfig, loadEnv, type Plugin } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
|
||||
// 自定义插件:提供虚拟模块 virtual:build-info,在代码中拿到构建时间和版本
|
||||
function buildInfo(version: string): Plugin {
|
||||
const id = "virtual:build-info";
|
||||
const resolved = "\0" + id; // \0 前缀:约定表示虚拟模块,其他插件不要处理
|
||||
return {
|
||||
name: "build-info",
|
||||
resolveId(source) {
|
||||
return source === id ? resolved : null;
|
||||
},
|
||||
load(moduleId) {
|
||||
if (moduleId !== resolved) return null;
|
||||
return `export default ${JSON.stringify({ version, builtAt: new Date().toISOString() })};`;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
// 第三个参数 "" 表示加载全部变量(包括没有 VITE_ 前缀的),只在配置文件里用,不会暴露给浏览器
|
||||
const env = loadEnv(mode, process.cwd(), "");
|
||||
const apiTarget = env.API_TARGET ?? "http://127.0.0.1:8000";
|
||||
|
||||
return {
|
||||
plugins: [vue(), buildInfo(env.npm_package_version ?? "dev")],
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
// 普通接口:/api/xxx → 后端 /xxx
|
||||
"/api": {
|
||||
target: apiTarget,
|
||||
changeOrigin: true,
|
||||
rewrite: (p) => p.replace(/^\/api/, ""),
|
||||
},
|
||||
// SSE 流式接口:开发代理默认不会缓冲,但要确保后端返回 text/event-stream 并及时 flush
|
||||
"/stream": { target: apiTarget, changeOrigin: true },
|
||||
},
|
||||
},
|
||||
build: {
|
||||
sourcemap: mode !== "production",
|
||||
rolldownOptions: {
|
||||
output: {
|
||||
// Vite 8 基于 Rolldown:拆包用 codeSplitting.groups(Rollup 的 manualChunks 已不推荐)
|
||||
codeSplitting: {
|
||||
groups: [{ name: "vendor-vue", test: /node_modules[\\/]@?vue/ }],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -4,12 +4,37 @@ title: Vite
|
||||
type: index
|
||||
domain: frontend
|
||||
stack: [vite]
|
||||
tags: []
|
||||
tags: [Vite, Rolldown, 构建工具]
|
||||
week: [7]
|
||||
---
|
||||
|
||||
# Vite
|
||||
|
||||
开发服务器原理、构建、配置、插件、环境变量。
|
||||
开发服务器原理、Vite 8(Rolldown)构建、环境变量、代理、插件和拆包。你已有多年前端经验,这里重点补“原理能讲清楚”和“迁移能落地”。
|
||||
|
||||
> 本专题建设中:课程、企业场景、面试题和示例代码会陆续补充。
|
||||
## 学习路线
|
||||
|
||||
| 顺序 | 课程 | 级别 | 预计 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | [Vite 8:开发服务器原理、Rolldown 构建、环境变量、代理(含 SSE)、插件和拆包](01-dev-build-config.md) | 进阶 | 2.5 小时 |
|
||||
|
||||
## 企业场景
|
||||
|
||||
- [老项目从 webpack 迁移到 Vite:评估、步骤、风险和回滚](scenarios/webpack-to-vite.md)
|
||||
|
||||
## 面试题
|
||||
|
||||
- [Vite 面试题](interview.md)
|
||||
|
||||
## 示例代码
|
||||
|
||||
- [examples/vite-config](examples/vite-config/):虚拟模块插件、`loadEnv`、`/api` 和 SSE 代理、`codeSplitting` 拆包(类型检查、构建、开发代理均已验证)
|
||||
|
||||
## 在 16 周计划中
|
||||
|
||||
- [第 7 周:前端工程化查漏](../../../plan/week07-frontend.md)
|
||||
|
||||
## 官方资料
|
||||
|
||||
- https://vite.dev/guide/
|
||||
- https://rolldown.rs/
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
id: frontend.vite.interview
|
||||
title: Vite 面试题
|
||||
type: interview
|
||||
domain: frontend
|
||||
stack: [vite]
|
||||
tags: [Vite, Rolldown, HMR, 插件, 面试]
|
||||
level: intermediate
|
||||
related: [frontend.vite.dev-build-config, frontend.webpack.interview]
|
||||
week: [7, 16]
|
||||
---
|
||||
|
||||
# Vite 面试题
|
||||
|
||||
## 基础
|
||||
|
||||
### 【基础】Vite 为什么开发时启动快?
|
||||
|
||||
**参考答案**:开发时不打包,利用浏览器原生 ES 模块按需请求、按需转换;第三方依赖预构建并强缓存;热更新只更新修改的模块,与项目规模基本无关。
|
||||
|
||||
**追问**:预构建做了什么?→ 把 CommonJS / UMD 依赖转成 ESM,把有很多内部模块的依赖合并成少量文件,减少浏览器请求数。
|
||||
|
||||
### 【基础】既然浏览器支持 ESM,生产环境为什么还要打包?
|
||||
|
||||
**参考答案**:未打包时模块请求多、瀑布式加载慢;打包可以做 tree shaking、压缩、拆包、资源哈希命名以便长期缓存、兼容处理。
|
||||
|
||||
**追问**:Vite 8 生产构建用什么?→ Rolldown(Rust 实现、兼容 Rollup 插件接口);Vite 7 及以前是 Rollup。
|
||||
|
||||
### 【基础】Vite 中的环境变量怎么使用?
|
||||
|
||||
**参考答案**:`.env` / `.env.[mode]` 文件;浏览器代码通过 `import.meta.env` 读取,只有 `VITE_` 前缀的变量会暴露;配置文件中用 `loadEnv` 读取全部变量。
|
||||
|
||||
**追问**:密钥可以放进 `VITE_` 变量吗?→ 不可以,会被打包进前端代码。
|
||||
|
||||
## 进阶
|
||||
|
||||
### 【进阶】Vite 插件有哪些常用钩子?
|
||||
|
||||
**参考答案**:通用钩子(与 Rollup 兼容):`resolveId`、`load`、`transform`、`buildStart` / `buildEnd`;Vite 专有:`config`、`configResolved`、`configureServer`(给开发服务器加中间件)、`transformIndexHtml`、`handleHotUpdate`。可以用 `enforce` 控制顺序、`apply` 控制只在开发或构建时生效。
|
||||
|
||||
**追问**:虚拟模块怎么实现?→ `resolveId` 识别特定 id 返回带 `\0` 前缀的内部 id,`load` 返回模块源码。
|
||||
|
||||
### 【进阶】Vite 的 HMR 是如何工作的?
|
||||
|
||||
**参考答案**:开发服务器监听文件变化,通过 WebSocket 通知浏览器;客户端根据模块依赖图找到接受更新的边界模块(例如 Vue 组件),重新请求该模块并替换,组件状态尽量保留;找不到边界时刷新整页。
|
||||
|
||||
**追问**:为什么有时候修改后整页刷新?→ 修改的模块及其上层没有模块接受热更新(例如修改了入口文件或纯工具模块的上层没有 HMR 边界)。
|
||||
|
||||
### 【进阶】怎么做拆包?
|
||||
|
||||
**参考答案**:动态 `import()` 自动生成独立 chunk(路由懒加载);第三方库按变化频率分组(Vite 8 用 `codeSplitting.groups`),利于长期缓存;避免拆得过碎。
|
||||
|
||||
**追问**:如何分析产物体积?→ 使用打包分析插件生成可视化报告,找大依赖和重复依赖。
|
||||
|
||||
## 场景题
|
||||
|
||||
### 【场景题】老项目从 webpack 迁移到 Vite,你怎么推进?
|
||||
|
||||
**参考答案**:盘点 webpack 专有用法 → 两套配置并存 → 逐项替换(`require.context`、环境变量、自定义 loader)→ 冒烟测试和产物对比 → 灰度上线并保留回滚。详见[迁移场景](scenarios/webpack-to-vite.md)。
|
||||
|
||||
**追问**:迁移遇到的最大问题?→ 常见的是 CommonJS 依赖和隐式的 Node 全局变量,以及开发模式正常、生产构建报错。
|
||||
|
||||
### 【场景题】开发时用 Vite 代理调用 AI 流式接口,前端收到的内容是一次性出现的,怎么排查?
|
||||
|
||||
**参考答案**:先用 curl 直连后端确认后端是否真的逐条发送和 flush;再经过代理用 `curl -N` 验证;检查响应头是否是 `text/event-stream`、是否被压缩中间件缓冲;最后检查前端是否用流式方式读取(`fetch` 的 `body.getReader()` 或 `EventSource`),而不是 `await res.text()`。
|
||||
|
||||
**追问**:生产环境同样问题?→ 检查 Nginx 的 `proxy_buffering` 和 `X-Accel-Buffering` 响应头、CDN / 网关是否缓冲。
|
||||
@@ -0,0 +1,77 @@
|
||||
---
|
||||
id: frontend.vite.scenario.webpack-to-vite
|
||||
title: 老项目从 webpack 迁移到 Vite:评估、步骤、风险和回滚
|
||||
type: scenario
|
||||
domain: frontend
|
||||
stack: [vite, webpack]
|
||||
tags: [迁移, 构建工具, CommonJS, 环境变量, 灰度]
|
||||
level: intermediate
|
||||
prerequisites: [frontend.vite.dev-build-config, frontend.webpack.build-optimization]
|
||||
est_hours: 2
|
||||
related: [frontend.vite.interview, frontend.webpack.interview]
|
||||
week: [7]
|
||||
---
|
||||
|
||||
# 老项目从 webpack 迁移到 Vite:评估、步骤、风险和回滚
|
||||
|
||||
## 业务背景与问题
|
||||
|
||||
一个 5 年的 Vue 2 → Vue 3 后台项目(几百个页面)仍用 webpack 4 构建:开发启动要几分钟,改一行代码热更新要十几秒,团队抱怨效率低;依赖老旧,有安全漏洞告警。
|
||||
|
||||
## 约束
|
||||
|
||||
- 不能停止业务需求开发,迁移要渐进。
|
||||
- 有自定义的 webpack loader(处理内部 SVG 图标)和 `process.env.XXX` 的大量使用。
|
||||
- 线上不能出问题,要可以回滚。
|
||||
|
||||
## 方案设计
|
||||
|
||||
1. **评估**:列出 webpack 配置中所有的 loader / plugin,逐个找 Vite 对应方案(多数有现成插件);扫描代码中的 `require()`、`require.context`、`process.env`、`module.hot`。
|
||||
2. **并行配置**:先新增 vite.config.ts,与 webpack 配置并存,开发者可以选择用哪个启动;CI 两种都构建。
|
||||
3. **逐项替换**:
|
||||
- `require.context` → `import.meta.glob`;
|
||||
- `process.env.VUE_APP_XXX` → `import.meta.env.VITE_XXX`(批量替换 + 类型声明);
|
||||
- 自定义 loader → Vite 插件的 `transform` 钩子;
|
||||
- 别名、代理配置平移。
|
||||
4. **验证**:页面冒烟测试(Playwright 跑核心流程);对比两份构建产物的体积和关键页面性能指标。
|
||||
5. **灰度上线**:先上测试环境,再让部分流量使用新产物;保留 webpack 构建一个迭代周期,出问题可以切回。
|
||||
|
||||
## 关键代码
|
||||
|
||||
```ts
|
||||
// webpack:const modules = require.context("./icons", false, /\.svg$/);
|
||||
// Vite:
|
||||
const icons = import.meta.glob("./icons/*.svg", { query: "?raw", import: "default", eager: true });
|
||||
|
||||
// 自定义 loader 改写为插件
|
||||
function svgIcon(): Plugin {
|
||||
return {
|
||||
name: "svg-icon",
|
||||
transform(code, id) {
|
||||
if (!id.endsWith(".svg?icon")) return null;
|
||||
return { code: `export default ${JSON.stringify(optimizeSvg(code))};`, map: null };
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
可运行的 Vite 配置:[examples/vite-config](../examples/vite-config/)。
|
||||
|
||||
## 取舍
|
||||
|
||||
| 选择 | 优点 | 缺点 |
|
||||
| --- | --- | --- |
|
||||
| **迁移到 Vite(本方案)** | 开发体验大幅提升、配置简单 | 迁移成本、部分 webpack 专有特性需要替代 |
|
||||
| 升级 webpack 5 + 缓存 + 更快的 loader(如 swc-loader) | 改动小 | 开发启动仍然慢于 Vite |
|
||||
| Rspack(兼容 webpack 配置的 Rust 构建工具) | 配置几乎不用改 | 生态兼容性需要验证 |
|
||||
|
||||
## 易错点
|
||||
|
||||
- 只验证开发模式,没有验证生产构建。
|
||||
- 批量替换环境变量时遗漏了第三方库读取的 `process.env.NODE_ENV`(Vite 构建时会自动替换这个,但其他 `process.env` 引用不会)。
|
||||
- 迁移后 chunk 的划分和文件名变化导致 CDN 缓存策略失效。
|
||||
- 一次性切换,没有回滚方案。
|
||||
|
||||
## 面试怎么讲
|
||||
|
||||
> “我主导过一个老项目从 webpack 迁到 Vite。先盘点配置和代码中的 webpack 专有用法,然后两套配置并存、CI 都构建,逐项替换 require.context、环境变量和自定义 loader;用 Playwright 跑核心流程冒烟,对比产物体积和性能指标;灰度上线,保留旧构建一个迭代。结果是开发启动和热更新明显变快(具体数字以你的项目实测为准)。”
|
||||
Reference in new issue
Block a user