Vue 基础体系 · 第 63/70 篇。示例基于 Vue 3、Composition API、TypeScript 与现代 Vite 工具链;版本敏感能力会单独标注。
Vue CI 质量流水线:类型、Lint、测试、构建、预览和制品
持续集成(Continuous Integration,CI)是指每次提交或合并请求进入共享分支时,由自动化环境重新安装依赖、执行质量检查并产出可验证结果。对 Vue 3 + TypeScript + Vite 项目而言,CI 不是把若干命令串起来,而是要回答五个不同问题:
- 类型检查:代码是否满足 TypeScript 和 Vue 单文件组件的静态类型约束?
- Lint:代码是否违反可自动检查的风格、语法和部分错误规则?
- 测试:在给定输入和环境下,行为是否符合预期?
- 构建:源代码、依赖和配置能否生成浏览器可加载的生产资源?
- 预览与制品:生成的资源是否能被启动和访问,并能被可靠保存、传递或部署?
可以把一个提交是否通过基础质量门禁形式化为:
其中:
- 是某次提交;
- 是类型检查;
- 是 Lint;
- 是测试;
- 是构建;
- 是对构建结果的预览或冒烟验证;
true表示对应步骤以退出码0完成。
这个条件是必要条件的合取,不是质量的充分条件:所有检查通过,仍不能证明没有需求遗漏、性能问题或未覆盖的真实浏览器差异。
一、先明确流水线中的对象和边界
1. 源代码、构建结果、制品和缓存不是同一个东西
Vue 项目通常包含四类容易混淆的目录或数据:
- 源代码:
src/、public/、index.html、配置文件和测试文件; - 依赖安装结果:
node_modules/,由package-lock.json等锁文件决定; - 构建结果:Vite 默认生成的
dist/; - CI 制品(artifact):由 CI 平台保存并供下载、部署或后续任务使用的文件归档。
dist/ 是构建工具在工作区中产生的目录;artifact 是 CI 平台对某些文件进行上传、保存和命名后的结果。一个 dist/ 可以被上传为 artifact,但两者不是同一个概念。
缓存也不同:
- 缓存用于加速,例如缓存 npm 下载内容;
- 制品用于保存结果,例如保存本次构建的
dist/。
缓存可以失效、被淘汰或重新生成;部署不应依赖缓存。部署应依赖可追踪的构建制品。
2. CI 的输入必须尽量确定
同一提交若在不同 Node.js 版本、不同依赖版本下产生不同结果,流水线就难以诊断。因此基础输入应包括:
- 固定 Node.js 主版本或项目要求的精确版本范围;
- 提交锁文件;
- 使用
npm ci而不是根据package.json重新解析依赖; - 明确环境变量;
- 不把开发者本机生成的
dist/当作 CI 输入。
npm ci 要求锁文件与 package.json 一致,并依据锁文件安装依赖。若两者不一致,命令应失败;这比 CI 隐式修改锁文件更容易发现依赖漂移。
二、一个可运行的 Vue 3 + TypeScript + Vite 基线
下面假设项目已经由现代 Vite 模板创建,使用 Vue 3、Composition API 和 TypeScript。典型目录如下:
my-vue-app/
├─ src/
│ ├─ App.vue
│ └─ main.ts
├─ tests/
│ └─ unit/
├─ index.html
├─ package.json
├─ package-lock.json
├─ tsconfig.json
├─ tsconfig.app.json
├─ vite.config.ts
└─ eslint.config.js
一个可执行的脚本配置可以是:
{
"scripts": {
"dev": "vite",
"typecheck": "vue-tsc --noEmit",
"lint": "eslint .",
"test": "vitest run",
"test:watch": "vitest",
"build": "vite build",
"preview": "vite preview"
}
}
这些脚本分别调用不同工具:
vue-tsc --noEmit:检查.vue单文件组件中的模板和脚本类型,但不输出 JavaScript;eslint .:遍历项目中的可检查文件并报告规则违规;vitest run:执行一次测试后退出,适合 CI;vite build:生成生产构建;vite preview:启动一个本地服务器,服务已经生成的构建目录。
这里的 npm run 只是 npm 对脚本的封装。npm run preview -- --host 127.0.0.1 中第二个 -- 之后的参数会传给 vite preview。
三、类型检查:检查“能否被类型系统证明”
3.1 TypeScript 检查的对象
TypeScript 类型检查关注的是静态类型关系。例如:
function formatPrice(value: number): string {
return `¥${value.toFixed(2)}`
}
formatPrice('12')
这里参数要求 number,调用处传入了 string,类型检查应报告错误。它通常不会执行函数,也不会验证按钮点击后页面是否真的显示了正确文字。
Vue 项目不能只运行普通的 tsc 就认为 .vue 文件被完整检查。.vue 文件包含模板、脚本和样式,Vue 官方工具链通常使用 vue-tsc 处理带有 Vue 语言服务的类型检查:
npm run typecheck
--noEmit 表示只检查,不写出编译产物。其成功条件不是“所有运行时行为正确”,而是“参与类型检查的代码满足当前 TypeScript、Vue 类型声明和配置”。
3.2 一个完整的类型错误传播过程
例如组件:
<script setup lang="ts">
const props = defineProps<{
count: number
}>()
const label = props.count.toUpperCase()
</script>
<template>
<span>{{ label }}</span>
</template>
检查过程可以拆成:
defineProps声明count的类型为number;props.count因此被推导为number;toUpperCase()是字符串方法,不是number的方法;vue-tsc在模板编译和类型分析阶段报告错误;npm run typecheck以非零退出码结束;- CI 任务失败,后续依赖该任务的质量门禁不应继续通过。
修正为:
<script setup lang="ts">
const props = defineProps<{
count: number
}>()
const label = String(props.count)
</script>
<template>
<span>{{ label }}</span>
</template>
这里不是“把错误隐藏掉”,而是明确把数值转换为字符串。若业务要求价格格式化,还应使用具有业务语义的格式化函数,而不是任意转换。
3.3 类型检查的边界
类型检查无法保证:
- API 在运行时一定返回声明的数据;
- 用户一定传入合法文本;
- DOM 查询一定找到元素;
- CSS 布局在浏览器中没有溢出;
- 异步请求一定成功;
- 测试覆盖了关键分支。
例如:
type User = {
name: string
}
const user = JSON.parse('{"name": 42}') as User
console.log(user.name.toUpperCase())
as User 是编译期断言,不会在运行时验证 JSON。类型检查可能通过,但实际调用 toUpperCase() 会失败。边界输入需要运行时校验、测试或接口契约,而不是仅依赖 TypeScript 断言。
四、Lint:检查“代码是否违反规则”
4.1 Lint 与类型检查不是同一层
Lint 是静态代码分析。它可以检查:
- 未使用的变量;
- 不允许的语法模式;
- 不一致的导入方式;
- Vue 模板中的特定问题;
- 违反团队约定的代码结构;
- 某些可静态发现的潜在错误。
例如:
const unusedValue = 1
export function add(a: number, b: number) {
return a + b
}
如果启用了未使用变量规则,ESLint 可以报告 unusedValue。但它通常不会证明 add(1, 2) 的业务结果符合产品需求。
反过来,类型检查也不负责所有格式和规则问题。下表描述常见职责:
| 问题 | 类型检查 | Lint |
|---|---|---|
string 传给 number 参数 |
是 | 可能不检查 |
| 未使用变量 | 可能由编译选项检查 | 是 |
| 禁止某种导入方式 | 否 | 是 |
| 模板中缺少可访问性属性 | 否 | 可由 Vue 插件检查 |
| 运行时 API 返回错误数据 | 否 | 否 |
| 页面行为是否正确 | 否 | 否 |
4.2 ESLint 配置必须匹配项目文件
现代 ESLint 常见配置形式是根目录的 eslint.config.js 或 eslint.config.mjs。具体配置取决于 ESLint、Vue ESLint 插件和 TypeScript ESLint 的版本,不能把不同代际的配置格式混用。
一个简化示例:
// eslint.config.js
import js from '@eslint/js'
import tseslint from 'typescript-eslint'
import vue from 'eslint-plugin-vue'
export default tseslint.config(
{
ignores: ['dist/', 'coverage/'],
},
js.configs.recommended,
...tseslint.configs.recommended,
...vue.configs['flat/recommended'],
)
这个示例表达了三层规则来源:
- ESLint 官方 JavaScript 推荐规则;
- TypeScript 语法和类型相关规则;
- Vue 单文件组件规则;
- 忽略构建和覆盖率输出,避免把生成文件当作源代码分析。
实际项目还需要依据安装的插件版本核对配置导出名称。配置格式是版本敏感内容:ESLint flat config、Vue ESLint 插件和 TypeScript ESLint 的组合必须使用相互兼容的版本。
执行:
npm run lint
若希望 CI 修复文件,不建议直接使用自动修复后默默继续构建。更安全的流程是开发者本地执行:
npx eslint . --fix
然后审查修改并提交。CI 主要负责判断提交是否合格,而不是替开发者修改工作区。
4.3 Lint 通过不等于类型通过
下面代码可能没有明显的格式问题:
function renderName(name: string) {
return name.toUpperCase()
}
renderName(100)
若规则集没有执行完整类型感知分析,Lint 可能通过;vue-tsc 则应发现调用参数错误。因此将 lint 和 typecheck 合并成一个“静态检查”概念,会削弱失败诊断能力。
五、测试:检查“在指定场景中行为是否正确”
5.1 测试的输入、执行和判定
以 Vitest 为例,测试至少包含:
- 输入:函数参数、组件 props、用户事件或模拟请求;
- 执行:调用函数、挂载组件、触发事件;
- 断言:比较实际结果与预期结果;
- 退出状态:所有断言通过则通常为
0,任一断言失败则为非零。
一个纯函数测试:
// src/formatPrice.ts
export function formatPrice(value: number): string {
return `¥${value.toFixed(2)}`
}
// tests/unit/formatPrice.test.ts
import { describe, expect, it } from 'vitest'
import { formatPrice } from '../../src/formatPrice'
describe('formatPrice', () => {
it('保留两位小数并添加货币符号', () => {
expect(formatPrice(12)).toBe('¥12.00')
})
it('处理带小数的价格', () => {
expect(formatPrice(12.5)).toBe('¥12.50')
})
})
运行:
npm run test
预期结果是 Vitest 输出两个测试通过,并以退出码 0 结束。CI 使用 vitest run 而不是 vitest,因为后者通常进入监听模式,不会自然结束。
5.2 Vue 组件测试必须验证可观察行为
组件测试不应只测试内部变量名。更有价值的是验证用户可以观察到的结果:
<!-- src/components/CounterButton.vue -->
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button type="button" @click="count++">
Count: {{ count }}
</button>
</template>
使用 Vue Test Utils 时,测试重点是挂载、查找按钮、触发点击和断言文本:
import { describe, expect, it } from 'vitest'
import { mount } from '@vue/test-utils'
import CounterButton from '../../src/components/CounterButton.vue'
describe('CounterButton', () => {
it('点击后显示递增的计数', async () => {
const wrapper = mount(CounterButton)
expect(wrapper.text()).toContain('Count: 0')
await wrapper.get('button').trigger('click')
expect(wrapper.text()).toContain('Count: 1')
})
})
这里 await 很重要:事件触发可能引起 Vue 的异步更新,等待完成后再读取 DOM,才能断言更新后的状态。
5.3 测试失败、类型失败和构建失败的诊断路径不同
typecheck失败:先看文件、行列号和推导类型;lint失败:看规则名,判断是代码错误还是规则配置不适用;test失败:看失败测试、实际值、期望值和测试环境;build失败:看模块解析、环境变量、插件转换和资源处理错误;preview失败:看端口、服务器日志和构建目录内容。
如果先运行测试,测试环境可能掩盖了生产构建问题;如果只运行构建,行为回归又可能未被捕获。因此这些步骤应保持独立的退出状态。
六、构建:把源代码转换为可部署资源
6.1 Vite 构建做了什么
Vite 的开发服务器和生产构建是两个不同场景:
- 开发服务器强调快速启动和按需模块处理;
- 生产构建通过 Rollup 等构建能力处理模块、资源、代码分割和压缩等任务;
- 构建结果通常写入
dist/,除非配置修改了输出目录。
执行:
npm run build
可以抽象为:
src/*.vue、src/*.ts、依赖、配置
│
▼
Vite 解析模块与插件
│
▼
转换、打包、资源处理、生成 HTML
│
▼
dist/index.html、dist/assets/*
构建成功的含义是:在当前 Node.js、依赖、配置和环境变量下,Vite 能生成资源。它不等于资源已经部署,也不等于所有浏览器都能正常执行。
6.2 构建阶段与运行阶段的环境变量
Vite 会处理以 VITE_ 开头的客户端环境变量。例如:
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL
在构建时,Vite 将对应值替换或注入到客户端资源中。因为客户端资源最终会发送给浏览器,所以不能把数据库密码、私钥或服务端令牌放入 VITE_* 变量。
这产生一个常见因果关系:
- CI 设置
VITE_API_BASE_URL; npm run build读取该值;- 该值进入
dist/中的 JavaScript; - 上传 artifact 后,任何能下载 artifact 的人可能看到该值;
- 因此
VITE_*只能存放可公开的客户端配置。
如果构建后修改环境变量,通常不能改变已经被静态替换进资源的值;要实现部署时配置,需要额外设计运行时配置文件或服务端注入机制,而不是假设 Vite 会自动重新读取变量。
6.3 构建前应清理旧输出
Vite 的具体清理行为受配置和版本影响。为了避免旧文件造成误判,可在构建前显式清理:
{
"scripts": {
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
"build": "npm run clean && vite build"
}
}
这段命令依赖 Node.js 的文件系统 API。若项目需要兼容更旧 Node.js 版本,应确认 rmSync 的可用性,或使用项目已有的跨平台清理工具。不要在 Windows、macOS 和 Linux 之间直接假设 rm -rf 都可用。
七、预览:验证“刚生成的构建能否被启动和访问”
7.1 vite preview 的准确含义
vite preview 启动一个本地静态服务器,服务已经存在的生产构建目录。典型流程是:
npm run build
npm run preview -- --host 127.0.0.1 --port 4173
如果 dist/index.html 存在,服务器启动后可以访问:
curl -I http://127.0.0.1:4173/
预期会获得成功的 HTTP 响应头,通常包括 200 状态。实际响应头会受 Vite 版本和配置影响,不能把具体的完整头集合当作稳定规范。
vite preview 不是生产级静态服务器,也不是部署命令。它适合:
- 本地查看生产构建;
- CI 中做基本 HTTP 冒烟检查;
- 验证
dist/是否可被一个静态服务器读取。
生产环境可以使用 CDN、Nginx、对象存储或云平台提供的静态托管能力,并需要额外处理 HTTPS、缓存、压缩、SPA 回退和安全响应头。
7.2 CI 中启动和停止预览服务器
一个可执行的 shell 片段如下:
npm run build
npm run preview -- --host 127.0.0.1 --port 4173 > preview.log 2>&1 &
PREVIEW_PID=$!
cleanup() {
kill "$PREVIEW_PID" 2>/dev/null || true
}
trap cleanup EXIT
for i in $(seq 1 30); do
if curl --fail --silent --show-error \
http://127.0.0.1:4173/ > /dev/null; then
break
fi
if ! kill -0 "$PREVIEW_PID" 2>/dev/null; then
cat preview.log
exit 1
fi
sleep 1
done
curl --fail --silent --show-error \
http://127.0.0.1:4173/ > /dev/null
逐步分析:
npm run build先生成dist/,没有构建结果就没有可预览对象;&让预览服务器在后台运行;- 保存进程号,便于任务结束时停止进程;
trap cleanup EXIT确保成功或失败都尝试清理;- 循环等待服务器就绪,避免刚启动就请求导致偶发失败;
- 如果进程已经退出,则输出日志并失败;
curl --fail让 HTTP 4xx/5xx 转为非零退出码;- 最后的请求是独立的成功确认。
循环中的 30 和 1 是经验参数,不是 Vite 的规范保证。网络、CI 虚拟机负载和项目启动时间变化时,应根据实际日志调整。
7.3 HTTP 200 仍不能证明页面正确
curl / 只能证明服务器返回了入口资源。例如,以下问题可能仍然存在:
- JavaScript 在浏览器中抛出异常;
- 路由刷新时服务器没有返回
index.html; - API 地址配置错误;
- 页面需要真实浏览器 API;
- CSS 或异步模块加载失败。
若需要验证浏览器行为,应在预览服务器之上增加 Playwright 等端到端测试。一个合理的分层是:
curl / 检查服务器和入口文件
浏览器加载首页 检查脚本执行和资源加载
端到端交互测试 检查路由、点击、表单和网络行为
真实部署环境测试 检查 CDN、HTTPS、回退和运行时配置
每一层覆盖的故障不同,不能用低层检查替代高层检查。
八、制品:把可部署结果交给后续流程
8.1 制品的生命周期
一个典型制品生命周期是:
flowchart LR
A[提交 Commit] --> B[安装锁定依赖]
B --> C[类型检查]
C --> D[Lint]
D --> E[单元测试]
E --> F[Vite 构建]
F --> G[预览与冒烟检查]
G --> H[上传 dist 制品]
H --> I[下载制品]
I --> J[部署或发布]
关键点在于:部署任务应下载并部署已经验证过的制品,而不是再次从源码构建一份“可能不同”的结果。
如果构建任务和部署任务分别执行:
构建任务:源码 A + Node 版本 X + 环境变量 Y → dist-1
部署任务:源码 A + Node 版本 Z + 环境变量 Y → dist-2
即使提交相同,依赖解析、工具版本或环境差异也可能使 dist-1 和 dist-2 不同。复用 dist-1 可以缩小不确定性。
8.2 制品命名和追踪
制品至少应能关联到:
- 提交 SHA;
- 分支或合并请求;
- 构建运行编号;
- 构建所使用的 Node.js 和依赖锁文件。
CI 平台通常会为 artifact 生成内部标识,但工程上仍应使用清晰名称,例如:
vue-app-dist-${commit-sha}
不要把包含密钥、.env 私密文件或整个工作区直接上传。对于静态站点,通常只上传 dist/;源代码和依赖可以由仓库和锁文件恢复。
九、一个完整的 GitHub Actions 示例
下面的工作流使用 Ubuntu runner、Node.js 22、npm 锁文件和 GitHub artifact。Node.js 版本只是示例;项目应根据 engines、部署平台和依赖支持范围选择,并在升级时验证。
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
quality:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci
- name: Type check
run: npm run typecheck
- name: Lint
run: npm run lint
- name: Unit tests
run: npm run test
- name: Build
run: npm run build
env:
VITE_API_BASE_URL: https://api.example.com
- name: Preview smoke check
shell: bash
run: |
npm run preview -- --host 127.0.0.1 --port 4173 > preview.log 2>&1 &
PREVIEW_PID=$!
cleanup() {
kill "$PREVIEW_PID" 2>/dev/null || true
}
trap cleanup EXIT
ready=0
for i in $(seq 1 30); do
if curl --fail --silent --show-error \
http://127.0.0.1:4173/ > /dev/null; then
ready=1
break
fi
if ! kill -0 "$PREVIEW_PID" 2>/dev/null; then
cat preview.log
exit 1
fi
sleep 1
done
if [ "$ready" -ne 1 ]; then
cat preview.log
exit 1
fi
curl --fail --silent --show-error \
http://127.0.0.1:4173/ > /dev/null
- name: Upload preview log on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: preview-log-${{ github.sha }}
path: preview.log
if-no-files-found: warn
- name: Upload production artifact
uses: actions/upload-artifact@v4
with:
name: dist-${{ github.sha }}
path: dist
if-no-files-found: error
9.1 这个顺序为什么成立
顺序不是唯一方案,但它体现了依赖关系:
checkout提供源代码和锁文件;setup-node提供规定的 Node.js,并配置 npm 缓存;npm ci提供可复现的依赖环境;- 类型检查和 Lint 不需要构建结果;
- 单元测试验证代码行为;
- 构建需要前面安装的依赖和构建时环境变量;
- 预览依赖
dist/,所以必须在构建后执行; - 只有构建和预览成功,才上传生产制品。
可以并行化类型检查、Lint 和测试以缩短时间,但并行只改变调度方式,不改变它们各自必须成功的条件。构建任务通常依赖安装步骤,却不一定依赖三项检查的执行结果;是否等全部质量检查通过后再构建,取决于团队对反馈速度和资源消耗的取舍。
9.2 缓存的正确使用
actions/setup-node 的 npm 缓存通常缓存 npm 下载内容,不是 node_modules/,也不是 dist/。缓存命中可以减少下载时间,但 npm ci 仍会按照锁文件重新准备安装结果。
缓存失效时,正确行为是重新下载并继续执行。若流水线只能在缓存命中时工作,说明把缓存误当成了依赖输入。
9.3 并发取消的风险
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
这表示同一工作流和同一引用上,新运行可以取消旧运行,减少重复消耗。对于 main 上的发布流程要谨慎:如果旧任务已经完成构建但尚未部署,取消策略可能影响发布顺序。质量检查可以激进取消,生产部署通常应使用更严格的串行和审批策略。
十、生产交付时如何复用制品
质量任务上传 dist 后,部署任务可以下载 artifact:
deploy:
needs: quality
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- name: Download production artifact
uses: actions/download-artifact@v4
with:
name: dist-${{ github.sha }}
path: dist
- name: Inspect artifact
run: |
test -f dist/index.html
find dist -maxdepth 2 -type f -print
这里的 needs: quality 表示部署任务依赖质量任务成功。test -f 是一个最小制品完整性检查:如果入口文件不存在,部署不应继续。
实际部署到静态托管平台时,还要确认:
- SPA 路由是否配置了未知路径回退到
index.html; - 资源路径是否与
base配置一致; - API 地址是否对应目标环境;
- CDN 是否正确处理旧 HTML 和新资源的缓存关系;
- artifact 下载内容是否确实来自目标提交。
不要在部署任务里再次执行 npm install 和 npm run build,除非你明确接受“部署时重新构建”带来的环境差异。
十一、失败路径与诊断方法
11.1 npm ci 失败
常见表现:
npm ci can only install packages when package.json and package-lock.json are in sync
原因通常是开发者修改了 package.json,却没有提交同步更新的锁文件。恢复步骤是:
npm install
git diff package-lock.json
git add package.json package-lock.json
git commit
不能通过在 CI 中改用 npm install 来掩盖问题,因为这样会让 CI 重新解析依赖,降低可重复性。
11.2 类型检查失败但本地通过
应逐项比较:
- Node.js 版本;
npm ci与本地已有node_modules的差异;- TypeScript、Vue、
vue-tsc版本; tsconfig是否通过环境变量或路径不同;- 本地是否只运行了编辑器诊断,而没有运行完整命令。
本地已有旧依赖时,开发者看到的结果可能不是锁文件对应的结果。使用全新目录执行 npm ci && npm run typecheck,通常更容易复现 CI。
11.3 Lint 检查了不应检查的文件
如果报告来自 dist/、覆盖率目录或临时文件,说明 ESLint 的忽略配置不完整。应优先调整扫描范围和忽略规则,而不是对生成代码大规模添加禁用注释。
同时要避免忽略整个 src/ 或所有 .vue 文件。那会使命令“成功”,但实际没有检查关键代码。
11.4 测试在 CI 中挂起
最常见原因是使用了监听模式:
{
"scripts": {
"test": "vitest"
}
}
CI 应改为:
{
"scripts": {
"test": "vitest run"
}
}
如果测试仍不退出,应检查:
- 是否创建了未关闭的定时器;
- 是否启动了未关闭的 HTTP 服务;
- 是否存在未完成的 Promise;
- 测试框架配置是否启用了监听;
- 测试是否依赖真实外部网络。
11.5 构建成功但预览失败
诊断顺序应为:
test -f dist/index.html
find dist -maxdepth 2 -type f -print
npm run preview -- --host 127.0.0.1 --port 4173
如果进程立即退出,读取 preview.log。如果服务器运行但请求失败,检查:
- 端口是否已被占用;
- host 是否绑定到了 CI 可访问的地址;
dist输出目录是否被改名;vite.config.ts是否配置了不同的outDir;- 构建是否实际生成了目标环境所需文件。
11.6 预览首页成功但部署后路由 404
这是静态 SPA 常见边界。访问 / 只需要服务器返回 dist/index.html;而访问 /settings 时,静态服务器可能寻找 dist/settings,找不到就返回 404。
修复不在 Vue 组件中,而在部署服务器的回退配置中。例如服务器需要将未知页面路径回退到 index.html,同时不能把不存在的静态资源错误地回退成 HTML。具体配置取决于 Nginx、CDN 或托管平台,不能用 vite preview 的成功替代生产服务器验证。
十二、质量门禁的取舍
1. 是否上传测试覆盖率
覆盖率报告可以帮助发现未执行分支,但覆盖率只是“执行过哪些代码”的统计,不等于断言质量。下面两组测试的行覆盖率可能相近,业务价值却不同:
expect(add(1, 2)).toBe(3)
和:
expect(add(1, 2)).toBe(999)
后者会失败,反而说明断言捕获了行为变化。若启用覆盖率,应将其作为额外证据,并结合分支覆盖率、关键路径和测试审查。
2. 是否把所有检查都设为硬门禁
通常类型检查、Lint、单元测试和生产构建适合成为合并门禁。浏览器端到端测试则可能更慢、更依赖服务和外部资源,可以拆成独立任务,但必须明确哪些失败阻止发布,哪些只产生报告。
关键不是让流水线步骤越多越好,而是使每个硬门禁都对应一种实际风险,并让失败能够定位到明确原因。
3. 不要用 || true 隐藏失败
例如:
npm run typecheck || true
会把非零退出码转换为成功,破坏质量条件中的合取关系。若某条规则暂时只告警,应使用工具或 CI 平台明确表达“告警但不阻塞”,而不是吞掉所有错误。否则后续维护者无法区分“检查通过”和“检查根本失败但被忽略”。
十三、从提交到制品的完整状态转换
一个提交的典型状态可以表示为:
stateDiagram-v2
[*] --> CheckedOut
CheckedOut --> DependenciesReady: npm ci 成功
DependenciesReady --> TypeChecked: vue-tsc 成功
DependenciesReady --> Linted: eslint 成功
DependenciesReady --> Tested: vitest run 成功
TypeChecked --> Built: 所需质量任务满足
Linted --> Built
Tested --> Built
Built --> Previewed: vite preview + HTTP 检查成功
Previewed --> ArtifactUploaded: 上传 dist 成功
ArtifactUploaded --> Deployable
DependenciesReady --> Failed: 安装失败
TypeChecked --> Failed: 类型错误
Linted --> Failed: Lint 错误
Tested --> Failed: 测试失败
Built --> Failed: 构建失败
Previewed --> Failed: 服务器或冒烟检查失败
ArtifactUploaded --> Failed: 上传失败
这个状态图中的“所需质量任务满足”不是 Vite 或 GitHub Actions 自动规定的固定状态,而是团队配置的门禁策略。可以让构建与类型、Lint、测试并行,也可以要求三者全部完成后才开始构建;但最终发布必须消费经过约定检查的构建结果。
十四、最终检查应回答的具体问题
一条合格的 Vue CI 流水线至少应能回答:
package.json与锁文件是否同步?- 当前 Node.js 和依赖版本是否明确?
.vue文件是否经过vue-tsc检查?- Lint 是否覆盖源代码而不是只检查少数文件?
- 测试是否在 CI 中一次性退出?
- 构建是否从干净输入生成
dist/? - 客户端环境变量是否包含敏感信息?
vite preview是否服务了刚刚生成的构建?- 预览服务器失败时是否保留日志?
curl检查失败时是否真的让任务失败?- 制品是否带有提交标识?
- 部署是否复用已验证制品,而不是重新构建?
- SPA 路由、资源路径和目标环境配置是否在真实部署环境中验证?
类型、Lint、测试、构建、预览和制品分别覆盖静态约束、规则约束、行为约束、产物生成、HTTP 可访问性和交付可追踪性。它们组合起来,才能把“代码看起来能运行”推进为“这次提交经过了可重复、可诊断并可交付的验证流程”。
系列导航与关联阅读
- 系列入口:Vue 完整学习路线:从响应式与组件到工程化、SSR 和生产交付
- 上一篇:Vue 微前端:路由、状态、样式、依赖隔离和迁移取舍
- 下一篇:Vue 静态站点交付:Nginx、CDN、History 回退、缓存和压缩
官方资料
本文依据 Vue、Vite 与生态项目官方文档重新梳理;正文与示例由 WR BLOG 编写。

评论
0 条讨论