Vue 3 前端工程化规范
前言
我们遵循以下核心工程原则,这些原则是所有规范的基石:
-
清晰胜于抖机灵 (Clarity over Cleverness):代码首先是写给人读的,其次才是给机器执行的。优先保证代码的直观和易于理解。
-
单一职责原则 (Single Responsibility Principle):每个模块、组件或函数都应该只负责一项功能,并把它做好。
-
自动化优于约定 (Automation over Convention):凡是能通过工具自动保证的规范,就不要依赖于人的记忆和自觉。
-
小步提交,持续集成 (Small Commits, Continuous Integration):通过小而专注的 Git 提交,简化代码审查,并利用 CI/CD 流水线尽早发现问题。
-
测试是项目的一部分,不是附属品 (Testing is a Feature):未经测试的代码在设计上就是有缺陷的。单元测试是保证代码质量和未来重构信心的关键。
-
拥抱类型系统。 TypeScript 不是可选项,而是必需品。它能在编码阶段发现大量潜在错误,并极大地提升代码的可维护性。
我们围绕可维护性、可测试性和团队协作效率等核心理念,整理了这样一份Vue3前端代码规范文档。
1. 文档概述
1.1 文档目的
本文档旨在为基于 Vue 3 技术栈的前端项目提供一套统一的开发标准、最佳实践和协作流程。目标是提升代码的可读性、可维护性、健壮性 和团队协作效率,确保项目在整个生命周期内保持高质量和技术领先性。
1.2 适用范围
本文档适用于所有参与项目的前端开发工程师、测试工程师、架构师及项目经理。它是代码审查(Code Review)、技术决策和新成员入职培训的核心依据。
2. Vue 3 编码规范
2.1. 函数与变量
2.1.1. 优先使用箭头函数
在 Vue 3 的 <script setup>
组合式 API 场景下,函数通常不依赖 this
上下文,使用箭头函数可以保持代码风格统一。
- 【推荐】
plaintext
// 使用 const 和箭头函数定义所有本地函数
const fetchUserData = async (userId) => {
// ...
};
- 【反例】
plaintext
// 风格不统一,降低可读性
function fetchUserData() { /* ... */ }
const updateUser = () => { /* ... */ };
-
评审要点:
-
检查是否存在
function
和箭头函数的无理由混用。 -
方法定义是否一致。
-
2.1.2. 使用 **let**
和 **const**
const
优先,仅在变量需要被重新赋值时使用 let
。禁止使用 var
以避免变量提升带来的潜在问题。
- 【推荐】
plaintext
const MAX_RETRIES = 3;
let currentUser = await fetchUser();
currentUser = 'guest';
- 【反例】
plaintext
var MAX_RETRIES = 3; // 禁止使用 var
-
评审要点:
- 代码中是否还存在
var
关键字。
- 代码中是否还存在
2.2. 数据请求 (异步操作)
2.2.1. 使用 **async/await**
和 **try...catch**
async/await
提供了更扁平、更易读的异步代码结构。必须使用 try...catch
块来捕获和处理潜在的 API 错误。
- 【推荐】
plaintext
const getTableData = async () => {
isLoading.value = true;
try {
const response = await getTableDataApi({ /* params */ });
tableData.value = response.data;
} catch (error) {
console.error("Failed to fetch table data:", error);
// 向用户显示错误提示
showErrorToast('数据加载失败');
} finally {
isLoading.value = false;
}
};
- 【反例】
plaintext
// 1. 回调地狱,难以阅读和维护
getTableDataApi().then(res1 => {
getMoreDataApi(res1.id).then(res2 => {
// ...
});
});
// 2. 缺少错误处理
const fetchData = async () => {
const response = await someApi(); // 如果 someApi() reject,整个函数会崩溃
data.value = response;
}
-
评审要点:
-
所有异步请求是否都被
try...catch
包裹? -
是否有清晰的
loading
状态管理? -
错误处理是否对用户友好(例如,显示提示)?
-
是否存在超过一层的
.then()
嵌套?
-
2.2.2. 合理利用 **Promise.all**
和 **Promise.allSettled**
当多个 API 请求没有依赖关系时,应使用 Promise.all
并发执行以缩短总加载时间。
- 【推荐】
plaintext
// 场景:页面需要同时加载用户信息和配置信息
const loadPageData = async () => {
try {
const [userData, configData] = await Promise.all([
fetchUserApi(),
fetchConfigApi(),
]);
user.value = userData;
config.value = configData;
} catch (error) {
// ...
}
};
-
评审要点:
- 是否存在可以用
Promise.all
优化的串行await
调用?
- 是否存在可以用
2.3. 响应式变量
-
【推荐】 对于需要深度响应的大对象或数组,使用
reactive
。对于基本类型或只需替换整个对象的场景,使用ref
。 -
【性能优化】 对于只有顶层属性需要响应性的大型、深层嵌套对象(例如,从后端获取的只读列表),应使用
shallowRef
或shallowReactive
来避免不必要的性能开销。
2.4. 逻辑分支优化
避免深层嵌套的 if-else
语句,它们会显著增加代码的"圈复杂度",使其难以理解和测试。
- 【推荐】使用卫语句 (Guard Clauses) 或映射表 (Map)
plaintext
// 卫语句(提前返回)
function processPayment(user, order) {
if (!user) {
console.error("User not logged in.");
return;
}
if (!order || order.status !== 'pending') {
showErrorToast("Invalid order.");
return;
}
// ...核心逻辑...
}
// 映射表处理多状态
const statusActions = {
pending: () => handlePending(),
shipped: () => handleShipped(),
delivered: () => handleDelivered(),
default: () => handleDefault(),
};
const action = statusActions[order.status] || statusActions.default;
action();
- 【反例】
plaintext
// 深度嵌套,难以阅读
if (user) {
if (order) {
if (order.status === 'pending') {
// ...核心逻辑...
} else {
showErrorToast("Invalid order status.");
}
} else {
showErrorToast("No order found.");
}
} else {
console.error("User not logged in.");
}
-
评审要点:
-
if
语句的嵌套是否超过了2层? -
是否存在可以用映射表或策略模式优化的长
if-else if
链?
-
2.5. 代码整洁度
-
【推荐】 及时删除被注释掉的代码、调试用的
console.log
和debugger
语句。版本控制系统(Git)是你的安全网,无需在代码中保留历史遗迹。 -
【推荐】 单个文件(特别是Vue组件)的行数应尽量控制在 400行 以内。超过这个长度通常意味着组件承担了过多的职责,需要进行拆分(例如,抽取为子组件或
composable
函数)。
3. 组件与文件命名规范
3.1. 文件命名
-
Vue 组件 : 大驼峰 (PascalCase),如
UserProfile.vue
。 -
Composable 函数 : 小驼峰 (camelCase),并以
use
开头,如useFormValidation.ts
。 -
其他 TS/JS 文件 : 小驼峰 (camelCase) 或大驼峰 (PascalCase) 均可,但项目内需统一。推荐 小驼峰 (如
apiClient.ts
,utils.ts
)。
3.2. 组件 **name**
属性
所有组件都必须 显式声明一个 name
属性,且与文件名保持一致。这对于 Vue Devtools 调试和组件递归至关重要。
- 【推荐】
JavaScript
plaintext
// 在 UserProfile.vue 中
export default defineComponent({
name: 'UserProfile',
// ...
});
-
评审要点:
- 所有
.vue
文件是否都有一个匹配文件名的name
属性?
- 所有
4. Composable (Hooks) 使用规范
Hooks 是 Vue 3 中逻辑复用和功能拆分的核心。
- 【推荐】将可复用的逻辑(如弹窗控制、数据获取、表单状态)封装成
**composable**
函数。
plaintext
// composables/useModal.ts
import { ref } from 'vue';
export function useModal() {
const isVisible = ref(false);
const openModal = () => isVisible.value = true;
const closeModal = () => isVisible.value = false;
return { isVisible, openModal, closeModal };
}
// 在组件中使用
// const { isVisible, openModal, closeModal } = useModal();
- 【反例】将所有状态和方法都堆砌在一个巨大的
**reactive**
对象中。
plaintext
// 这种写法将 UI 状态、表单数据和控制方法耦合在一起,难以复用和测试
const editModel = reactive({
isShow: false,
form: { name: '...' },
showFunc: () => { /* ... */ },
cancelFunc: () => { /* ... */ },
});
-
评审要点:
-
组件中是否存在可以被抽象为
composable
的重复逻辑块? -
composable
是否遵循单一职责原则?
-
5. 性能优化
-
路由懒加载 : 所有路由必须 使用动态导入 (
() => import(...)
) 实现懒加载。 -
异步组件 : 对于非首屏、体积较大或只在特定条件下渲染的组件(如复杂的弹窗、图表),使用
defineAsyncComponent
。 -
善用
**v-once**
和**v-memo**
: 对于纯静态内容,使用v-once
。对于渲染开销大且依赖特定变量的列表,使用v-memo
。 -
虚拟滚动 : 对于超过100项的长列表,应采用虚拟滚动技术(可使用
vue-virtual-scroller
等库)。 -
减少依赖体积 : 优先选用
lodash-es
等支持 tree-shaking 的库。定期使用vite-bundle-analyzer
分析打包产物,移除不必要的依赖。
6. Git 工作流与提交规范
6.1 Git 分支模型
推荐使用简化的 Git-Flow 模型:
-
main
: 主分支,用于部署生产环境,代码必须是稳定且经过测试的。只能从develop
分支合并。 -
develop
: 开发主分支,集成了所有已完成的功能。是所有功能分支的父分支。 -
feature/<feature-name>
: 功能开发分支,从develop
创建。命名应清晰,如feature/user-authentication
。 -
fix/<fix-name>
: Bug 修复分支,从develop
(或紧急情况下从main
)创建。
6.2 Git 提交规范
采用 Conventional Commits 标准,强制通过 commitlint
工具校验。
格式 : <type>(<scope>): <subject>
-
常用
**type**
:-
feat
: 新增功能 -
fix
: 修复 Bug -
docs
: 仅修改文档 -
style
: 代码格式调整(不影响代码逻辑) -
refactor
: 代码重构(既不新增功能,也不修复 Bug) -
perf
: 性能优化 -
test
: 新增或修改测试 -
chore
: 构建流程、辅助工具的变更
-
-
示例:
plaintext
# 良好示例
feat(user): add user login and registration functionality
fix(form): correct validation logic for email field
refactor(api): abstract api calls into a dedicated service module
7. 项目结构
统一的目录结构是高效协作的基础。推荐以下结构:
plaintext
/src
├── api/ # API 请求模块 (按业务划分)
│ ├── auth.ts
│ └── user.ts
├── assets/ # 静态资源 (图片, 字体等)
├── components/ # 全局组件
│ ├── base/ # 基础 UI 组件 (BaseButton.vue)
│ └── business/ # 全局业务组件 (UserSelector.vue)
├── composables/ # Vue Composition API 函数 (Hooks)
│ └── usePagination.ts
├── config/ # 全局配置
│ └── index.ts
├── layouts/ # 布局组件
│ └── DefaultLayout.vue
├── router/ # 路由配置
│ └── index.ts
├── stores/ # Pinia 状态管理
│ ├── user.ts
│ └── index.ts
├── styles/ # 全局样式
│ ├── main.scss
│ └── _variables.scss
├── types/ # TypeScript 类型定义
│ └── api.d.ts
├── utils/ # 工具函数
│ └── format.ts
├── views/ # 页面级组件 (路由入口)
│ ├── user/
│ │ ├── UserProfile.vue
│ │ └── UserList.vue
│ └── Home.vue
├── App.vue # 根组件
└── main.ts # 应用入口
8. 组件化开发规范
8.1 组件分类标准
明确组件的职责分类,有助于合理规划项目结构和提升复用性。
组件分类 | 职责说明 | 示例 | 存放位置 |
---|---|---|---|
基础组件 (Base Components) | 不包含任何业务逻辑,纯粹的 UI 展示和交互。具有极高的复用性,通常带有 Base 或 App 前缀。 |
BaseButton.vue , BaseInput.vue , BaseModal.vue |
src/components/base/ |
业务组件 (Business Components) | 封装特定业务逻辑,由多个基础组件组合而成。与特定业务场景强相关,可在项目内多处复用。 | UserCard.vue , OrderHistoryTable.vue |
src/components/business/ 或 页面私有组件 src/views/xxx/components/ |
页面组件 (View Components) | 作为路由的入口,负责组织页面布局和业务组件,处理当前页面的数据请求与状态管理。 | UserProfile.vue , HomePage.vue |
src/views/ |
布局组件 (Layout Components) | 定义应用的整体页面结构,如页头、侧边栏、内容区等。 | DefaultLayout.vue , AdminLayout.vue |
src/layouts/ |
8.2 组件编写标准
8.2.1 命名规范
-
文件名 : 采用大驼峰命名法 (PascalCase),如
MyComponent.vue
。 -
组件
**name**
属性 : 必须添加name
选项,且与文件名保持一致。这对于 Vue Devtools 调试至关重要。
8.2.2 <script setup>
内部顺序
为了保持一致性和可读性,推荐遵循以下顺序:
plaintext
<script setup lang="ts">
// 1. imports
import { ref, computed } from 'vue';
import ChildComponent from './ChildComponent.vue';
// 2. 类型定义 (Props, Emits, etc.)
interface Props {
title: string;
items: string[];
}
// 3. defineProps, defineEmits, defineExpose
const props = withDefaults(defineProps<Props>(), {
title: 'Default Title',
});
const emit = defineEmits<{
(e: 'select', item: string): void;
}>();
defineExpose({ reset });
// 4. 响应式状态 (State)
const internalState = ref(0);
// 5. 计算属性 (Computed Properties)
const formattedTitle = computed(() => `--- ${props.title} ---`);
// 6. 侦听器 (Watchers)
watch(() => props.items, (newVal) => {
// ...
});
// 7. 生命周期钩子 (Lifecycle Hooks)
onMounted(() => {
// ...
});
// 8. 方法 (Methods)
function handleClick() {
emit('select', 'some-item');
}
function reset() {
internalState.value = 0;
}
</script>
8.2.3 Props & Emits
-
必须使用 TypeScript 进行类型定义,以获得最终的类型安全。
-
为
props
提供合理的默认值 (withDefaults
)。 -
事件 (
emits
) 必须明确定义其名称和载荷类型。
plaintext
// 【推荐】清晰、类型安全
interface Props {
user: User;
isVisible?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
isVisible: false,
});
const emit = defineEmits<{
(e: 'update:isVisible', value: boolean): void;
(e: 'submit', data: User): void;
}>();
// 【反例】类型不明确,难以维护
defineProps(['user', 'isVisible']);
defineEmits(['update:isVisible', 'submit']);
8.2.4 样式规范
-
强制使用
**<style scoped>**
避免全局样式污染。 -
若需修改子组件样式,优先使用 CSS 变量。万不得已时,使用
:deep()
选择器,但需谨慎。
8.3 组件通信标准
根据场景选择最合适的通信方式。
通信场景 | 推荐方式 | 优点 | 缺点/注意事项 |
---|---|---|---|
父 -> 子 | Props | 单向数据流,清晰易懂,性能好。 | 仅限直系父子。 |
子 -> 父 | Emits | 标准事件模式,解耦。 | 仅限直系父子。 |
祖先 -> 后代 | Provide / Inject | 避免 Props 逐层传递(Prop Drilling)。 | 数据来源不直观,可能导致耦合。 |
兄弟或任意组件 | Pinia (状态管理) | 集中式状态管理,数据流可预测,Devtools 支持强大。 | 增加了少量模板代码。 |
【反例】禁止使用 Event Bus (如 mitt.js) 虽然 Event Bus 能实现任意组件通信,但它会导致数据流混乱,难以追踪和调试,在大型项目中是灾难性的。Pinia 是其完美的替代方案。
9. 单元测试规范
单元测试是代码质量的守护神。我们使用 Vitest 作为测试框架,Vue Test Utils 作为组件测试工具库。
9.1 测试标准
-
公共组件、Hooks 函数、工具函数必须编写单元测试。
-
业务组件应测试其核心交互逻辑。
-
测试覆盖率目标:核心模块 > 90%,项目平均 > 80%。
9.2 编写规范
-
测试文件名 :
*.spec.ts
或*.test.ts
。 -
测试关注点 : 测试组件的"公共契约",即 Props、Events 和 Slots,而不是内部实现细节。
-
遵循 AAA 模式:
-
Arrange (安排): 准备测试环境,如挂载组件、设置 props。
-
Act (行动): 执行操作,如触发点击事件、修改输入框。
-
Assert (断言): 验证结果是否符合预期,如检查 DOM 变化、emit 事件。
-
9.3 示例
测试一个基础按钮组件 (BaseButton.spec.ts
)
plaintext
// 【正例】
import { mount } from '@vue/test-utils';
import { describe, it, expect } from 'vitest';
import BaseButton from '../BaseButton.vue';
describe('BaseButton.vue', () => {
// 测试点1:渲染
it('should render the slot content', () => {
// Arrange
const wrapper = mount(BaseButton, {
slots: {
default: 'Click Me',
},
});
// Assert
expect(wrapper.text()).toBe('Click Me');
});
// 测试点2:交互 (emit)
it('should emit a click event when clicked', async () => {
// Arrange
const wrapper = mount(BaseButton);
// Act
await wrapper.trigger('click');
// Assert
expect(wrapper.emitted()).toHaveProperty('click');
expect(wrapper.emitted('click')).toHaveLength(1);
});
// 测试点3:属性 (props)
it('should be disabled when the disabled prop is true', async () => {
// Arrange
const wrapper = mount(BaseButton, {
props: {
disabled: true,
},
});
// Assert
expect(wrapper.attributes('disabled')).toBeDefined();
// Act
await wrapper.trigger('click');
// Assert
expect(wrapper.emitted('click')).toBeUndefined();
});
});
plaintext
// 【反例】测试内部实现
it('should have a specific internal class when hovered', () => {
// 这是一个糟糕的测试,因为它依赖于组件的内部样式实现,
// 一旦样式类名改变,测试就会失败,即使组件功能完好。
});
10. 代码审查 (Code Review) 标准
Code Review (CR) 是保证代码质量、促进知识共享的关键环节。
10.1 CR 要点 (Checklist)
审查者应重点关注以下方面:
-
功能性 (Functionality): 代码是否正确地实现了需求?是否考虑了边界情况?
-
可读性 (Readability): 变量和函数命名是否清晰?逻辑是否易于理解?是否存在过于复杂的代码?
-
规范性 (Consistency): 是否遵循了本文档中定义的所有规范?
-
可维护性 (Maintainability): 代码是否易于修改和扩展?是否存在硬编码或"魔法数字"?
-
性能 (Performance) : 是否存在明显的性能问题?(如不必要的
watch
deep、过大的 VNode 渲染) -
测试 (Testing): 是否包含了必要的单元测试?测试用例是否有效且覆盖了关键逻辑?
-
注释 (Comments): 复杂的逻辑或"为什么"这么做的原因是否添加了注释?
11. 自动化与工程化
11.1 工具链
-
包管理器 :
pnpm
(推荐,速度快且节省磁盘空间) -
构建工具 :
Vite
-
代码规范 :
ESLint
(集成vue
,typescript
插件) -
代码格式化 :
Prettier
(与 ESLint 集成,解决格式冲突) -
Git 钩子 :
Husky
+lint-staged
-
Commit 规范 :
@commitlint/cli
Vue 3 前端工程化规范
11.2 自动化流程
11.2.1 本地开发 (Pre-commit)
通过 husky
和 lint-staged
配置 pre-commit
钩子,在代码提交前自动执行:
-
格式化 :
prettier --write
对暂存区的文件进行格式化。 -
代码检查 :
eslint --fix
对暂存区的文件进行语法和规范检查。 -
(可选)单元测试 :
vitest related --run
仅运行与修改文件相关的测试。
**package.json**
配置示例:
plaintext
{
"scripts": {
"prepare": "husky install"
},
"lint-staged": {
"*.{js,ts,vue}": ["eslint --fix", "prettier --write"],
"*.{scss,css}": ["stylelint --fix"]
}
}
11.2.2 持续集成 (CI/CD)
以 GitHub Actions 为例,配置工作流,在提交 Pull Request 时自动触发。
**.github/workflows/ci.yml**
:
plaintext
name: Frontend CI
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 8
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Lint and Format Check
run: pnpm lint
- name: Run Unit Tests
run: pnpm test:run --coverage # 运行测试并生成覆盖率报告
- name: Build Project
run: pnpm build
# 可选:上传覆盖率报告到 Codecov 等服务
# - name: Upload coverage reports to Codecov
# uses: codecov/codecov-action@v3
这个 CI 流程确保了每一份合入 develop
或 main
分支的代码都经过了严格的自动化校验,极大地保障了主干分支的健康度。