Vue 基础体系 · 第 2/70 篇。示例基于 Vue 3、Composition API、TypeScript 与现代 Vite 工具链;版本敏感能力会单独标注。
Vue 项目与 Vite 工具链:创建、环境变量、构建、代理和依赖治理
Vue 3 项目通常由三层工具组成:
- Vue:负责组件、响应式状态、模板和运行时渲染。
- Vite:负责开发服务器、模块转换、依赖预构建、生产构建和静态资源处理。
- Node.js 与包管理器:负责运行工具链、解析依赖、安装包和生成锁文件。
因此,“创建一个 Vue 项目”并不只是执行一个脚手架命令。一个可交付项目还必须明确:
- 开发服务器如何启动;
- TypeScript 是否经过类型检查;
- 环境变量从哪里来,哪些变量可以进入浏览器;
- 开发环境代理如何工作,生产环境由谁承担反向代理;
- 构建产物是什么,如何验证;
- 依赖如何安装、锁定、升级、审计和恢复。
本文示例基于 Vue 3、Composition API、TypeScript 和现代 Vite 工具链。Vite 的命令、Node.js 支持范围及部分配置能力会随主版本变化,实际使用时应以当前 Vite 官方文档和项目生成的配置为准。
一、创建 Vue 项目前,先明确工具链边界
1. Node.js 是工具链的运行时
Vite 本身是 Node.js 程序。执行 npm create vite、npm run dev 或 npm run build 时,首先运行的是 Node.js,然后 Node.js 加载 Vite、Vue 插件、TypeScript 检查工具等依赖。
Node.js 版本过低时,常见失败表现包括:
This version of Node.js is not supported
SyntaxError: Unexpected token
Cannot find module ...
这些错误不一定来自 Vue 代码,可能是工具链依赖使用了当前 Node.js 不支持的语法或 API。
先检查环境:
node --version
npm --version
输出类似:
v22.14.0
10.9.2
具体需要哪个 Node.js 版本,应查看当前 Vite 版本的 engines 要求。不要只根据团队其他项目的版本推断,因为不同 Vite 主版本的最低 Node.js 版本可能不同。
工程上通常还应固定 Node.js 版本,例如使用 .nvmrc:
22.14.0
或者在 package.json 中声明范围:
{
"engines": {
"node": ">=20.19.0"
}
}
engines 主要用于声明和提示,不等同于所有环境都会强制阻止安装。CI 中仍应显式检查 Node.js 版本。
2. 用官方脚手架创建 Vue + TypeScript 项目
创建项目:
npm create vite@latest my-vue-app -- --template vue-ts
cd my-vue-app
npm install
npm run dev
命令的因果关系如下:
npm create vite@latest下载并执行 Vite 的项目生成器;my-vue-app是目录名;--template vue-ts选择 Vue 3 + TypeScript 模板;npm install根据package.json安装依赖并生成或更新锁文件;npm run dev执行package.json中的dev脚本,启动 Vite 开发服务器。
终端通常会显示类似:
VITE vX.Y.Z ready in 300 ms
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
这里的 5173 是 Vite 常见的默认端口,但如果端口被占用,Vite 可能自动选择其他端口,因此应以终端实际输出为准。
可以用浏览器访问:
http://localhost:5173/
也可以使用:
curl -I http://localhost:5173/
预期得到 200 OK 或类似的成功响应。若项目设置了 SPA 回退,直接访问一个不存在的前端路径也可能返回 index.html,这不代表该路由一定存在,而是开发服务器将页面交给前端路由处理。
3. 生成项目后,应先阅读脚本而不是假设脚本名称
典型的 package.json 可能包含:
{
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
}
}
但不同模板版本可能生成不同脚本,例如使用 tsc、vue-tsc 或项目引用配置。应以当前项目文件为准:
npm run
这会列出可执行脚本。
三个核心命令的职责不同:
vite:启动开发服务器;vite build:生成生产构建产物;vite preview:用 Vite 提供本地静态预览服务。
vite preview 不是生产服务器。它适合检查构建结果是否能够被访问,不负责替代生产环境中的 Nginx、CDN、对象存储或云平台静态托管。
二、一个 Vue + TypeScript 项目的基本执行链
一个典型请求链可以抽象为:
flowchart LR
A[浏览器请求页面] --> B[Vite 开发服务器]
B --> C[index.html]
C --> D[main.ts]
D --> E[Vue createApp]
E --> F[根组件 App.vue]
F --> G[组件模板与响应式状态]
G --> H[浏览器 DOM]
I[浏览器 API 请求 /api] --> B
B --> J[开发代理]
J --> K[后端服务]
开发阶段,浏览器不一定直接获得一个已经打包完成的 JavaScript 文件。Vite 会按模块提供源码,并在浏览器请求模块时进行必要转换。这样可以减少开发启动时的全量打包成本。
生产构建则不同:
flowchart LR
A[Vue 源码] --> B[Vite 构建]
B --> C[模块解析]
C --> D[TypeScript/模板转换]
D --> E[依赖处理与代码分割]
E --> F[压缩与资源命名]
F --> G[dist 静态文件]
G --> H[CDN/Nginx/静态托管]
需要注意,TypeScript 的“转换”和“类型检查”是两个概念:
- Vite 需要把
.ts、.tsx等代码转换为浏览器可执行的 JavaScript; - 类型检查则需要
tsc或vue-tsc分析类型关系。
许多 Vite 开发流程为了保持速度,不会在每次模块转换时完整执行类型检查。因此,项目应该把类型检查作为独立脚本或构建前置步骤。
例如:
{
"scripts": {
"typecheck": "vue-tsc --noEmit",
"build": "npm run typecheck && vite build"
}
}
vue-tsc 能够理解 Vue 单文件组件中的 <script setup lang="ts"> 和模板类型。只执行 vite build,不能自动证明所有 TypeScript 类型正确。
三、Vue 入口、Composition API 与 TypeScript 的最小端到端示例
典型入口文件是 src/main.ts:
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
createApp(App).mount('#app')
对应的 index.html 通常包含挂载节点:
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vue App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
createApp(App) 创建 Vue 应用实例,.mount('#app') 将根组件渲染到 #app 元素中。根组件通常使用 Composition API:
<script setup lang="ts">
import { computed, ref } from 'vue'
interface User {
id: number
name: string
}
const users = ref<User[]>([
{ id: 1, name: 'Ada' },
{ id: 2, name: 'Linus' }
])
const keyword = ref('')
const filteredUsers = computed(() => {
const normalized = keyword.value.trim().toLowerCase()
if (!normalized) {
return users.value
}
return users.value.filter((user) =>
user.name.toLowerCase().includes(normalized)
)
})
</script>
<template>
<main>
<label>
搜索:
<input v-model="keyword" />
</label>
<ul>
<li v-for="user in filteredUsers" :key="user.id">
{{ user.id }} - {{ user.name }}
</li>
</ul>
</main>
</template>
这里有三条重要数据流:
keyword是响应式引用,模板中的v-model会更新它;filteredUsers是计算状态,只在依赖的keyword或users变化后重新计算;v-for根据filteredUsers产生 DOM,:key用于帮助 Vue 识别列表项身份。
TypeScript 的 interface User 只在开发和检查阶段存在,构建后的浏览器代码不会携带这个接口定义。类型信息不会自动在运行时验证后端 JSON,因此 API 响应仍需要运行时校验或可靠的服务端契约。
四、环境变量:配置输入,不是秘密存储
1. 环境变量的本质
环境变量是构建或启动工具时提供给进程的键值输入。例如:
VITE_API_BASE=/api npm run dev
在 Unix-like shell 中,这表示只对这次命令设置 VITE_API_BASE。
Vite 会将环境变量分为两类:
- Vite 内置变量:例如
import.meta.env.MODE、import.meta.env.DEV、import.meta.env.PROD、import.meta.env.BASE_URL; - 用户变量:默认只有以
VITE_开头的变量才会暴露给客户端代码。
客户端读取:
const apiBase = import.meta.env.VITE_API_BASE
这些值会被替换或注入到前端构建结果中。只要变量名以 VITE_ 开头,就应假设用户最终可以在浏览器开发者工具、静态资源或构建产物中看到它。
因此,下面的做法是错误的:
VITE_DATABASE_PASSWORD=secret
VITE_PRIVATE_SIGNING_KEY=secret
前端环境变量适合放:
VITE_API_BASE=/api
VITE_APP_TITLE=用户中心
不适合放数据库密码、JWT 签名密钥、云厂商私钥、内部管理接口凭证等秘密。秘密应保留在服务端或 CI/CD 的安全变量系统中。
2. .env 文件和 mode
Vite 支持按模式加载环境文件:
.env
.env.local
.env.development
.env.development.local
.env.production
.env.production.local
常见理解是:
.env:所有模式共享;.env.development:开发模式使用;.env.production:生产模式使用;.local:本机或部署环境专用,通常不提交到 Git。
可提交的示例文件可以命名为:
.env.example
内容:
VITE_API_BASE=/api
本地文件:
.env.local
内容:
VITE_API_BASE=http://localhost:8080/api
通常应将 .env.local 加入 .gitignore:
.env.local
.env.*.local
环境文件的最终值取决于模式、文件优先级和外部环境变量。已有的 shell 环境变量通常具有更高优先级,Vite 不会用环境文件覆盖已经存在的变量。因此,排查“明明改了 .env 却不生效”时,应同时检查:
printenv VITE_API_BASE
以及启动命令是否指定了其他 mode。
启动开发模式:
npm run dev -- --mode development
生产构建:
npm run build -- --mode production
-- 用于将参数继续传给底层 Vite 命令。开发服务器或构建进程启动时才读取环境配置;修改 .env 后通常需要重新启动进程。
3. 环境变量全部是字符串
例如:
VITE_ENABLE_DEBUG=false
VITE_PORT=3000
读取后仍然是字符串:
const enabled = import.meta.env.VITE_ENABLE_DEBUG
console.log(enabled === false) // false
console.log(enabled === 'false') // true
如果直接写:
if (import.meta.env.VITE_ENABLE_DEBUG) {
// ...
}
那么字符串 'false' 是真值,代码会错误地进入分支。
应显式解析:
function parseBoolean(value: string | undefined, fallback = false) {
if (value === undefined) {
return fallback
}
return value === 'true'
}
const debugEnabled = parseBoolean(import.meta.env.VITE_ENABLE_DEBUG)
数字也需要转换:
const timeoutMs = Number(import.meta.env.VITE_TIMEOUT_MS ?? 5000)
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
throw new Error('VITE_TIMEOUT_MS 必须是正数')
}
4. 在 vite.config.ts 中读取环境变量
配置文件本身运行在 Node.js 中,不能默认把客户端代码中的 import.meta.env 当作同一套运行环境使用。若配置需要根据 mode 变化,应使用 loadEnv:
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), 'VITE_')
return {
plugins: [vue()],
server: {
port: Number(env.VITE_DEV_PORT ?? 5173)
}
}
})
第三个参数 'VITE_' 表示只加载此前缀开头的变量。若配置确实需要读取没有此前缀的变量,可以传入空字符串:
const env = loadEnv(mode, process.cwd(), '')
但这样会把更多环境变量读入配置,容易误把服务端秘密混入配置处理流程。除非有明确理由,否则应限制前缀。
5. 不要用动态属性访问环境变量
这种写法通常不能被 Vite 的静态替换机制正确处理:
const key = 'VITE_API_BASE'
const apiBase = import.meta.env[key]
应直接访问:
const apiBase = import.meta.env.VITE_API_BASE
如果需要按名称选择配置,应先显式建立映射:
const config = {
apiBase: import.meta.env.VITE_API_BASE,
appTitle: import.meta.env.VITE_APP_TITLE
}
const value = config.apiBase
这是因为构建工具通常通过静态分析识别环境变量访问,而不是在浏览器运行时保留一个完整的环境变量对象。
五、为环境变量建立类型声明与运行时边界
TypeScript 默认不一定知道项目自定义环境变量。可以在 src/vite-env.d.ts 或其他被 tsconfig 包含的声明文件中扩展类型:
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_BASE: string
readonly VITE_APP_TITLE?: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
这个声明的作用是让编辑器和类型检查知道变量名称及类型,但它不会在运行时验证变量是否真的存在。
因此,类型声明:
readonly VITE_API_BASE: string
并不意味着部署时一定设置了 VITE_API_BASE。如果缺失,构建结果仍可能出现空值或未预期行为。可以在应用启动时验证:
function requireEnv(value: string | undefined, name: string): string {
if (!value) {
throw new Error(`缺少环境变量:${name}`)
}
return value
}
export const runtimeConfig = {
apiBase: requireEnv(import.meta.env.VITE_API_BASE, 'VITE_API_BASE')
}
完整的客户端请求封装示例:
import { runtimeConfig } from './config'
interface ApiErrorBody {
message?: string
}
export async function requestJson<T>(
path: string,
init?: RequestInit
): Promise<T> {
const response = await fetch(`${runtimeConfig.apiBase}${path}`, {
...init,
headers: {
Accept: 'application/json',
...init?.headers
}
})
if (!response.ok) {
let detail = ''
try {
const body = (await response.json()) as ApiErrorBody
detail = body.message ? `:${body.message}` : ''
} catch {
// 响应不是 JSON 时保留 HTTP 状态即可
}
throw new Error(`请求失败 ${response.status}${detail}`)
}
return (await response.json()) as T
}
这里的 T 只保证调用方对返回值的静态使用方式,不验证服务器返回的数据结构。如果后端契约不稳定,应在边界位置增加运行时校验,例如使用专门的 schema 校验库,或手写必要的字段检查。
六、Vite 开发代理:解决开发期跨域,不是生产网关
1. 为什么浏览器会遇到跨域
假设前端运行在:
http://localhost:5173
后端运行在:
http://localhost:8080
前端请求:
fetch('http://localhost:8080/users')
此时请求源的协议、主机或端口与页面不同,浏览器会执行同源策略检查。后端必须通过 CORS 响应头允许该来源,否则浏览器可能阻止前端读取响应。
开发代理的思路是:
浏览器 -> localhost:5173/api/users -> Vite -> localhost:8080/users
浏览器看到的请求仍然属于 localhost:5173,跨域请求由 Vite 在 Node.js 进程中代发。
2. 一个可运行的代理配置
vite.config.ts:
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), 'VITE_')
const backendUrl = env.VITE_DEV_BACKEND_URL ?? 'http://localhost:8080'
return {
plugins: [vue()],
server: {
port: 5173,
proxy: {
'/api': {
target: backendUrl,
changeOrigin: true,
secure: false
}
}
}
}
})
前端代码只请求相对路径:
const response = await fetch('/api/users')
如果后端实际接口是:
http://localhost:8080/api/users
则不需要重写路径。
如果后端实际接口是:
http://localhost:8080/users
则可以重写:
proxy: {
'/api': {
target: backendUrl,
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
此时:
/api/users
会被代理为:
/users
3. target、changeOrigin、secure 和 rewrite 的含义
target:代理要连接的后端地址;changeOrigin:是否修改转发请求中的Host等来源信息,某些后端虚拟主机配置需要它;secure:使用 HTTPS 目标时是否校验证书;设置为false可以绕过本地自签名证书校验,但不应把它当成生产安全方案;rewrite:修改转发路径,不会改变浏览器地址栏中的路径。
WebSocket 代理需要单独启用:
proxy: {
'/socket.io': {
target: 'ws://localhost:8080',
ws: true,
changeOrigin: true
}
}
4. 代理失败时如何诊断
假设浏览器请求 /api/users 失败,诊断顺序应区分不同故障层:
浏览器没有发出请求
检查:
- 是否触发了请求代码;
- 是否在请求前抛出了 JavaScript 异常;
- Network 面板中是否出现请求;
- 请求路径是否真的以
/api开头。
Vite 返回 404
可能是:
- 代理匹配前缀错误;
rewrite删除了后端实际需要的路径;- Vite 配置修改后没有重启开发服务器。
Vite 返回 502 或连接错误
可能是:
- 后端没有启动;
target主机或端口错误;- 容器环境中使用了错误的
localhost。
在容器中,localhost 通常指当前容器,而不是宿主机或另一个服务容器。此时应使用 Docker Compose 服务名或可达的网络地址。
后端返回 401、403 或 500
这通常说明代理已经成功连接后端,问题进入了认证、权限、请求格式或后端业务层。不要把所有代理错误都归因于 CORS。
5. 代理只解决开发阶段
server.proxy 只影响 Vite 开发服务器。执行:
npm run build
后,dist 中没有一个 Vite 代理进程会继续替你转发 /api。
生产环境必须由以下某个组件承担转发:
- Nginx 或 Apache;
- 云负载均衡器;
- CDN 边缘规则;
- Kubernetes Ingress;
- 独立后端网关;
- 应用平台提供的路由配置。
例如 Nginx 的概念配置:
location /api/ {
proxy_pass http://backend:8080/;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location / {
try_files $uri $uri/ /index.html;
}
这里有一个容易出错的细节:proxy_pass 末尾斜杠会影响 URI 拼接方式。实际部署前应使用具体请求验证:
curl -i https://example.com/api/users
同时检查后端收到的实际路径,而不能只根据配置文字推测。
七、生产构建:从源码到 dist
1. 构建命令和输出
执行:
npm run build
如果脚本是:
{
"scripts": {
"build": "vue-tsc -b && vite build"
}
}
则流程是:
vue-tsc -b检查 TypeScript 和 Vue 模板;- 检查失败时,后面的
vite build不会执行; - 类型检查成功后,Vite 解析模块、转换代码、处理资源并写入
dist。
成功时通常会输出类似:
✓ built in 2.31s
dist/index.html
dist/assets/index-xxxxx.js
dist/assets/index-xxxxx.css
文件名中的哈希通常由内容决定。内容变化后哈希变化,便于浏览器和 CDN 长期缓存静态资源。
构建完成后检查:
find dist -maxdepth 2 -type f
预览:
npm run preview
随后根据终端输出访问预览地址。预览服务验证的是 dist,不是开发服务器中的源码转换过程。
2. base 决定资源 URL 前缀
如果应用部署在域名根路径:
https://example.com/
通常使用:
export default defineConfig({
base: '/'
})
如果应用部署在子路径:
https://example.com/admin/
应配置:
export default defineConfig({
base: '/admin/'
})
或者构建时传入:
vite build --base=/admin/
base 会影响构建产物中脚本、样式和动态资源的引用路径。忘记设置时,常见表现是:
- 页面 HTML 可以打开;
- JavaScript 请求
/assets/...; - 实际资源位于
/admin/assets/...; - 浏览器得到 404,页面停留在空白状态。
配置后应检查:
grep -R "assets/" dist/index.html
并用部署后的真实子路径验证,而不是只在 / 根路径访问。
3. 静态资源的两种处理方式
通过模块导入的资源
<script setup lang="ts">
import logoUrl from './assets/logo.svg'
</script>
<template>
<img :src="logoUrl" alt="Logo" />
</template>
这类资源会进入 Vite 的资源处理流程,通常获得哈希文件名,并参与依赖关系分析。
public 目录中的资源
目录结构:
public/
favicon.svg
访问方式:
<link rel="icon" href="/favicon.svg" />
public 下的文件通常按原文件名直接复制到构建输出目录,不经过模块导入和哈希重命名。它适合必须保持固定 URL 的文件,例如:
favicon.ico;robots.txt;- 某些外部系统约定的固定文件名。
误区是把所有图片都放入 public。如果资源是应用模块的一部分,通过导入通常更容易获得正确的路径处理和缓存策略。
4. 构建时环境变量与运行时配置不是一回事
Vite 的环境变量默认在构建时进入前端资源。例如:
VITE_API_BASE=https://api.example.com npm run build
构建完成后,dist 中的 JavaScript 已经包含这个值。把同一个 dist 部署到测试、预发布和生产环境时,不能仅通过修改服务器进程环境变量改变已经构建好的值。
这形成两种交付模型:
每个环境分别构建
测试环境变量 -> 构建测试包
生产环境变量 -> 构建生产包
优点是简单、明确;风险是不同环境的构建产物可能不完全相同。
构建一次,运行时注入
例如让服务器返回一个固定路径的配置文件:
{
"apiBase": "/api",
"release": "2025-03-08"
}
应用启动时读取:
const response = await fetch('/runtime-config.json', {
cache: 'no-store'
})
const runtimeConfig = await response.json()
这种模型可以让同一份静态资源在多个环境复用,但增加了启动时序、缓存失效和配置加载失败处理。若配置加载失败,应用应该显示明确错误,而不是静默使用错误的默认地址。
八、SPA 路由与静态服务器回退
Vue Router 的 history 模式会产生如下前端路由:
/settings/profile
当用户从应用内部点击跳转时,浏览器通常已经加载了 index.html,Vue Router 在客户端处理路径。
但用户直接刷新 /settings/profile 时,服务器会把它当作静态文件路径。如果服务器没有该文件,可能返回 404。正确的服务器行为是:
- 真实存在的静态文件直接返回;
- 不存在但属于前端路由的路径返回
index.html; - API 路径
/api/*转发给后端,而不是回退到index.html。
Nginx 示例:
location /api/ {
proxy_pass http://backend:8080/;
}
location / {
try_files $uri $uri/ /index.html;
}
若把 /api 也回退到 index.html,前端收到的可能是 HTML,却按 JSON 解析,从而出现:
Unexpected token '<', "<!doctype "... is not valid JSON
这类错误的根因不是 JSON 库,而是静态服务器路由规则把 API 请求错误地交给了前端页面。
九、代码分割、动态导入与构建诊断
Vite 会处理静态导入:
import UserPage from './pages/UserPage.vue'
如果页面不需要在首屏加载,可以使用动态导入:
const UserPage = () => import('./pages/UserPage.vue')
Vue Router 中常见写法:
const routes = [
{
path: '/users',
component: () => import('./views/UsersView.vue')
}
]
动态导入通常形成独立的异步 chunk。请求链变为:
- 首屏加载入口 JavaScript;
- 用户进入
/users; - 浏览器请求对应异步 chunk;
- chunk 加载成功后实例化组件;
- 组件参与渲染。
因此,代码分割不是“文件越多越好”。如果把极小且总会使用的模块拆成大量 chunk,可能增加请求和加载调度;如果所有页面都静态导入,则首屏 JavaScript 可能过大。应结合真实入口、网络缓存和页面访问路径检查构建结果。
常见失败表现及诊断方法:
构建成功但页面空白
检查:
npm run preview
然后查看浏览器 Console 和 Network:
- JavaScript 是否 404;
base是否正确;- 是否发生运行时异常;
- 是否有动态 chunk 加载失败;
- 是否把生产 API 地址配置错误。
构建后请求仍指向开发地址
检查:
grep -R "localhost:8080" dist
如果能搜到开发地址,通常说明:
- 使用了错误的 mode;
.env.production没有被加载;- shell 中已有同名变量覆盖了文件配置;
- 配置在构建后无法再改变。
类型检查通过但运行时数据错误
TypeScript 只检查静态代码和声明,不会自动验证网络响应。例如:
interface User {
id: number
name: string
}
const user = await requestJson<User>('/users/1')
如果后端返回:
{
"id": "not-a-number",
"name": null
}
TypeScript 不会在浏览器运行时自动抛错。API 契约需要服务端规范、生成代码、运行时 schema 校验或组合使用。组件类型、模板检查和 API 契约应作为独立的类型边界治理,而不是把接口声明当作运行时验证。
十、vite.config.ts 的配置边界
一个基础配置可以写成:
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), 'VITE_')
return {
base: env.VITE_BASE_PATH ?? '/',
plugins: [vue()],
server: {
host: '127.0.0.1',
port: Number(env.VITE_DEV_PORT ?? 5173),
strictPort: false
},
preview: {
port: Number(env.VITE_PREVIEW_PORT ?? 4173)
},
build: {
outDir: 'dist',
sourcemap: false
}
}
})
配置项的作用:
plugins: [vue()]:让 Vite 识别并转换 Vue 单文件组件;base:设置部署路径前缀;server:配置开发服务器;preview:配置本地构建预览服务器;build.outDir:设置构建输出目录;build.sourcemap:决定是否生成 source map。
source map 便于定位压缩代码对应的源码,但也可能暴露源码结构。是否在生产生成,应根据错误监控系统、访问控制和组织安全要求决定。即使生成 source map,也不应把秘密写进源码,因为 source map 可能被下载或上传到第三方监控平台。
配置文件中的 server 与 preview 不是同一个服务器:
server只影响vite开发服务器;preview只影响vite preview;- 生产服务器的端口、压缩、TLS、缓存和代理由部署平台配置。
十一、依赖治理:从“能安装”到“可复现、可升级、可恢复”
1. package.json 与锁文件的关系
package.json 声明项目直接依赖,例如:
{
"dependencies": {
"vue": "^3.5.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.0",
"vite": "^7.0.0",
"typescript": "~5.8.0",
"vue-tsc": "^2.2.0"
}
}
这里的版本符号表示允许的版本范围:
^3.5.0:通常允许更新到小于4.0.0的兼容版本;~5.8.0:通常允许更新到小于5.9.0的补丁版本;- 固定版本:只允许该精确版本。
实际安装的完整依赖树由锁文件记录,例如 npm 的:
package-lock.json
锁文件不仅记录直接依赖,也记录间接依赖的具体版本、解析地址和完整性校验值。提交锁文件的意义是让开发机、CI 和部署环境尽量安装相同的依赖树。
CI 中应优先使用:
npm ci
而不是:
npm install
npm ci 通常要求锁文件与 package.json 一致,并按锁文件执行干净安装。若两者不一致,它会失败,而不是悄悄修改锁文件。这种失败是有价值的,因为它阻止了“本地能跑、CI 安装出另一棵依赖树”的情况。
2. dependencies 和 devDependencies 的判断标准
判断依据不是“开发者是否使用”,而是生产运行时是否需要该包。
对于典型的纯前端 Vue 项目:
- Vue 运行在浏览器中,通常放在
dependencies; - Vite、Vue 插件、TypeScript、
vue-tsc只参与开发和构建,通常放在devDependencies。
但如果项目同时包含一个 Node.js 服务端,服务端运行时需要的包必须放在 dependencies。不能因为部署脚本只执行了 npm install --production,就把服务端运行依赖误放到 devDependencies。
前端最终打包后,部分 dependencies 也会被内联进静态资源。dependencies 的分类仍然有助于表达项目生命周期和部署要求,但并不意味着每个依赖都会作为独立文件出现在服务器上。
3. 添加、升级和移除依赖
添加依赖:
npm install vue-router
添加开发依赖:
npm install --save-dev eslint
查看依赖树:
npm ls --depth=0
npm ls vite
检查过时依赖:
npm outdated
升级时不要直接把所有包一次性升级到最新版本。更可控的流程是:
- 记录当前分支和锁文件;
- 选择一个相关依赖或一个兼容升级组;
- 修改版本范围并重新安装;
- 执行类型检查、单元测试和生产构建;
- 检查构建产物、代理行为和关键页面;
- 提交
package.json与锁文件。
例如升级 Vite 后,应重点检查:
- Node.js 最低版本要求;
- 插件兼容性;
- 配置选项是否发生弃用或行为变化;
- 构建输出和资源路径;
- CI 是否仍能执行
npm ci。
4. peerDependencies 冲突不能简单忽略
某些库通过 peerDependencies 声明兼容的 Vue、TypeScript 或框架版本。如果出现:
ERESOLVE unable to resolve dependency tree
应先阅读冲突链:
npm ls
npm explain <package-name>
不要默认使用:
npm install --legacy-peer-deps
这个选项可能绕过依赖约束,使项目进入“安装成功但运行时不兼容”的状态。只有在确认依赖实际兼容、且团队明确接受该策略时,才应将其作为临时方案,并记录原因。
5. overrides 用于控制间接依赖
当间接依赖存在已知问题,而上游尚未发布可用版本时,npm 支持通过 overrides 约束解析结果:
{
"overrides": {
"some-transitive-package": "1.2.3"
}
}
这不是无条件修复。强制替换间接依赖可能破坏上层包的 API 或类型假设。使用后应:
npm install
npm ls some-transitive-package
npm run typecheck
npm run build
并记录:
- 为什么需要覆盖;
- 哪个上层依赖引入了它;
- 何时可以移除覆盖;
- 是否存在上游修复版本。
6. 安全审计的边界
可以执行:
npm audit
它能根据公开漏洞数据库报告部分依赖风险,但不能证明项目安全,也不能证明升级后行为正确。
npm audit fix
可能修改版本范围和锁文件,甚至触发主版本升级。执行前应保存变更并阅读 diff:
git diff -- package.json package-lock.json
依赖安全治理至少要区分:
- 直接依赖还是间接依赖;
- 生产代码是否实际使用受影响路径;
- 漏洞是否适用于当前构建和部署方式;
- 自动修复是否引入破坏性升级;
- 是否需要等待上游发布修复。
十二、常见错误的因果分析
错误一:把 process.env 用在浏览器代码中
错误示例:
const apiBase = process.env.API_BASE
Vite 浏览器代码的常规访问方式是:
const apiBase = import.meta.env.VITE_API_BASE
如果项目确实需要兼容某些旧代码,可以通过构建配置显式定义替换,但这不是把完整 Node.js process.env 暴露给浏览器的理由。客户端不存在可安全读取的服务器环境变量集合。
错误二:修改 .env 后页面仍使用旧值
可能原因:
- 没有重启 Vite;
- 使用了错误的 mode;
- 外部 shell 变量覆盖了文件变量;
- 读取的变量没有
VITE_前缀; - 访问方式使用了动态属性;
- 实际运行的是已经构建好的旧
dist。
诊断时可以在应用入口临时输出非敏感变量:
console.log({
mode: import.meta.env.MODE,
dev: import.meta.env.DEV,
apiBase: import.meta.env.VITE_API_BASE
})
不要为了调试把秘密输出到浏览器 Console,因为只要进入客户端,它就已经不是秘密。
错误三:认为开发代理配置会随构建产物部署
错误理解:
配置了 server.proxy,所以生产请求 /api 也会自动转发。
实际情况是:
server.proxy -> 只存在于 Vite 开发服务器
dist -> 只有静态文件
生产代理 -> 需要 Nginx、网关、CDN 或平台配置
验证方法是用生产域名执行:
curl -i https://example.com/api/health
并确认响应来自后端,而不是 index.html。
错误四:把 vite build 当作完整质量检查
vite build 主要验证构建流程能否生成产物。它不自动覆盖:
- Vue 模板的全部类型错误;
- 单元测试;
- 端到端交互;
- API 返回数据结构;
- 生产网关路径;
- 运行时环境变量;
- 浏览器兼容性和权限策略。
一个更清晰的脚本组织方式是:
{
"scripts": {
"typecheck": "vue-tsc --noEmit",
"test": "vitest run",
"build": "npm run typecheck && npm run test && vite build"
}
}
具体测试工具和脚本名称取决于项目是否安装并配置了对应依赖。不要在未安装工具时直接复制脚本。
错误五:删除锁文件来“解决安装问题”
删除锁文件可能暂时绕过某些解析结果,但它也会重新计算整棵依赖树,导致大量间接依赖变化。正确做法通常是:
- 保存当前锁文件;
- 读取 npm 的冲突信息;
- 确认
package.json与锁文件是否同步; - 只升级相关依赖;
- 在干净环境执行
npm ci验证; - 失败时恢复锁文件,而不是继续叠加修改。
如果怀疑安装缓存损坏,可以在确认锁文件正确后重新安装,但缓存清理不是解决版本冲突的通用方法。
十三、从创建到交付的一套可验证流程
下面是一套适合新项目和 CI 的基础流程:
# 1. 检查 Node.js
node --version
# 2. 安装锁定的依赖
npm ci
# 3. 执行 Vue + TypeScript 类型检查
npm run typecheck
# 4. 构建生产资源
npm run build
# 5. 本地预览构建结果
npm run preview
每一步的验证目标不同:
| 步骤 | 验证对象 | 失败说明 |
|---|---|---|
node --version |
工具链运行时 | Node.js 版本可能不满足要求 |
npm ci |
锁文件和依赖树 | package.json、锁文件或 peer 依赖存在问题 |
npm run typecheck |
Vue、TypeScript、模板类型 | 静态类型或模板使用不一致 |
npm run build |
生产构建流程 | 配置、模块、资源或插件处理失败 |
npm run preview |
dist 的可访问性 |
构建后路径、资源或运行时逻辑可能有问题 |
然后验证 API:
curl -i http://localhost:5173/api/health
验证生产部署:
curl -i https://example.com/
curl -i https://example.com/assets/
curl -i https://example.com/api/health
第二条命令中的具体路径应替换为实际构建生成的资源文件。核心不是请求一个不存在的 /assets/ 目录,而是确认:
- HTML 能返回;
- HTML 引用的 JavaScript 和 CSS 能返回;
- API 路径被转发到后端;
- 前端路由刷新不会返回 404;
- 错误响应不会被错误地替换成
index.html。
十四、依赖和构建问题的恢复策略
构建失败
先保留完整日志:
npm run build 2>&1 | tee build.log
然后区分:
- 类型错误:查看
vue-tsc报告的文件和模板位置; - 模块解析错误:检查导入路径、大小写和依赖是否安装;
- 插件错误:检查 Vite、Vue 插件和 Node.js 版本;
- 环境变量错误:检查 mode、变量前缀和 shell 覆盖;
- 资源路径错误:检查
base和部署子路径。
依赖安装失败
先检查:
npm ci
npm ls
npm explain <package-name>
如果最近刚升级依赖,可以比较:
git diff HEAD~1 -- package.json package-lock.json
确认修改范围后,优先恢复到最近一次可用锁文件,再单独处理目标依赖。不要让一次依赖升级同时混入大量无关格式化或源代码修改,否则难以定位回归原因。
生产页面白屏
可以按以下路径检查:
- HTML 是否返回
200; - HTML 中引用的资源 URL 是否包含正确的
base; - JavaScript、CSS、动态 chunk 是否返回
200; - 浏览器 Console 是否有运行时异常;
- API 请求是否打到了正确的生产地址;
/api是否被静态服务器错误回退为index.html;- 是否有旧 HTML 引用了已被清理的旧哈希资源。
如果采用 CDN 或强缓存,HTML 和带哈希的静态资源应采用不同缓存策略:HTML 通常需要较短缓存或重新验证,带内容哈希的静态资源可以长期缓存。具体策略属于部署系统配置,不由 Vite 单独决定。
十五、规范保证、常见实现与工程取舍
需要区分三类结论:
规范或工具明确保证的行为
- Vite 支持通过
import.meta.env访问内置环境信息; - 默认只有特定前缀的用户环境变量会暴露给客户端;
vite build生成静态构建产物;server.proxy配置开发服务器代理;- TypeScript 类型声明不会自动产生运行时校验。
常见实现,但不应当成绝对保证
- 开发服务器默认端口通常是
5173; - 构建文件通常位于
dist; - 静态资源通常带内容哈希;
- Vite 开发阶段通常按请求提供模块并进行转换。
这些行为可能通过配置或版本变化而改变,应以项目实际输出为准。
需要根据项目选择的经验方案
- 是否启用生产 source map;
- 是否每个环境分别构建;
- 是否使用运行时配置;
- 是否将 API 统一为
/api前缀; - 是否允许依赖自动升级;
- 是否把所有升级拆成单独变更;
- 是否在构建前执行测试和类型检查。
真正稳定的 Vue + Vite 工具链,不是配置文件越复杂越好,而是每个边界都能被解释和验证:
- Node.js 负责运行工具;
- npm 和锁文件负责复现依赖;
- Vite 负责开发转换和生产构建;
- Vue 负责组件运行时与响应式更新;
import.meta.env提供受控的构建配置输入;- 开发代理只承担本地转发;
- 生产网关负责真实部署路径和 API 路由;
vue-tsc、测试和运行时校验分别承担不同层次的正确性检查。
系列导航与关联阅读
- 系列入口:Vue 完整学习路线:从响应式与组件到工程化、SSR 和生产交付
- 下一篇:Vue 模板与渲染:指令、表达式、列表、条件和 Virtual DOM
- 延伸:Vue 与 TypeScript:组件类型、泛型、模板检查和 API 契约
- 延伸:Vue 生产交付:环境配置、静态资源、缓存、灰度和回滚
官方资料
本文依据 Vue、Vite 与生态项目官方文档重新梳理;正文与示例由 WR BLOG 编写。

评论
0 条讨论