主题
Vite
Vite(法语意为"快")是由 Vue 作者尤雨溪开发的下一代前端构建工具。它的核心理念是:开发阶段利用浏览器原生 ESM 能力按需加载模块,跳过打包步骤,从而实现毫秒级的冷启动和极速的热更新。生产构建则基于 Rollup,产出高度优化的静态资源。
本文将从底层原理 → 配置体系 → 插件机制 → 性能优化 → 横向对比 → 面试高频题六个维度全面拆解 Vite。
一、Vite 核心原理
1.1 为什么 Vite 快
传统构建工具(如 Webpack)采用 Bundle-based 模式:不管你改了哪一行代码,dev server 启动时都要先把所有模块打包成一个或多个 bundle,然后才能在浏览器中运行。项目越大,启动越慢。
Vite 采用 Native ESM-based 模式:利用现代浏览器原生支持 <script type="module"> 的能力,dev server 启动时只需启动一个 HTTP 服务器,浏览器请求哪个模块才编译哪个模块。
Webpack 启动流程(Bundle-based):
┌─────────────────────────────────────────────────────┐
│ 启动 dev server │
│ ↓ │
│ ┌────────────────────────────────────────────┐ │
│ │ 扫描全部入口和依赖关系 │ │
│ │ ↓ │ │
│ │ 编译所有模块(Loader 处理) │ │
│ │ ↓ │ │
│ │ 构建完整依赖图 │ │
│ │ ↓ │ │
│ │ 生成 Bundle(打包输出) │ │
│ └────────────────────────────────────────────┘ │
│ ↓ │
│ Bundle 准备好,页面可访问 │
└─────────────────────────────────────────────────────┘
时间线:[===== 全量打包 =====]→ 页面可用
↑
可能需要数十秒Vite 启动流程(Native ESM-based):
┌─────────────────────────────────────────────────────┐
│ 启动 dev server │
│ ↓ │
│ ┌────────────────────────────────────────────┐ │
│ │ esbuild 预构建第三方依赖(仅首次/依赖变化) │ │
│ │ (极快,毫秒级) │ │
│ └────────────────────────────────────────────┘ │
│ ↓ │
│ HTTP Server 就绪,页面可访问 │
│ ↓ │
│ ┌────────────────────────────────────────────┐ │
│ │ 浏览器发起 ESM 请求 → 按需编译单个模块 │ │
│ │ GET /src/App.tsx → 编译 App.tsx → 返回 │ │
│ │ GET /src/utils.ts → 编译 utils.ts → 返回 │ │
│ │ (只编译请求到的文件) │ │
│ └────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
时间线:[==预构建==]→ 页面可用 → [按需编译...]
↑
通常 < 1 秒关键差异:Webpack 的启动时间和项目规模成正比,而 Vite 的启动时间几乎与项目规模无关——因为它把编译工作推迟到了浏览器请求时按需执行。
1.2 开发环境:原生 ESM + esbuild 预构建
Vite 开发服务器的核心架构分为两部分:
第一部分:Dependency Pre-Bundling(依赖预构建)
浏览器原生 ESM 有两个问题需要解决:
- 裸模块导入:
import React from 'react'浏览器无法识别,不知道去哪里找react - 请求瀑布流:
lodash-es有 600+ 个内部模块,如果每个都发起一个 HTTP 请求,浏览器会被压垮
Vite 的解决方案:在 dev server 启动前,使用 esbuild 对 node_modules 中的第三方依赖进行预构建:
预构建流程:
node_modules/react/ node_modules/lodash-es/
├── index.js (CJS) ├── add.js
├── cjs/ ├── chunk.js
│ └── react.development.js ├── clone.js
└── ... ├── ... (600+ 文件)
└── lodash.js
│ │
│ esbuild 预构建 │
▼ ▼
.vite/deps/ .vite/deps/
├── react.js (ESM, 单文件) └── lodash-es.js (ESM, 单文件)
└── react.js.mapesbuild 做了两件事:
- 格式转换:将 CJS/UMD 格式转为 ESM
- 合并模块:将有大量内部模块的包(如 lodash-es)合并为单个文件,减少 HTTP 请求数
为什么选 esbuild?因为 esbuild 使用 Go 编写,利用多线程和高效的内存管理,编译速度比传统 JS 编写的工具(如 tsc、Babel)快 10-100 倍:
各工具编译速度对比(编译大型项目):
esbuild ████ 0.33s
swc ████████ 0.8s
Babel ████████████████████████████ 38s
tsc ████████████████████████████████████ 46s第二部分:源码按需编译
对于项目自身的源码(src/ 下的文件),Vite 不做任何预编译。当浏览器通过 ESM 请求某个模块时,Vite 的 dev server 拦截请求,实时编译并返回:
浏览器请求链路:
Browser Vite Dev Server
│ │
│ GET /src/main.tsx │
│ ──────────────────────────────────→│
│ │ 读取 main.tsx
│ │ esbuild 编译 TSX → JS
│ │ 重写 import 路径
│ 200 OK (编译后的 ESM JS) │
│ ←──────────────────────────────────│
│ │
│ import './App.tsx' 触发新请求 │
│ GET /src/App.tsx │
│ ──────────────────────────────────→│
│ │ 同样按需编译
│ 200 OK │
│ ←──────────────────────────────────│源码编译使用 esbuild 处理 TypeScript 和 JSX 转换(仅做语法转换,不做类型检查),CSS 等其他资源由 Vite 内置插件链处理。
1.3 生产环境:为什么用 Rollup 而不是 esbuild
Vite 在生产构建时使用 Rollup 而不是 esbuild。这个设计决策背后有深层考量:
为什么生产构建不用 esbuild?
┌───────────────────────────────────────────────────┐
│ esbuild 不足 │
│ │
│ 1. 代码分割(Code Splitting)能力有限 │
│ - 不够灵活的 chunk 分割策略 │
│ - 无法做细粒度的 manualChunks 控制 │
│ │
│ 2. CSS 处理能力弱 │
│ - 不支持 CSS Code Splitting │
│ - 不支持 CSS Modules 的完整语义 │
│ │
│ 3. 插件生态不成熟 │
│ - 插件 API 不够丰富 │
│ - 无法复用 Rollup 庞大的插件生态 │
│ │
│ 4. 产物优化不够精细 │
│ - Tree-shaking 不如 Rollup 精确 │
│ - 缺少高级优化如 scope hoisting │
└───────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────┐
│ Rollup 优势 │
│ │
│ 1. 成熟的代码分割和 Tree-shaking │
│ 2. 完善的插件 API 和庞大的插件生态 │
│ 3. 输出格式灵活(ESM / CJS / IIFE / UMD) │
│ 4. 对 ESM 输出做了 scope hoisting 优化 │
│ 5. 稳定可靠,经过大量生产环境验证 │
└───────────────────────────────────────────────────┘值得注意的是,Vite 团队正在开发 Rolldown(用 Rust 重写的 Rollup 兼容构建工具),目标是未来统一开发和生产构建,同时兼顾速度和产物质量。
1.4 HMR 热更新原理
HMR(Hot Module Replacement)是开发体验的核心。Vite 的 HMR 和 Webpack 有本质区别:
Webpack HMR 流程:
文件修改
↓
重新编译受影响的 Chunk(可能包含多个模块)
↓
生成增量更新补丁(.hot-update.js / .hot-update.json)
↓
通过 WebSocket 通知客户端
↓
客户端下载整个更新后的 Chunk
↓
替换旧模块,执行 accept 回调
问题:修改一个文件,整个 Chunk 需要重新编译和传输
项目越大,HMR 越慢Vite HMR 流程:
文件修改
↓
分析该模块的 HMR 边界(import.meta.hot.accept)
↓
仅重新编译修改的单个模块(或几个直接关联的模块)
↓
通过 WebSocket 发送更新通知(仅包含模块路径)
↓
客户端通过 ESM 动态 import 加载更新后的模块
GET /src/App.tsx?t=1709123456789 (时间戳使缓存失效)
↓
模块级精确替换
优势:更新粒度是单个模块,与项目规模无关
修改一个组件,只重新请求该组件的文件Vite HMR 的核心 API:
js
if (import.meta.hot) {
import.meta.hot.accept((newModule) => {
render(newModule.default)
})
import.meta.hot.accept(['./foo.js', './bar.js'], ([fooModule, barModule]) => {
update(fooModule, barModule)
})
import.meta.hot.dispose((data) => {
clearInterval(timer)
data.savedState = currentState
})
import.meta.hot.prune(() => {
cleanupSideEffects()
})
}accept 定义了 HMR 边界:
- 自身接受(Self-accepting):
accept((mod) => {...})— 模块自己处理更新 - 依赖接受:
accept(['./dep'], ([dep]) => {...})— 接受特定依赖的更新 - 无回调接受:
accept()— 接受更新并完全替换模块(框架插件常用)
当一个文件修改后,Vite 沿着模块依赖图向上查找最近的 HMR 边界。如果找不到边界,则整页刷新。React/Vue 等框架插件会自动为组件文件注入 HMR accept 逻辑。
1.5 模块解析与路径重写
Vite dev server 在返回编译后的代码之前,会进行关键的路径重写:
js
import React from 'react'
import { useState } from 'react'
import App from './App.tsx'
import styles from './index.module.css'经过 Vite 处理后变成:
js
import React from '/node_modules/.vite/deps/react.js?v=abc123'
import { useState } from '/node_modules/.vite/deps/react.js?v=abc123'
import App from '/src/App.tsx?t=1709123456'
import styles from '/src/index.module.css?direct'核心转换规则:
路径重写规则:
1. 裸模块(bare import)
'react' → '/node_modules/.vite/deps/react.js?v=<hash>'
指向预构建产物,?v= 用于强缓存
2. 相对路径
'./App.tsx' → '/src/App.tsx'
转为绝对路径,dev server 可直接定位文件
3. 特殊后缀
'./icon.svg?url' → 返回资源 URL 字符串
'./shader.glsl?raw' → 返回文件原始文本
'./worker.js?worker' → 返回 Web Worker 构造
4. CSS Modules
'./x.module.css' → 返回 JS 对象 + 注入 <style>二、Vite 配置深入
2.1 vite.config.ts 核心配置
ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
export default defineConfig({
root: process.cwd(),
base: '/',
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
'@components': path.resolve(__dirname, 'src/components'),
'@utils': path.resolve(__dirname, 'src/utils'),
},
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json'],
},
server: {
host: '0.0.0.0',
port: 3000,
strictPort: true,
open: true,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
'/ws': {
target: 'ws://localhost:8080',
ws: true,
},
},
cors: true,
},
build: {
target: 'es2015',
outDir: 'dist',
assetsDir: 'assets',
cssCodeSplit: true,
sourcemap: false,
minify: 'esbuild',
rollupOptions: {
input: {
main: path.resolve(__dirname, 'index.html'),
},
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
router: ['react-router-dom'],
},
chunkFileNames: 'assets/js/[name]-[hash].js',
entryFileNames: 'assets/js/[name]-[hash].js',
assetFileNames: 'assets/[ext]/[name]-[hash].[ext]',
},
},
chunkSizeWarningLimit: 500,
},
css: {
modules: {
localsConvention: 'camelCaseOnly',
scopeBehaviour: 'local',
},
preprocessorOptions: {
scss: {
additionalData: `@use "@/styles/variables" as *;`,
},
less: {
math: 'always',
javascriptEnabled: true,
},
},
postcss: './postcss.config.js',
devSourcemap: true,
},
define: {
__APP_VERSION__: JSON.stringify('1.0.0'),
__DEV__: JSON.stringify(true),
},
plugins: [react()],
})2.2 核心配置项详解
vite.config.ts 配置结构一览:
defineConfig({
├── root 项目根目录(index.html 所在目录)
├── base 公共基础路径(部署到子路径时设置)
├── mode 模式:'development' | 'production'
├── define 全局常量替换(编译时替换)
│
├── resolve
│ ├── alias 路径别名映射
│ ├── extensions 省略扩展名的解析顺序
│ └── conditions package.json exports 的条件解析
│
├── server
│ ├── host 监听地址
│ ├── port 端口号
│ ├── proxy 代理配置(基于 http-proxy)
│ ├── cors CORS 配置
│ ├── hmr HMR 连接配置
│ └── watch 文件监听配置(基于 chokidar)
│
├── build
│ ├── target 编译目标(默认 'modules')
│ ├── outDir 输出目录
│ ├── minify 压缩方式:'esbuild' | 'terser' | false
│ ├── sourcemap 是否生成 sourcemap
│ ├── cssCodeSplit 是否拆分 CSS
│ ├── lib 库模式配置
│ └── rollupOptions 直接传递给 Rollup 的配置
│
├── css
│ ├── modules CSS Modules 配置
│ ├── preprocessorOptions 预处理器选项
│ ├── postcss PostCSS 配置
│ └── devSourcemap 开发环境 CSS sourcemap
│
├── plugins 插件数组
├── optimizeDeps 预构建配置
└── ssr SSR 相关配置
})2.3 环境变量
Vite 使用 dotenv 从项目根目录的 .env 文件中加载环境变量:
环境变量文件加载优先级(从低到高):
.env 所有模式共享
.env.local 所有模式共享(被 git 忽略)
.env.[mode] 指定模式专用
.env.[mode].local 指定模式专用(被 git 忽略)
模式由 --mode 参数决定:
vite dev → mode = 'development'
vite build → mode = 'production'
vite build --mode staging → mode = 'staging'.env 文件示例:
VITE_API_BASE=https://api.example.com
VITE_APP_TITLE=My App
DB_PASSWORD=secret123在代码中使用:
ts
console.log(import.meta.env.VITE_API_BASE)
console.log(import.meta.env.VITE_APP_TITLE)
console.log(import.meta.env.MODE)
console.log(import.meta.env.DEV)
console.log(import.meta.env.PROD)
console.log(import.meta.env.BASE_URL)安全机制:只有以 VITE_ 前缀开头的变量才会暴露给客户端代码。上面示例中的 DB_PASSWORD 不会被打包到客户端,防止敏感信息泄露。
在 vite.config.ts 中如果需要访问环境变量,不能直接使用 import.meta.env,需要用 loadEnv:
ts
import { defineConfig, loadEnv } from 'vite'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '')
return {
define: {
__APP_ENV__: JSON.stringify(env.APP_ENV),
},
}
})TypeScript 类型支持:
ts
interface ImportMetaEnv {
readonly VITE_API_BASE: string
readonly VITE_APP_TITLE: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}2.4 CSS 处理
Vite 内置了对 CSS 的完整支持,无需额外安装 loader。
CSS Modules
任何以 .module.css 结尾的文件都会被视为 CSS Modules 文件:
css
.container {
max-width: 1200px;
margin: 0 auto;
}
.title {
font-size: 24px;
color: #333;
}tsx
import styles from './App.module.css'
function App() {
return (
<div className={styles.container}>
<h1 className={styles.title}>Hello</h1>
</div>
)
}编译后 CSS 类名会被转换为带 hash 的唯一标识,如 _container_1a2b3c。
预处理器
Vite 原生支持 .scss、.sass、.less、.styl、.stylus 文件,只需安装对应的预处理器即可:
bash
npm install -D sass
npm install -D less
npm install -D stylus不需要额外安装 loader 或配置规则,Vite 自动识别并处理。
全局注入变量:
ts
export default defineConfig({
css: {
preprocessorOptions: {
scss: {
additionalData: `
@use "@/styles/variables" as *;
@use "@/styles/mixins" as *;
`,
},
},
},
})PostCSS
Vite 自动读取项目根目录下的 PostCSS 配置文件(postcss.config.js):
js
module.exports = {
plugins: {
'postcss-preset-env': {
stage: 2,
features: {
'nesting-rules': true,
},
},
autoprefixer: {},
...(process.env.NODE_ENV === 'production' ? { cssnano: {} } : {}),
},
}2.5 静态资源处理
Vite 对静态资源提供了丰富的导入方式:
ts
import imgUrl from './img.png'
import imgUrl from './img.png?url'
import imgRaw from './shader.glsl?raw'
import Worker from './worker.js?worker'
import init from './example.wasm?init'各种导入方式的行为:
静态资源导入方式对比:
┌──────────────────┬────────────────────────────────────────┐
│ 导入方式 │ 行为 │
├──────────────────┼────────────────────────────────────────┤
│ import x from │ 默认导入,返回解析后的 URL 字符串 │
│ './img.png' │ 小于 assetsInlineLimit 的文件会内联为 │
│ │ base64 data URI │
├──────────────────┼────────────────────────────────────────┤
│ ?url │ 显式以 URL 形式导入,始终返回 URL │
│ │ 不会内联为 base64 │
├──────────────────┼────────────────────────────────────────┤
│ ?raw │ 以字符串形式导入资源的原始内容 │
│ │ 适合导入着色器、SVG 源码、文本文件等 │
├──────────────────┼────────────────────────────────────────┤
│ ?worker │ 导入为 Web Worker │
│ │ 返回 Worker 类的构造函数 │
├──────────────────┼────────────────────────────────────────┤
│ ?worker&inline │ 导入为内联 Worker(base64 编码) │
│ │ 不生成独立的 Worker 文件 │
├──────────────────┼────────────────────────────────────────┤
│ ?init │ WASM 模块导入 │
│ │ 返回初始化函数 │
└──────────────────┴────────────────────────────────────────┘public/ 目录下的资源不会被 Vite 处理,直接原样复制到输出目录根路径。适合放 favicon.ico、robots.txt 等不需要编译处理的文件。引用方式:
html
<img src="/logo.png" />src/assets/ 目录下的资源会被 Vite 处理(加 hash、内联等)。引用方式:
tsx
import logo from '@/assets/logo.png'
function Header() {
return <img src={logo} alt="logo" />
}三、Vite 插件系统
3.1 插件 API 概览
Vite 的插件系统基于 Rollup 插件接口扩展而来。一个 Vite 插件本质上就是一个返回对象的函数,对象中包含各种钩子函数:
ts
function myPlugin(): Plugin {
return {
name: 'my-plugin',
config(config, env) {
return {
define: {
__MY_VAR__: JSON.stringify('hello'),
},
}
},
configResolved(resolvedConfig) {
console.log('最终配置:', resolvedConfig.build.outDir)
},
configureServer(server) {
server.middlewares.use((req, res, next) => {
if (req.url === '/health') {
res.end('ok')
return
}
next()
})
},
transformIndexHtml(html) {
return html.replace(
'</head>',
'<script>window.__BUILD_TIME__ = Date.now()</script></head>'
)
},
resolveId(source, importer) {
if (source === 'virtual:my-module') {
return '\0virtual:my-module'
}
},
load(id) {
if (id === '\0virtual:my-module') {
return `export const msg = "from virtual module"`
}
},
transform(code, id) {
if (id.endsWith('.custom')) {
return {
code: `export default ${JSON.stringify(code)}`,
map: null,
}
}
},
}
}3.2 钩子执行顺序
Vite 插件钩子分为 Vite 独有钩子和兼容 Rollup 的通用钩子两类,执行顺序如下:
Vite 插件钩子执行顺序:
┌─ 配置阶段 ──────────────────────┐
│ │
│ 1. config 修改配置 │
│ 2. configResolved 读取最终配置 │
│ │
└───────────────────────────────────┘
│
┌─ 服务器阶段(仅 dev)─────────────┐
│ │
│ 3. configureServer 配置 dev 服务 │
│ │
└───────────────────────────────────┘
│
┌─ 构建阶段 ──────────────────────┐
│ │
│ 4. buildStart 构建开始 │
│ │ │
│ ┌─ 模块解析循环 ─────────────┐ │
│ │ │ │
│ │ 5. resolveId 解析模块 ID │ │
│ │ ↓ │ │
│ │ 6. load 加载模块内容 │ │
│ │ ↓ │ │
│ │ 7. transform 转换模块代码 │ │
│ │ │ │
│ └─────────────────────────────┘ │
│ │
│ 8. buildEnd 构建结束 │
│ │
└─────────────────────────────────────┘
│
┌─ 输出阶段(仅 build)────────────┐
│ │
│ 9. renderStart │
│ 10. renderChunk │
│ 11. generateBundle │
│ 12. writeBundle │
│ 13. closeBundle │
│ │
└───────────────────────────────────┘
│
┌─ HTML 转换 ─────────────────────┐
│ │
│ transformIndexHtml │
│ (dev:每次请求触发) │
│ (build:generateBundle 后触发) │
│ │
└───────────────────────────────────┘enforce 属性控制插件执行顺序:
插件排序:
enforce: 'pre' → 在 Vite 核心插件之前执行
无 enforce → 在 Vite 核心插件之后执行(默认)
enforce: 'post' → 在 Vite 构建插件之后执行
实际执行顺序:
1. alias 解析
2. enforce: 'pre' 的用户插件
3. Vite 核心插件(模块解析、CSS、esbuild 等)
4. 无 enforce 的用户插件
5. Vite 构建插件(minify 等)
6. enforce: 'post' 的用户插件apply 属性控制插件在哪个阶段生效:
ts
function myPlugin(): Plugin {
return {
name: 'my-plugin',
apply: 'build',
}
}
function myPlugin(): Plugin {
return {
name: 'my-plugin',
apply: 'serve',
}
}
function myPlugin(): Plugin {
return {
name: 'my-plugin',
apply(config, { command }) {
return command === 'build' && !config.build?.ssr
},
}
}3.3 常用插件
| 插件 | 功能 | 包名 |
|---|---|---|
| React 支持 | JSX 转换 + Fast Refresh HMR | @vitejs/plugin-react |
| React SWC | 用 SWC 替代 Babel,更快的 React 编译 | @vitejs/plugin-react-swc |
| Vue 支持 | SFC 编译 + HMR | @vitejs/plugin-vue |
| SVG 转 React 组件 | SVG → React Component | vite-plugin-svgr |
| PWA 支持 | Service Worker + Manifest | vite-plugin-pwa |
| 自动导入 | 自动导入 API,无需手写 import | unplugin-auto-import |
| 组件自动注册 | 自动注册组件,无需手动 import | unplugin-vue-components |
| 构建可视化 | 分析产物体积 | rollup-plugin-visualizer |
| Legacy 兼容 | 为旧浏览器生成兼容 chunk | @vitejs/plugin-legacy |
| 压缩 | gzip / brotli 压缩 | vite-plugin-compression |
| mock 数据 | 本地 mock API | vite-plugin-mock |
3.4 手写 Vite 插件实战
实战一:虚拟模块插件
虚拟模块是 Vite/Rollup 的强大特性——允许你创建不存在于文件系统中的模块:
ts
import type { Plugin } from 'vite'
interface BuildInfo {
version: string
buildTime: string
gitCommit: string
}
function virtualBuildInfoPlugin(): Plugin {
const virtualModuleId = 'virtual:build-info'
const resolvedVirtualModuleId = '\0' + virtualModuleId
return {
name: 'vite-plugin-build-info',
resolveId(id) {
if (id === virtualModuleId) {
return resolvedVirtualModuleId
}
},
load(id) {
if (id === resolvedVirtualModuleId) {
const info: BuildInfo = {
version: process.env.npm_package_version || '0.0.0',
buildTime: new Date().toISOString(),
gitCommit: 'unknown',
}
return `export default ${JSON.stringify(info)}`
}
},
}
}
export default virtualBuildInfoPlugin使用方式:
ts
import buildInfo from 'virtual:build-info'
console.log(buildInfo.version)
console.log(buildInfo.buildTime)类型声明(env.d.ts 或 vite-env.d.ts):
ts
declare module 'virtual:build-info' {
interface BuildInfo {
version: string
buildTime: string
gitCommit: string
}
const info: BuildInfo
export default info
}实战二:自动导入插件
ts
import type { Plugin } from 'vite'
import fs from 'fs'
import path from 'path'
function autoImportPlugin(options: { dirs: string[] }): Plugin {
const importMap = new Map<string, string>()
function scanDir(dir: string) {
if (!fs.existsSync(dir)) return
const files = fs.readdirSync(dir, { recursive: true }) as string[]
for (const file of files) {
if (/\.(ts|js)$/.test(file) && !file.includes('.d.ts')) {
const fullPath = path.join(dir, file)
const content = fs.readFileSync(fullPath, 'utf-8')
const exportMatches = content.matchAll(/export\s+(?:function|const|class)\s+(\w+)/g)
for (const match of exportMatches) {
const exportName = match[1]
const relativePath = fullPath.replace(/\\/g, '/')
importMap.set(exportName, relativePath)
}
}
}
}
return {
name: 'vite-plugin-auto-import',
enforce: 'pre',
configResolved(config) {
for (const dir of options.dirs) {
const fullDir = path.resolve(config.root, dir)
scanDir(fullDir)
}
},
transform(code, id) {
if (!id.endsWith('.ts') && !id.endsWith('.tsx') &&
!id.endsWith('.js') && !id.endsWith('.jsx')) {
return null
}
let hasChanges = false
let imports: string[] = []
for (const [name, filePath] of importMap) {
if (filePath === id) continue
const usageRegex = new RegExp(`\\b${name}\\b`)
const importRegex = new RegExp(`import.*${name}.*from`)
if (usageRegex.test(code) && !importRegex.test(code)) {
const relative = path.relative(path.dirname(id), filePath)
.replace(/\\/g, '/')
.replace(/\.(ts|js)$/, '')
const importPath = relative.startsWith('.') ? relative : `./${relative}`
imports.push(`import { ${name} } from '${importPath}';`)
hasChanges = true
}
}
if (!hasChanges) return null
return {
code: imports.join('\n') + '\n' + code,
map: null,
}
},
}
}
export default autoImportPlugin实战三:Markdown 转 Vue/React 组件插件
ts
import type { Plugin } from 'vite'
import { marked } from 'marked'
function markdownPlugin(): Plugin {
return {
name: 'vite-plugin-markdown',
enforce: 'pre',
transform(code, id) {
if (!id.endsWith('.md')) return null
const html = marked.parse(code) as string
const escaped = JSON.stringify(html)
return {
code: `export default ${escaped}`,
map: null,
}
},
}
}
export default markdownPlugin实战四:configureServer 实现 Mock API 中间件
ts
import type { Plugin } from 'vite'
interface MockRoute {
url: string
method: 'GET' | 'POST' | 'PUT' | 'DELETE'
response: (params: { query: Record<string, string>; body: any }) => any
}
function mockPlugin(routes: MockRoute[]): Plugin {
return {
name: 'vite-plugin-mock',
apply: 'serve',
configureServer(server) {
for (const route of routes) {
server.middlewares.use(route.url, (req, res, next) => {
if (req.method?.toUpperCase() !== route.method) {
next()
return
}
const url = new URL(req.url || '', `http://${req.headers.host}`)
const query = Object.fromEntries(url.searchParams)
let body = ''
req.on('data', (chunk) => { body += chunk })
req.on('end', () => {
let parsedBody = {}
try { parsedBody = JSON.parse(body) } catch {}
const data = route.response({ query, body: parsedBody })
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify(data))
})
})
}
},
}
}
export default mockPlugin四、性能优化
4.1 预构建优化
Vite 自动检测需要预构建的依赖,但有些场景需要手动干预:
ts
export default defineConfig({
optimizeDeps: {
include: [
'lodash-es',
'axios',
'dayjs',
'dayjs/plugin/relativeTime',
],
exclude: [
'@vueuse/core',
],
esbuildOptions: {
target: 'esnext',
supported: {
'top-level-await': true,
},
},
},
})何时需要手动配置 optimizeDeps?
┌──────────────────────────────────────────────────────────┐
│ include(强制预构建) │
│ │
│ 1. 动态 import 的依赖(Vite 静态分析检测不到) │
│ const module = await import(`./locale/${lang}.js`) │
│ │
│ 2. 依赖的依赖(深层依赖)导致请求瀑布流 │
│ 例如 A 依赖 B,B 依赖 C,C 依赖 D... │
│ include: ['A > B > C > D'] │
│ │
│ 3. monorepo 中 linked 的包 │
│ workspace 包不在 node_modules 中,不会自动预构建 │
│ │
├──────────────────────────────────────────────────────────┤
│ exclude(排除预构建) │
│ │
│ 1. 本身已经是 ESM 格式且模块数量少的包 │
│ 预构建没有收益反而增加启动时间 │
│ │
│ 2. 需要在 Vite 插件中自定义处理的包 │
│ 预构建会绕过 Vite 插件链 │
│ │
│ 3. 含有特殊 import 语法的包 │
│ esbuild 可能无法正确处理 │
└──────────────────────────────────────────────────────────┘4.2 代码分割策略
生产构建时,合理的代码分割对性能至关重要:
ts
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('react') || id.includes('react-dom') || id.includes('scheduler')) {
return 'react-vendor'
}
if (id.includes('lodash') || id.includes('dayjs') || id.includes('axios')) {
return 'lib-vendor'
}
if (id.includes('@ant-design') || id.includes('antd')) {
return 'ui-vendor'
}
return 'vendor'
}
},
},
},
},
})常见的分割策略:
代码分割策略对比:
策略一:按更新频率分割
┌─────────────────────────────────────────┐
│ 框架(react/vue) → framework.js │ 极少更新,强缓存
│ UI 库(antd) → ui.js │ 偶尔更新
│ 工具库(lodash) → lib.js │ 较少更新
│ 业务代码 → app-[hash].js │ 频繁更新
│ 路由页面 → page-[hash].js │ 按需加载
└─────────────────────────────────────────┘
策略二:按体积分割
┌─────────────────────────────────────────┐
│ 大于 200KB 的包单独成 chunk │
│ 其余 node_modules 合并为 vendor │
│ 业务代码自动按路由分割 │
└─────────────────────────────────────────┘
策略三:按功能分割
┌─────────────────────────────────────────┐
│ 编辑器相关 → editor.js (monaco 等) │ 仅编辑页加载
│ 图表相关 → chart.js (echarts 等) │ 仅报表页加载
│ 地图相关 → map.js (mapbox 等) │ 仅地图页加载
│ 核心功能 → core.js │ 所有页面加载
└─────────────────────────────────────────┘路由级别的代码分割(以 React 为例):
tsx
import { lazy, Suspense } from 'react'
import { createBrowserRouter } from 'react-router-dom'
const Home = lazy(() => import('./pages/Home'))
const Dashboard = lazy(() => import('./pages/Dashboard'))
const Settings = lazy(() => import('./pages/Settings'))
const router = createBrowserRouter([
{
path: '/',
element: (
<Suspense fallback={<div>Loading...</div>}>
<Home />
</Suspense>
),
},
{
path: '/dashboard',
element: (
<Suspense fallback={<div>Loading...</div>}>
<Dashboard />
</Suspense>
),
},
{
path: '/settings',
element: (
<Suspense fallback={<div>Loading...</div>}>
<Settings />
</Suspense>
),
},
])Vite 会自动将 lazy(() => import(...)) 的模块分割为独立 chunk。
4.3 依赖外部化
对于 CDN 加载的库或服务端渲染不需要打包的依赖:
ts
export default defineConfig({
build: {
rollupOptions: {
external: ['react', 'react-dom'],
output: {
globals: {
react: 'React',
'react-dom': 'ReactDOM',
},
},
},
},
})配合 transformIndexHtml 钩子注入 CDN 链接:
ts
function cdnPlugin(externals: Record<string, string>): Plugin {
return {
name: 'vite-plugin-cdn',
apply: 'build',
transformIndexHtml(html) {
const scripts = Object.values(externals)
.map((url) => `<script src="${url}"></script>`)
.join('\n ')
return html.replace('</head>', ` ${scripts}\n </head>`)
},
}
}4.4 构建分析
使用 rollup-plugin-visualizer 分析产物体积:
ts
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
visualizer({
open: true,
gzipSize: true,
brotliSize: true,
filename: 'stats.html',
}),
],
})构建分析产出 stats.html:
┌──────────────────────────────────────────────────┐
│ │
│ ┌────────────────────┐ ┌──────────────────────┐ │
│ │ │ │ │ │
│ │ react-vendor │ │ ui-vendor │ │
│ │ 142 KB (gzip) │ │ 238 KB (gzip) │ │
│ │ │ │ │ │
│ │ ┌──────┐┌──────┐ │ │ ┌────────────────┐ │ │
│ │ │react ││r-dom │ │ │ │ antd │ │ │
│ │ │ 42KB ││100KB │ │ │ │ 238KB │ │ │
│ │ └──────┘└──────┘ │ │ └────────────────┘ │ │
│ └────────────────────┘ └──────────────────────┘ │
│ │
│ ┌────────────┐ ┌──────────┐ ┌────────────────┐ │
│ │ lib-vendor │ │ app │ │ pages (lazy) │ │
│ │ 56 KB │ │ 32 KB │ │ 18 KB each │ │
│ └────────────┘ └──────────┘ └────────────────┘ │
│ │
└──────────────────────────────────────────────────┘其他有用的构建优化配置:
ts
export default defineConfig({
build: {
target: 'es2015',
minify: 'esbuild',
cssMinify: 'esbuild',
reportCompressedSize: false,
assetsInlineLimit: 4096,
chunkSizeWarningLimit: 500,
sourcemap: false,
},
})4.5 SSR 支持
Vite 原生支持 SSR,提供了框架无关的底层 API:
ts
import express from 'express'
import { createServer as createViteServer } from 'vite'
async function createServer() {
const app = express()
const vite = await createViteServer({
server: { middlewareMode: true },
appType: 'custom',
})
app.use(vite.middlewares)
app.use('*', async (req, res, next) => {
const url = req.originalUrl
try {
let template = fs.readFileSync(
path.resolve(__dirname, 'index.html'),
'utf-8'
)
template = await vite.transformIndexHtml(url, template)
const { render } = await vite.ssrLoadModule('/src/entry-server.tsx')
const appHtml = await render(url)
const html = template.replace('<!--ssr-outlet-->', appHtml)
res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
} catch (e) {
vite.ssrFixStacktrace(e as Error)
next(e)
}
})
app.listen(5173)
}
createServer()Vite SSR 架构:
┌──────────── 开发模式 ──────────────────────────────┐
│ │
│ Express Server │
│ │ │
│ ├── vite.middlewares(静态资源 + HMR) │
│ │ │
│ └── SSR 路由处理 │
│ │ │
│ ├── vite.transformIndexHtml() │
│ │ 处理 HTML 模板 │
│ │ │
│ ├── vite.ssrLoadModule() │
│ │ 加载服务端入口(支持 HMR!) │
│ │ │
│ └── render() → HTML 字符串 │
│ │
└──────────────────────────────────────────────────────┘
┌──────────── 生产模式 ──────────────────────────────┐
│ │
│ vite build → dist/client/ │
│ vite build --ssr → dist/server/ │
│ │
│ Express Server │
│ │ │
│ ├── sirv('dist/client') 静态资源服务 │
│ │ │
│ └── import('dist/server/entry-server.js') │
│ │ │
│ └── render() → HTML 字符串 │
│ │
└──────────────────────────────────────────────────────┘关键的 SSR 相关配置:
ts
export default defineConfig({
ssr: {
external: ['express'],
noExternal: ['my-shared-lib'],
target: 'node',
format: 'esm',
},
})external:不打包的依赖,保持require()/import语句,由 Node.js 运行时解析noExternal:强制打包的依赖(通常是 ESM only 的包需要转为 CJS 时使用)target:'node'或'webworker'
五、Vite vs 其他构建工具
5.1 全面对比表
| 维度 | Vite | Webpack | Turbopack | Rspack |
|---|---|---|---|---|
| 开发启动速度 | ⚡ 极快(毫秒级) | 🐌 慢(秒-十秒级) | ⚡ 极快 | ⚡ 快(亚秒级) |
| HMR 速度 | ⚡ 极快(模块级) | 🐌 较慢(chunk 级) | ⚡ 极快 | ⚡ 快 |
| 生产构建速度 | 🔵 中等(Rollup) | 🔵 中等 | 🟡 快(Turbo 引擎) | ⚡ 快(Rust) |
| 开发原理 | Native ESM | Bundle | Bundle(Turbo 引擎) | Bundle(Rust) |
| 生产原理 | Rollup | Webpack | Webpack 兼容 | Webpack 兼容 |
| 语言实现 | JS + Go(esbuild) | JS | Rust | Rust |
| 配置复杂度 | 🟢 简单 | 🔴 复杂 | 🟢 简单(Next.js 内置) | 🟡 中等(兼容 Webpack) |
| 插件生态 | 🟡 中等(Rollup 生态) | 🟢 最丰富 | 🔴 较少(封闭在 Next.js) | 🟡 兼容 Webpack 插件 |
| Loader/Plugin 兼容 | Rollup 插件兼容 | Webpack 专有 | Webpack 部分兼容 | Webpack 高度兼容 |
| Tree-shaking | 🟢 优秀(Rollup) | 🟡 一般 | 🟡 一般 | 🟡 一般 |
| CSS 处理 | 内置完善 | 需 loader 配置 | Next.js 内置 | 需 loader 配置 |
| TypeScript | 内置(esbuild) | 需 ts-loader/babel | 内置 | 内置(SWC) |
| SSR 支持 | 内置基础 API | 需额外配置 | Next.js 内置 | 需额外配置 |
| 适用场景 | 新项目首选 | 存量大型项目 | Next.js 项目 | Webpack 项目迁移 |
| 成熟度 | 🟢 成熟 | 🟢 最成熟 | 🟡 较新 | 🟡 较新 |
5.2 如何选择
选择决策树:
新项目?
├── 是 → 使用 React/Vue/Svelte 等?
│ ├── 是 → Vite(首选)
│ └── Next.js 项目 → Turbopack(内置)
│
└── 否(存量项目迁移)→ 当前用 Webpack?
├── 是 → 迁移成本可接受?
│ ├── 是 → Vite(完整迁移,收益大)
│ └── 否 → Rspack(兼容 Webpack 配置,迁移成本低)
│
└── 否 → 具体分析5.3 Vite vs Webpack 深度对比
开发服务器架构对比:
Webpack Dev Server:
┌──────────────────────────────────────┐
│ entry.js │
│ ├── moduleA.js │
│ ├── moduleB.js │
│ │ ├── moduleC.js │
│ │ └── moduleD.js │
│ └── moduleE.js │
│ │
│ 全部模块 → Webpack 编译 → Bundle │
│ ↓ │
│ bundle.js (完整) │
│ ↓ │
│ Dev Server 提供服务 │
└──────────────────────────────────────┘
Vite Dev Server:
┌──────────────────────────────────────┐
│ entry.js │
│ ├── moduleA.js ← 请求时编译 │
│ ├── moduleB.js ← 请求时编译 │
│ │ ├── moduleC.js ← 可能不请求 │
│ │ └── moduleD.js ← 可能不请求 │
│ └── moduleE.js ← 请求时编译 │
│ │
│ 浏览器 ESM → 按需请求 → 即时编译返回 │
│ │
│ 只有实际访问到的模块才会被编译 │
└──────────────────────────────────────┘HMR 更新粒度对比:
场景:修改了 moduleD.js
Webpack HMR:
moduleD.js 变化
↓
重新编译包含 moduleD 的整个 Chunk
(可能包含 moduleB + moduleC + moduleD + 其他模块)
↓
客户端下载整个更新后的 Chunk
↓
替换旧 Chunk 中的模块
更新时间:~300ms - 2000ms(取决于 Chunk 大小)
Vite HMR:
moduleD.js 变化
↓
仅重新编译 moduleD.js
↓
WebSocket 通知:{ type: 'update', path: '/src/moduleD.js' }
↓
客户端:import('/src/moduleD.js?t=timestamp')
↓
仅下载和替换 moduleD 这一个模块
更新时间:~10ms - 50ms(几乎恒定)5.4 Vite 的潜在问题
Vite 不是银弹,也有一些需要注意的问题:
Vite 的潜在挑战:
1. 开发/生产一致性
┌─────────────────────────────────────────┐
│ 开发环境:esbuild + Native ESM │
│ 生产环境:Rollup Bundle │
│ │
│ 两套不同的编译流程可能导致: │
│ - 开发正常但生产构建报错 │
│ - 依赖解析行为不一致 │
│ - CSS 处理差异 │
│ │
│ 解决:CI 中始终运行 build 验证 │
└─────────────────────────────────────────┘
2. 首屏请求瀑布流(大型项目)
┌─────────────────────────────────────────┐
│ 首次访问页面时,浏览器需要逐级加载 ESM: │
│ │
│ main.tsx │
│ → import App.tsx │
│ → import Header.tsx │
│ → import Button.tsx │
│ → import Icon.tsx │
│ │
│ 每一级都是一个 HTTP 请求 │
│ 深层嵌套会导致首屏加载变慢 │
│ │
│ 缓解:预构建 + HTTP/2 多路复用 │
└─────────────────────────────────────────┘
3. CommonJS 兼容性
┌─────────────────────────────────────────┐
│ 原生 ESM 环境下,CJS 模块需要预构建转换 │
│ │
│ 部分 CJS 包的动态 require 可能转换失败: │
│ const mod = require(condition ? 'a' : 'b')│
│ │
│ 解决:optimizeDeps.include 手动指定 │
│ 或联系包作者发布 ESM 版本 │
└─────────────────────────────────────────┘六、面试高频问题
问题一:Vite 为什么比 Webpack 快?底层原理是什么?
回答思路:
分两个层面来解释——开发环境和构建工具选择。
开发环境:Webpack 采用 Bundle-based 架构,启动 dev server 前需要把所有模块打包成 bundle。项目越大,启动越慢。Vite 利用浏览器原生 ESM 能力,dev server 只需启动 HTTP 服务器,不做全量打包。浏览器发出 ESM 请求时才按需编译对应模块。因此启动时间几乎与项目规模无关。
构建工具选择:Vite 使用 esbuild(Go 编写)做依赖预构建和 TS/JSX 转译,比 Babel/tsc(JS 编写)快 10-100 倍。esbuild 利用多核并行和高效内存管理。
HMR 层面:Webpack 的 HMR 需要重新编译整个 Chunk 并传输,Vite 只需重新编译修改的单个模块文件,通过原生 ESM 动态 import 加载,更新速度恒定不受项目规模影响。
追问:那 Vite 有什么劣势?
开发和生产环境使用不同的构建工具(esbuild vs Rollup),可能存在行为不一致。首次访问大型页面时,深层 ESM 依赖链可能导致请求瀑布流。Vite 团队正在通过 Rolldown 项目解决开发/生产统一的问题。
问题二:Vite 的依赖预构建(Pre-Bundling)是什么?为什么需要它?
回答思路:
预构建解决两个核心问题:
第一,格式兼容。npm 上大量包仍使用 CommonJS 格式(如 React),浏览器原生 ESM 无法直接执行 CJS 代码。esbuild 将 CJS/UMD 转为 ESM。
第二,减少请求数。某些 ESM 包内部有大量细碎模块(如 lodash-es 有 600+ 文件),如果每个都发起 HTTP 请求会严重影响性能。esbuild 将这些内部模块合并为单个文件。
预构建产物缓存在 node_modules/.vite/deps,通过 hash 比对 package.json 的 dependencies、lock 文件、Vite 配置来判断是否需要重新预构建。
追问:如果依赖预构建失败怎么办?
可以通过 optimizeDeps.include 强制指定需要预构建的包,用 optimizeDeps.exclude 排除有问题的包。删除 node_modules/.vite 目录可以强制重新预构建。也可以启动时加 --force 参数。
问题三:Vite 的插件机制是怎样的?和 Rollup 插件有什么关系?
回答思路:
Vite 的插件 API 是 Rollup 插件 API 的超集。在通用构建钩子(resolveId、load、transform、buildStart、buildEnd 等)上与 Rollup 完全兼容,很多 Rollup 插件可以直接在 Vite 中使用。
Vite 在此基础上扩展了一些独有钩子:config(修改配置)、configResolved(读取最终配置)、configureServer(配置 dev server,可添加自定义中间件)、transformIndexHtml(转换 HTML)、handleHotUpdate(自定义 HMR 处理)。
插件可以通过 enforce: 'pre' | 'post' 控制执行顺序,通过 apply: 'serve' | 'build' 控制生效环境。
追问:如何开发一个 Vite 虚拟模块插件?
使用 resolveId 钩子拦截特定的模块 ID(如 virtual:xxx),返回带 \0 前缀的 resolved ID(\0 是 Rollup 约定,表示虚拟模块,告诉其他插件不要处理)。然后在 load 钩子中匹配该 ID,返回动态生成的模块内容。
问题四:Vite 生产构建为什么用 Rollup 而不是 esbuild?
回答思路:
esbuild 虽然极快,但在生产构建的某些关键能力上还不够成熟:
- 代码分割不够灵活,无法精细控制 chunk 的分割策略(如
manualChunks) - CSS 处理能力有限,不支持 CSS Code Splitting
- 插件生态不如 Rollup 丰富
- Tree-shaking 不如 Rollup 精确,Rollup 的 scope hoisting 可以生成更小的产物
Rollup 是专为 ESM 设计的打包器,Tree-shaking 最为精确,插件生态完善,产物质量高。Vite 选择 Rollup 是在构建速度和产物质量之间做的权衡。
未来 Rolldown(Rust 重写的 Rollup 兼容工具)可能会统一开发和生产构建。
问题五:Vite 的 HMR 是如何实现的?和 Webpack 有什么区别?
回答思路:
核心区别在于更新粒度。
Webpack HMR:文件修改后,重新编译该文件所在的整个 Chunk,将增量更新补丁通过 WebSocket 发送给客户端。客户端下载新的 Chunk 并替换旧模块。项目越大、Chunk 越大,HMR 越慢。
Vite HMR:文件修改后,Vite 通过模块依赖图找到该模块的 HMR 边界(由 import.meta.hot.accept 定义)。仅重新编译该单一模块,通过 WebSocket 发送更新通知(只包含模块路径),客户端通过 ESM 动态 import 加上时间戳 query 参数请求新模块,实现模块级精确替换。更新速度与项目规模无关。
框架插件(如 @vitejs/plugin-react)会自动为组件注入 HMR accept 代码,开发者通常不需要手写 HMR API。
问题六:如何优化 Vite 项目的生产构建产物体积?
回答思路:
代码分割:通过
build.rollupOptions.output.manualChunks实现合理的 chunk 分割。按更新频率分离框架代码、UI 库、工具库和业务代码,最大化缓存利用率。路由级别使用lazy()+import()做按需加载。Tree-shaking:确保使用 ESM 格式的依赖(如用
lodash-es替代lodash)。避免 barrel files(index.ts中export * from ...)导致的 Tree-shaking 失效。依赖外部化:大型不常变的依赖(如 React)通过
build.rollupOptions.external外部化,走 CDN 加载。压缩优化:使用 esbuild 或 terser 压缩。配合
vite-plugin-compression生成 gzip/brotli 压缩产物。分析产物:用
rollup-plugin-visualizer可视化分析各 chunk 的体积分布,找出异常大的依赖进行针对性优化。图片和资源:配置
build.assetsInlineLimit控制小资源内联阈值。使用现代图片格式(WebP/AVIF)。
问题七:Vite 中如何处理环境变量?安全机制是什么?
回答思路:
Vite 使用 .env 文件管理环境变量,通过 import.meta.env 访问。文件加载优先级为:.env → .env.local → .env.[mode] → .env.[mode].local,后加载的覆盖先加载的。
安全机制:只有以 VITE_ 前缀开头的变量才会暴露给客户端代码(通过 import.meta.env.VITE_XXX 访问)。不带 VITE_ 前缀的变量不会被注入客户端 bundle,防止数据库密码、API Secret 等敏感信息泄露。
在 vite.config.ts 中需要使用 loadEnv(mode, root) 来读取环境变量,因为配置文件在 Vite 初始化前执行,import.meta.env 还不可用。
define 配置项做的是编译时的字符串替换(类似 C 的宏),和环境变量不同,它在 bundle 中会被直接替换为字面量值。
问题八:Vite 开发环境和生产环境的差异可能导致哪些问题?如何规避?
回答思路:
Vite 开发环境使用原生 ESM + esbuild,生产环境使用 Rollup 打包。两套不同的系统可能导致:
模块解析差异:开发时通过浏览器 ESM 逐个请求模块,生产时 Rollup 静态分析依赖图。某些动态 import 模式在开发正常但生产构建可能失败。
CSS 处理差异:开发时 CSS 通过 JS 注入
<style>标签,生产时提取为独立 CSS 文件。可能出现样式顺序不一致。esbuild vs Rollup 的 Tree-shaking 差异:esbuild 不做 Tree-shaking(开发不需要),Rollup 会做。某些有副作用的代码可能在生产被误删。
规避方案:
- CI/CD 中必须运行
vite build验证 - 使用
vite preview本地预览生产构建产物 - 关注依赖的
sideEffects声明 - 对关键功能做 E2E 测试覆盖