前端项目专用开发规范
适用框架:admin-plus(基于 vue-admin-beautiful)v13.3.0
技术栈:Vue 3 + TypeScript + Vite 5 + Pinia + Vue Router 4 + Element Plus 2.x
最后更新:2026-03-06
目录
- 项目初始化清单
- 目录结构
- 路径别名速查表
- 新增页面开发流程
- API 函数规范
- HTTP 请求规范
- 组件开发规范
- 状态管理规范(Pinia)
- 路由规范
- 认证流程规范
- i18n 规范
- 样式规范
- TypeScript 规范
- 测试规范
- 构建 & 环境规范
- .gitignore 必含项
1. 项目初始化清单
包管理器
必须使用 pnpm,禁止使用 npm 或 yarn。
# 安装依赖
pnpm install
# 新增依赖
pnpm add <package>
# 新增开发依赖
pnpm add -D <package>
项目根目录已有 pnpm-lock.yaml,不要提交 package-lock.json 或 yarn.lock。
Node 版本要求
Node.js ≥ 20,推荐使用 LTS 版本。
node -v # 确认 >= 20.x.x
建议在项目根目录添加 .nvmrc 或 .node-version 文件固定版本:
20
从框架仓库 clone 后必须做的改动
从 admin-plus 框架 clone 新项目后,按以下清单逐项修改:
1. package.json
{
"name": "<your-project-name>",
"version": "0.1.0",
"description": "<项目描述>"
}
2. vite.config.ts
修改 banner 注释(build.rollupOptions 或插件配置中):
// 修改为项目名称
const banner = `/* <your-project-name> v${pkg.version} */`
3. 清理框架 demo 页面
删除或替换 src/views/ 下的框架演示路由和页面,只保留必要的基础页面(login、404、403 等)。
4. 修改 src/config/index.ts
export default {
// Token 在 localStorage 中的 key 名,项目间建议保持唯一
tokenTableName: '<project>-token',
// 请求超时(毫秒)
requestTimeout: 30000,
// 后端约定的成功码
successCode: [200, 0],
// ...
}
5. 配置环境变量文件
在项目根目录创建以下文件(不提交到 git,.env.*.local 用于本地覆盖):
.env.development
# 本地开发 API 地址
VITE_APP_BASE_API=http://localhost:8000
# 应用 ID(用于多租户/埋点区分)
VITE_APP_ID=your-app-dev
# 应用标题(显示在浏览器 tab)
VITE_APP_TITLE=MyApp (Dev)
# Google OAuth Client ID
VITE_GOOGLE_CLIENT_ID=xxxxx.apps.googleusercontent.com
# 开发端口(与 vite.config.ts 中 server.port 保持一致)
VITE_PORT=15000
.env.production
# 生产 API 地址
VITE_APP_BASE_API=https://api.example.com
# 应用 ID
VITE_APP_ID=your-app-prod
# 应用标题
VITE_APP_TITLE=MyApp
# Google OAuth Client ID(生产环境)
VITE_GOOGLE_CLIENT_ID=xxxxx.apps.googleusercontent.com
⚠️
.env.development.local/.env.production.local可用于个人本地覆盖,已在.gitignore中排除,不要提交。
2. 目录结构
{project}/
├── src/
│ ├── api/ # API 函数(每个文件对应一个业务域)
│ │ ├── user.ts # 用户相关 API
│ │ └── {domain}.ts # 其他业务域 API
│ ├── assets/ # 静态资源(图片、音频等,经 Vite 处理)
│ ├── components/ # 全局共享组件(自动引入,无需 import)
│ │ └── MyButton/
│ │ └── index.vue
│ ├── config/ # 应用配置
│ │ └── index.ts # tokenTableName / requestTimeout / successCode 等
│ ├── constants/ # 常量定义
│ ├── directives/ # Vue 自定义指令
│ ├── enum/ # 枚举
│ │ └── ApiUrls.ts # 所有 API 路径统一在此定义
│ ├── hooks/ # Composition API hooks
│ ├── i18n/
│ │ ├── index.ts # vue-i18n 初始化
│ │ └── locales/
│ │ ├── zh.ts # 中文语言包
│ │ └── en.ts # 英文语言包
│ ├── layouts/ # 页面布局(引用 library/layouts/)
│ ├── modules/ # 子包(按需懒加载)
│ ├── plugins/ # 本地插件(项目级,非 library)
│ ├── router/
│ │ ├── index.ts # 路由定义(静态路由 + 动态路由挂载)
│ │ └── permissions.ts # 路由守卫(beforeEach,内置 token 检查)
│ ├── store/
│ │ └── modules/
│ │ ├── user.ts # 认证状态(token / username / uid)
│ │ ├── acl.ts # 权限(roles / permissions)
│ │ ├── routes.ts # 动态路由状态
│ │ ├── settings.ts # 应用全局设置
│ │ └── tabs.ts # Tab 导航状态
│ ├── styles/ # 全局样式
│ │ └── element-plus/
│ │ └── index.scss # Element Plus 主题变量覆盖
│ ├── types/ # TypeScript 类型定义(项目级)
│ ├── utils/
│ │ ├── Request.ts # Axios 封装(主 HTTP 客户端,默认导出 instance)
│ │ ├── token.ts # Token 存取工具(get/set/remove)
│ │ └── ... # 其他工具函数
│ └── views/ # 页面组件(按功能模块分子目录)
│ ├── login/
│ │ └── index.vue
│ ├── error/
│ │ ├── 404.vue
│ │ └── 403.vue
│ └── {module}/
│ └── index.vue # 新业务模块页面
├── library/ # Vab 框架层(一般不修改)
│ ├── components/ # Vab 框架组件(VabMenu / VabHeader 等,自动引入)
│ ├── layouts/ # Vab 布局实现
│ ├── plugins/
│ │ └── vab/ # @gp 别名指向此目录(全局插件)
│ ├── styles/
│ │ └── variables/
│ │ └── variables.module.scss # 全局 SCSS 变量(已自动注入所有 .scss)
│ └── types/ # Vab 内部类型声明
├── types/ # 全局 TypeScript 类型声明(/# 别名指向此目录)
├── public/ # 静态资源(不经过 Vite 处理,原样输出到 dist/)
│ └── favicon.ico
├── tests/
│ └── unit/ # 单元测试(Vitest)
│ └── {module}.spec.ts
├── .env.development # 开发环境变量
├── .env.production # 生产环境变量
├── .gitignore
├── package.json
├── pnpm-lock.yaml
├── tsconfig.json
└── vite.config.ts
3. 路径别名速查表
| 别名 | 实际路径 | 用途示例 |
|---|---|---|
@ | src/ | import { useUserStore } from '@/store/modules/user' |
~ | 项目根目录 | import pkg from '~/package.json' |
/# | types/ | import type { UserInfo } from '/#/user' |
@vab | library/ | import VabIcon from '@vab/components/VabIcon/index.vue' |
@gp | library/plugins/vab | import { setupVab } from '@gp'(全局插件) |
⚠️ 路径别名已在
vite.config.ts和tsconfig.json中同步配置,新增别名两处都要更新。
4. 新增页面开发流程
Step 1:创建页面组件
在 src/views/{模块}/ 下创建 index.vue:
<template>
<div class="user-list">
<el-table :data="tableData" v-loading="loading">
<el-table-column prop="name" :label="t('user.list.name')" />
</el-table>
</div>
</template>
<script setup lang="ts">
import type { UserItem } from '@/types/user'
import { getUserList } from '@/api/user'
const { t } = useI18n()
const loading = ref(false)
const tableData = ref<UserItem[]>([])
const fetchData = async () => {
loading.value = true
try {
const res = await getUserList()
tableData.value = res.data
} finally {
loading.value = false
}
}
onMounted(fetchData)
</script>
<style scoped lang="scss">
.user-list {
padding: 16px;
}
</style>
注意:
ref、onMounted、useI18n等均已自动引入,无需手动import。
Step 2:定义 API 函数
在 src/api/{模块}.ts 下定义 API 函数(详见第 5 节):
// src/api/user.ts
import instance from '@/utils/Request'
import { ApiUrls } from '@/enum/ApiUrls'
import type { UserListResponse } from '@/types/user'
export const getUserList = () =>
instance.get<UserListResponse>(ApiUrls.USER_LIST)
Step 3:添加路由
在 src/router/index.ts 中添加路由配置:
{
path: '/user',
component: Layout,
meta: { title: 'user.menu.title', icon: 'user' },
children: [
{
path: 'list',
name: 'UserList',
component: () => import('@/views/user/index.vue'),
meta: { title: 'user.list.title', icon: 'list', roles: ['admin'] },
},
],
}
Step 4:新建 Pinia Store(如需全局状态)
在 src/store/modules/{模块}.ts 新建 store(详见第 8 节):
// src/store/modules/user-mgmt.ts
import { defineStore } from 'pinia'
export const useUserMgmtStore = defineStore('userMgmt', {
state: () => ({
selectedUserId: null as number | null,
}),
actions: {
setSelectedUser(id: number) {
this.selectedUserId = id
},
},
})
Step 5:添加 i18n 文案(如需国际化)
在 src/i18n/locales/zh.ts 和 en.ts 中添加对应 key:
// zh.ts
export default {
user: {
list: {
title: '用户列表',
name: '用户名',
},
},
}
// en.ts
export default {
user: {
list: {
title: 'User List',
name: 'Username',
},
},
}
Step 6:编写单元测试
在 tests/unit/ 下创建对应测试文件:
// tests/unit/user.spec.ts
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import UserList from '@/views/user/index.vue'
import * as userApi from '@/api/user'
describe('UserList', () => {
it('renders correctly', async () => {
vi.spyOn(userApi, 'getUserList').mockResolvedValue({
data: [{ id: 1, name: 'Tom' }],
})
const wrapper = mount(UserList)
expect(wrapper.exists()).toBe(true)
})
})
Step 7:本地验证并提交 MR
# 本地开发验证
pnpm dev
# 运行单元测试
pnpm test:unit
# Lint 检查
pnpm lint
# 确认无误后提交
git add .
git commit -m "feat(user): add user list page"
git push origin feature/user-list
# 在 GitLab/GitHub 创建 MR/PR,等待 Code Review
5. API 函数规范
文件命名
src/api/{业务域}.ts
每个文件对应一个业务域,例如:
src/api/user.ts— 用户相关src/api/order.ts— 订单相关src/api/product.ts— 商品相关
函数命名
使用动词前缀,语义清晰:
| 操作 | 前缀 | 示例 |
|---|---|---|
| 查询列表 | get | getUserList |
| 查询详情 | get | getUserById |
| 创建 | create | createUser |
| 更新 | update | updateUser |
| 删除 | delete | deleteUser |
| 批量操作 | batch | batchDeleteUsers |
API 路径管理
所有 API 路径统一在 src/enum/ApiUrls.ts 中定义,禁止在 API 函数中硬编码路径字符串:
// src/enum/ApiUrls.ts
export enum ApiUrls {
// 用户模块
USER_LIST = '/api/v1/users',
USER_DETAIL = '/api/v1/users/:id',
USER_CREATE = '/api/v1/users',
USER_UPDATE = '/api/v1/users/:id',
USER_DELETE = '/api/v1/users/:id',
// 认证模块
AUTH_LOGIN = '/api/v1/auth/login',
AUTH_LOGOUT = '/api/v1/auth/logout',
AUTH_GOOGLE_CALLBACK = '/api/v1/auth/google/callback',
}
响应类型定义
每个 API 函数的响应必须有 TypeScript 类型定义:
// src/types/user.ts
export interface UserItem {
id: number
username: string
email: string
roles: string[]
createdAt: string
}
export interface UserListResponse {
code: number
message: string
data: UserItem[]
total: number
}
export interface CreateUserParams {
username: string
email: string
password: string
roles?: string[]
}
完整示例
// src/api/user.ts
import instance from '@/utils/Request'
import { ApiUrls } from '@/enum/ApiUrls'
import type {
UserItem,
UserListResponse,
CreateUserParams,
} from '@/types/user'
/** 获取用户列表 */
export const getUserList = (params?: { page?: number; size?: number }) =>
instance.get<UserListResponse>(ApiUrls.USER_LIST, { params })
/** 获取用户详情 */
export const getUserById = (id: number) =>
instance.get<{ data: UserItem }>(`${ApiUrls.USER_LIST}/${id}`)
/** 创建用户 */
export const createUser = (data: CreateUserParams) =>
instance.post<{ data: UserItem }>(ApiUrls.USER_CREATE, data)
/** 更新用户 */
export const updateUser = (id: number, data: Partial<CreateUserParams>) =>
instance.put<{ data: UserItem }>(`${ApiUrls.USER_LIST}/${id}`, data)
/** 删除用户 */
export const deleteUser = (id: number) =>
instance.delete<void>(`${ApiUrls.USER_LIST}/${id}`)
6. HTTP 请求规范
使用方式
始终使用 src/utils/Request.ts 的默认导出 instance,禁止在业务代码中直接 import axios:
// ✅ 正确
import instance from '@/utils/Request'
// ❌ 错误
import axios from 'axios'
自动附加 Token
请求拦截器已配置自动从 localStorage 读取 token 并附加 Authorization: Bearer {token} 头,业务代码无需手动处理。
统一响应格式
后端所有接口必须遵循以下格式:
{
"code": 0,
"message": "ok",
"data": { ... }
}
错误码处理规则
响应拦截器已统一处理以下场景:
| 场景 | 处理行为 |
|---|---|
code === 0 | 正常,返回 data |
code === -1 | 业务错误,弹出 message 提示,抛出错误 |
HTTP 401 | token 过期/无效,清除本地 token,跳转 /login |
HTTP 403 | 无权限,跳转 /403 |
| 网络错误 / 超时 | 弹出通用错误提示 |
业务代码只需
try/catch处理预期的业务异常,通用错误框架已处理。
请求取消
框架支持 AbortController 取消请求,适用于搜索防抖等场景:
const controller = new AbortController()
instance.get('/api/search', {
params: { q: keyword },
signal: controller.signal,
})
// 取消请求
controller.abort()
7. 组件开发规范
基本写法
所有组件统一使用 <script setup lang="ts"> 语法:
<template>
<div class="my-component">
<slot />
</div>
</template>
<script setup lang="ts">
// Props 必须有 TypeScript 类型定义
interface Props {
title: string
count?: number
variant?: 'primary' | 'secondary'
}
const props = withDefaults(defineProps<Props>(), {
count: 0,
variant: 'primary',
})
const emit = defineEmits<{
change: [value: number]
close: []
}>()
</script>
<style scoped lang="scss">
.my-component {
// 样式
}
</style>
命名规范
- 组件文件名:使用 PascalCase,如
UserCard.vue、SearchInput.vue - 组件目录:建议使用
index.vue作为主文件(如UserCard/index.vue) - 组件名(
defineOptions):与文件名保持一致
// 如需显式声明组件名(用于 DevTools 调试)
defineOptions({ name: 'UserCard' })
自动引入说明
以下内容无需手动 import,已通过 unplugin-auto-import 和 unplugin-vue-components 自动引入:
| 类型 | 包含内容 |
|---|---|
| Vue API | ref、computed、watch、onMounted、defineProps 等全部 Composition API |
| Vue Router | useRouter、useRoute |
| Pinia | defineStore、storeToRefs |
| VueUse | @vueuse/core 全部函数 |
| Element Plus | 所有 El* 组件 |
| src/components/ | 项目自定义全局组件 |
| library/components/ | Vab 框架组件(VabMenu、VabHeader 等) |
组件存放位置
| 类型 | 存放位置 | 是否自动引入 |
|---|---|---|
| 全局共享组件 | src/components/ | ✅ 是 |
| 页面内局部组件 | src/views/{模块}/components/ | ❌ 否(需手动 import) |
| Vab 框架组件 | library/components/ | ✅ 是 |
8. 状态管理规范(Pinia)
Store 文件规范
每个业务模块一个 store 文件,存放于 src/store/modules/:
// src/store/modules/product.ts
import { defineStore } from 'pinia'
import type { ProductItem } from '@/types/product'
import { getProductList } from '@/api/product'
export const useProductStore = defineStore('product', {
state: () => ({
list: [] as ProductItem[],
loading: false,
total: 0,
currentPage: 1,
}),
getters: {
isEmpty: (state) => state.list.length === 0,
},
actions: {
async fetchList(params?: { page?: number }) {
this.loading = true
try {
const res = await getProductList(params)
this.list = res.data.items
this.total = res.data.total
} finally {
this.loading = false
}
},
reset() {
this.$reset()
},
},
})
命名规范
- Store ID:camelCase,与模块名一致(如
'userMgmt') - Store Hook:
use{Module}Store(如useProductStore)
使用规范
// 在组件中使用
const productStore = useProductStore()
// 解构响应式状态(必须用 storeToRefs)
const { list, loading } = storeToRefs(productStore)
// 调用 action(直接调用,不用 storeToRefs)
productStore.fetchList({ page: 1 })
禁止事项
- ❌ 禁止跨 store 直接修改另一个 store 的
state - ❌ 禁止在
state外直接修改属性(应通过action) - ✅ 跨 store 通信:在 action 内引入并调用其他 store 的 action
9. 路由规范
路由配置格式
// src/router/index.ts
import type { RouteRecordRaw } from 'vue-router'
const routes: RouteRecordRaw[] = [
{
path: '/dashboard',
component: Layout, // 使用 Vab 布局组件
redirect: '/dashboard/index',
meta: {
title: 'dashboard.title', // i18n key
icon: 'grid', // 菜单图标
},
children: [
{
path: 'index',
name: 'Dashboard', // name 必须唯一,使用 PascalCase
component: () => import('@/views/dashboard/index.vue'), // 懒加载
meta: {
title: 'dashboard.index.title',
icon: 'home',
roles: ['admin', 'editor'], // 有权限访问的角色列表,不填则所有人可见
noClosable: true, // 是否禁止关闭 Tab(首页建议设 true)
},
},
],
},
]
meta 字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
title | string | i18n key,显示在菜单和面包屑 |
icon | string | 菜单图标名称 |
roles | string[] | 可访问角色列表,不设置则所有已登录用户可见 |
noClosable | boolean | 禁止关闭该 Tab(默认 false) |
hidden | boolean | 在菜单中隐藏(默认 false) |
keepAlive | boolean | 是否开启 keep-alive 缓存(默认 false) |
路由懒加载
所有业务页面必须使用懒加载:
// ✅ 正确
component: () => import('@/views/user/index.vue')
// ❌ 错误(静态 import 会打包进主 chunk)
import UserList from '@/views/user/index.vue'
component: UserList
已内置路由
以下路由已在框架中处理,不要重复定义:
/login— 登录页/404— 404 页面/403— 无权限页面/:pathMatch(.*)— 通配符,自动跳转 404
10. 认证流程规范
整体流程
用户访问页面
↓
路由守卫 (src/router/permissions.ts) beforeEach
↓
检查 localStorage token
├── 无 token → 跳转 /login
└── 有 token → 检查用户信息
├── 无用户信息 → 调用 userStore.getUserInfo()
└── 有用户信息 → 放行
Google OAuth 登录流程
前端跳转 Google 授权页
↓
Google 回调带 code 参数返回前端 /auth/callback
↓
前端调用后端 API:POST /api/v1/auth/google/callback { code }
↓
后端验证 code,返回 JWT token
↓
前端存储 token:
localStorage.setItem(tokenTableName, token)
↓
调用 userStore.getUserInfo() 获取用户信息
↓
跳转首页 /dashboard
Token 操作
使用框架提供的工具函数,不要直接操作 localStorage:
import { getToken, setToken, removeToken } from '@/utils/token'
// 存储
setToken(token)
// 读取
const token = getToken()
// 清除
removeToken()
tokenTableName 的值在 src/config/index.ts 中配置,工具函数会自动使用。
登出
import { useUserStore } from '@/store/modules/user'
const userStore = useUserStore()
// 完整登出:清除 token + 重置所有 store + 跳转登录页
await userStore.logout()
路由守卫
src/router/permissions.ts 已实现完整的守卫逻辑,新项目无需修改,只需确保:
useUserStore中的tokengetter 正确返回当前 tokengetUserInfoaction 在 token 有效时能正确获取并填充用户信息
11. i18n 规范
基本使用
<template>
<!-- 模板中使用 $t -->
<h1>{{ $t('user.login.title') }}</h1>
<el-button>{{ $t('common.btn.submit') }}</el-button>
</template>
<script setup lang="ts">
// script 中使用 useI18n(已自动引入)
const { t } = useI18n()
const message = t('user.login.welcomeBack', { name: username })
</script>
Key 命名规范
格式:{模块}.{功能}.{描述}
// 正确示例
'user.login.title' // 用户模块 > 登录功能 > 标题
'user.list.searchPlaceholder' // 用户模块 > 列表功能 > 搜索框占位
'common.btn.submit' // 公共 > 按钮 > 提交
'common.btn.cancel' // 公共 > 按钮 > 取消
'common.msg.success' // 公共 > 消息 > 成功
'order.detail.totalAmount' // 订单模块 > 详情功能 > 总金额
// 错误示例
'submitBtn' // 没有模块层级
'用户列表' // 不能用中文作为 key
'USER_LIST_TITLE' // 不要用大写
语言包结构
// src/i18n/locales/zh.ts
export default {
common: {
btn: {
submit: '提交',
cancel: '取消',
confirm: '确认',
delete: '删除',
edit: '编辑',
search: '搜索',
reset: '重置',
},
msg: {
success: '操作成功',
failed: '操作失败',
deleteConfirm: '确定要删除吗?',
},
},
user: {
login: {
title: '登录',
welcomeBack: '欢迎回来,{name}',
},
list: {
title: '用户列表',
name: '用户名',
email: '邮箱',
},
},
}
强制要求
- 所有面向用户的文字必须走 i18n,禁止硬编码中文或英文
- 新增 key 时,
zh.ts和en.ts必须同步添加 - 不要在同一个 key 下同时出现文字和子对象(避免 vue-i18n 报错)
12. 样式规范
技术选型
- 预处理器:SCSS(已配置,直接使用)
- 组件内样式:
<style scoped lang="scss"> - 全局样式:
src/styles/
全局 SCSS 变量
@vab/styles/variables/variables.module.scss 中的变量已通过 Vite css.preprocessorOptions.additionalData 自动注入到所有 .scss 文件,无需 @import:
/* ✅ 直接使用,无需 import */
.my-component {
color: $--vab-color-primary;
font-size: $--vab-font-size-base;
}
/* ❌ 错误,不要手动 import 变量文件 */
@import '@vab/styles/variables/variables.module.scss';
Element Plus 主题定制
在 src/styles/element-plus/index.scss 中覆盖 CSS 变量:
:root {
--el-color-primary: #1890ff;
--el-border-radius-base: 4px;
}
组件样式规范
<style scoped lang="scss">
/* 1. 组件根元素使用语义化类名(与组件名对应) */
.user-card {
/* 2. 布局属性 */
display: flex;
flex-direction: column;
gap: 12px;
/* 3. 使用全局 SCSS 变量 */
padding: $--vab-padding-base;
border-radius: $--vab-border-radius-base;
/* 4. 嵌套选择器 */
&__header {
font-size: 16px;
font-weight: 600;
}
&__body {
flex: 1;
}
}
</style>
禁止事项
- ❌ 禁止内联样式(
style="color: red"),除非是动态绑定(:style="{ color: computedColor }") - ❌ 禁止在 scoped 样式中使用全局选择器(如
.el-table直接修改),如需穿透用:deep() - ✅ 穿透第三方组件样式:使用
:deep(.el-table__header)
13. TypeScript 规范
当前配置说明
tsconfig.json 中 strict 模式当前为 false(框架历史原因),但新增代码必须按 strict 标准编写:
- 显式标注所有变量、参数、返回值类型
- 不使用
any(除非框架层绝对必要) - 不使用隐式
any(noImplicitAny等效要求)
类型定义存放
src/types/ # 业务类型定义
user.ts # 用户相关类型
order.ts # 订单相关类型
common.ts # 通用类型(分页、响应包装等)
types/ # 全局声明文件(/@ 别名)
global.d.ts # 全局类型扩展(Window、环境变量等)
shims.d.ts # 模块声明
通用类型示例
// src/types/common.ts
/** 标准分页参数 */
export interface PaginationParams {
page: number
size: number
}
/** 标准分页响应 */
export interface PaginatedResponse<T> {
items: T[]
total: number
page: number
size: number
}
/** 标准 API 响应包装 */
export interface ApiResponse<T = unknown> {
code: number
message: string
data: T
}
禁止事项
// ❌ 禁止 any
const data: any = await fetchData()
// ❌ 禁止隐式 any 函数参数
function process(item) { ... }
// ✅ 正确
const data: UserItem = await fetchData()
function process(item: UserItem): void { ... }
// ⚠️ 确实无法确定类型时,用 unknown 替代 any
const raw: unknown = JSON.parse(str)
14. 测试规范
框架
- 测试框架:Vitest
- 组件测试:@vue/test-utils
- 测试文件位置:
tests/unit/
测试文件命名
tests/unit/
user.spec.ts # 测试 src/api/user.ts 或 src/views/user/
user-store.spec.ts # 测试 src/store/modules/user.ts
utils.spec.ts # 测试 src/utils/ 下的工具函数
覆盖率要求
| 类型 | 最低覆盖率 |
|---|---|
| 整体项目 | ≥ 70% |
| 核心业务逻辑(store / utils) | ≥ 90% |
| API 函数 | ≥ 80% |
示例
// tests/unit/user-store.spec.ts
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useUserStore } from '@/store/modules/user'
import * as userApi from '@/api/user'
describe('useUserStore', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('should fetch user info successfully', async () => {
vi.spyOn(userApi, 'getUserInfo').mockResolvedValue({
data: { id: 1, username: 'tom', roles: ['admin'] },
})
const store = useUserStore()
await store.getUserInfo()
expect(store.username).toBe('tom')
expect(store.roles).toContain('admin')
})
it('should clear token on logout', async () => {
const store = useUserStore()
store.$patch({ token: 'test-token' })
await store.logout()
expect(store.token).toBe('')
expect(localStorage.getItem('token')).toBeNull()
})
})
常用命令
# 运行全部单元测试
pnpm test:unit
# 带 watch 模式(开发时使用)
pnpm test:unit --watch
# 生成覆盖率报告
pnpm test:unit --coverage
# 运行特定测试文件
pnpm test:unit tests/unit/user.spec.ts
15. 构建 & 环境规范
常用命令
| 命令 | 说明 |
|---|---|
pnpm dev | 启动开发服务器(默认端口 15000) |
pnpm build | 构建生产包(使用 .env.production) |
pnpm build:test | 构建测试环境包(使用 .env.test) |
pnpm preview | 预览构建产物 |
pnpm lint | 运行 ESLint + Stylelint 检查 |
pnpm lint:fix | 自动修复可修复的 Lint 问题 |
pnpm test:unit | 运行单元测试 |
pnpm type-check | TypeScript 类型检查(不输出文件) |
开发服务器
// vite.config.ts(参考)
server: {
port: 15000,
open: true,
proxy: {
'/api': {
target: env.VITE_APP_BASE_API,
changeOrigin: true,
},
},
}
构建输出
- 输出目录:
dist/ - 构建工具:Vite 5(编译器:SWC,速度优于 Babel)
- 代码分割:框架已配置路由级 chunk 分割(lazy import 自动生效)
环境变量使用
在代码中通过 import.meta.env 访问:
// 访问环境变量(必须以 VITE_ 开头才会暴露给前端)
const baseURL = import.meta.env.VITE_APP_BASE_API
const appId = import.meta.env.VITE_APP_ID
// TypeScript 类型扩展(在 types/global.d.ts 中)
interface ImportMetaEnv {
VITE_APP_BASE_API: string
VITE_APP_ID: string
VITE_APP_TITLE: string
VITE_GOOGLE_CLIENT_ID: string
}
CI/CD 注意事项
- 构建前执行
pnpm lint和pnpm test:unit(失败则不部署) - 生产构建产物中不应包含 source map(
build.sourcemap: false) - 构建产物
dist/不提交到 git
16. .gitignore 必含项
以下条目必须在 .gitignore 中存在:
# 构建产物
dist/
dist-ssr/
# 依赖
node_modules/
# 环境变量(本地覆盖,不提交)
.env.*.local
# 自动生成文件(由 unplugin-auto-import / unplugin-vue-components 生成)
src/auto-imports.d.ts
src/components.d.ts
src/auto-components.d.ts
# IDE
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json
.idea/
# 系统文件
.DS_Store
Thumbs.db
# 日志
*.log
npm-debug.log*
pnpm-debug.log*
# 测试覆盖率
coverage/
# 临时文件
*.local
附录:快速参考卡
新建页面最小步骤
# 1. 创建页面文件
touch src/views/{module}/index.vue
# 2. 创建 API 文件
touch src/api/{module}.ts
# 3. 在 src/enum/ApiUrls.ts 添加路径常量
# 4. 在 src/types/{module}.ts 添加类型定义
# 5. 在 src/router/index.ts 添加路由
# 6. 在 src/i18n/locales/zh.ts 和 en.ts 添加文案
# 7. 创建测试文件
touch tests/unit/{module}.spec.ts
常见问题
Q: 组件 import 报错找不到
A: 检查组件是否在 src/components/ 下,重启 pnpm dev 刷新自动引入。
Q: ref / computed 报错未定义
A: 检查 src/auto-imports.d.ts 是否已生成(首次 pnpm dev 后自动生成)。
Q: Element Plus 组件样式不生效
A: 确认 src/styles/element-plus/index.scss 已在入口文件 import,并检查 CSS 变量覆盖优先级。
Q: 路由跳转后 404
A: 确认路由 name 唯一,检查父路由 component 是否为 Layout,子路由 path 不要带 / 前缀。
Q: 请求 401 但 token 存在
A: 检查 src/config/index.ts 的 tokenTableName 是否与存储时的 key 一致。