CSS
优先使用 **scoped**
防止样式污染全局,每个组件样式必须局部化。
错误示例:无作用域
1<style> 2.button { 3 color: red; 4} 5</style> 6
不加 scoped 会影响全局所有 .button
正确示例:使用 scoped
1<style scoped> 2.button { 3 color: red; 4} 5</style> 6
限制嵌套层级 ≤ 3 层
嵌套超过 3 层说明选择器设计有问题,建议拆分样式或使用 BEM。
错误示例:嵌套过深(5 层)
1.card { 2 .header { 3 .title { 4 .icon { 5 span { 6 color: red; 7 } 8 } 9 } 10 } 11} 12
正确方式是进行合理拆分
避免使用 !important
!important 会带来样式权重混乱,除非必要不推荐使用 !important
错误示例
1.button { 2 color: red !important; 3} 4 5.alert { 6 display: none !important; 7} 8
正确示例:提升选择器权重
1/* 通过增加父级选择器权重覆盖 */ 2.container .button { 3 color: red; 4} 5
合理使用 v-deep
在 Vue3 中,如果要覆盖子组件或第三方库的内部样式,必须使用 ::v-deep。禁止使用老版的 /deep/ 或 >>>,因为它们已废弃。同时要避免滥用 ::v-deep,只在必要时使用,并保持选择器短小。
错误示例
1<style scoped> 2.child-component .btn { 3 color: red; 4} 5</style> 6 7
正确示例
1<style scoped> 2::v-deep(.btn) { 3 color: red; 4} 5</style> 6
优先使用 UnoCSS
因为项目中引入 UnoCSS,首选使用 UnoCSS。
错误示例
使用了传统的 CSS 类名来定义样式,而不是利用 UnoCSS 的原子化类。这违背了优先使用 UnoCSS 的原则。
1<template> 2 <div class="my-button"> 3 点击我 4 </div> 5</template> 6 7<style scoped> 8.my-button { 9 background-color: #007bff; 10 color: white; 11 padding: 10px 20px; 12 border-radius: 5px; 13 cursor: pointer; 14} 15</style> 16
正确示例
充分利用了 UnoCSS 的原子化类来定义相同的样式
1<template> 2 <div class="bg-blue-500 text-white p-x-5 p-y-2 rounded-md cursor-pointer"> 3 点击我 4 </div> 5</template> 6 7<style scoped> 8/* 无需额外的 style 标签,因为样式已通过 UnoCSS 类名定义 */ 9</style> 10
JavaScript
变量与方法应采用统一的命名规范
命名应遵循语义清晰、风格一致、可读性高的原则,变量名体现数据类型/用途,方法名体现行为。团队建议统一小驼峰(camelCase) 风格,并避免无意义缩写或混用语言。
错误示例:变量命名不语义化
1let a = true; 2let b = []; 3let c = "http://api.example.com"; 4
正确示例如下
- 语义化命名
1let isActive = true; 2let userList: User[ ] = [ ]; 3const API_BASE_URL = "http://api.example.com";
- 方法名包含动词
1function fetchData() { ... } 2function saveUser() { ... } 3function deleteUser() { ... }
- 布尔值变量以 is/has 开头
1const isVisible = false; 2const hasError = true; 3 4
> 1. 布尔值遵循语法准确是前提 2 推荐用 has 开头
dev.to/michi/tips-… 参考写布尔值的工具
使用可选链
当访问对象的深层次属性时,如果中间某一级可能为 null 或 undefined, (?.) 替代传统的逐层判断,代码更简洁且避免运行时异常。
错误示例
1if ( 2 response && 3 response.data && 4 response.data.user && 5 response.data.user.profile 6) { 7 console.log(response.data.user.profile.name); 8} 9
上面的代码示例中判断条件冗长且可读性差、容易漏掉某一级判断和难以维护
正确示例
1const name = response?.data?.user?.profile?.name; 2if (name) { 3 console.log(name); 4} 5
函数参数超过 3 个应封装成对象
当函数参数 超过 3 个 或存在多个相同类型的参数时,推荐将这些参数封装为一个对象。这样可以提升代码可读性、维护性,并支持命名参数调用,避免顺序错误。
错误示例:多个参数直接传递
1function createUser( 2 name: string, 3 age: number, 4 role: string, 5 isActive: boolean, 6 department: string 7) { 8 // 创建用户逻辑 9} 10 11createUser("Alice", 28, "admin", true, "Engineering"); 12
正确示例
1interface CreateUserOptions { 2 name: string; 3 age: number; 4 role: string; 5 isActive?: boolean; 6 department?: string; 7} 8 9function createUser(options: CreateUserOptions) { 10 const { name, age, role, isActive = true, department = "General" } = options; 11 // 创建用户逻辑 12} 13 14createUser({ 15 name: "Alice", 16 age: 28, 17 role: "admin", 18 department: "Engineering", 19}); 20
使用 ResizeObserver 替代 onResize
window.onresize 只能监听浏览器窗口尺寸变化,无法感知单个 DOM 元素尺寸变化。Vue3 项目应使用 ResizeObserver 监听任意 DOM 元素的尺寸变化,支持多元素、精准触发、性能更优。
错误示例
1<script setup lang="ts"> 2const width = ref(0); 3 4onMounted(() => { 5 window.onresize = () => { 6 const el = document.getElementById("container"); 7 width.value = el?.offsetWidth || 0; 8 }; 9 window.onresize(); // 初始化 10}); 11</script> 12 13<template> 14 <div id="container" style="width: 50%;">宽度:{{ width }}px</div> 15</template> 16
问题
- 无法感知父容器/内容变化,只能在窗口尺寸变化时触发。
- 多组件绑定 window.onresize 时,回调容易互相覆盖。
- 卸载时忘记移除监听,可能导致内存泄漏。
正确示例
1<script setup lang="ts"> 2const elRef = ref<HTMLDivElement>(); 3const size = ref({ width: 0, height: 0 }); 4 5onMounted(() => { 6 const observer = new ResizeObserver((entries) => { 7 const rect = entries[0].contentRect; 8 size.value.width = rect.width; 9 size.value.height = rect.height; 10 }); 11 observer.observe(elRef.value!); 12 13 onUnmounted(() => observer.disconnect()); 14}); 15</script> 16 17<template> 18 <div ref="elRef" style="width: 50%;"> 19 宽度:{{ size.width }}px,高度:{{ size.height }}px 20 </div> 21</template> 22
TypeScript
避免在组件/逻辑中使用 any
在 Vite + TS 项目里,一旦滥用 any,类型检查形同虚设。要尽量用明确类型或 unknown(再做类型收窄)。
错误示例
1function parseData(data: any) { 2 return JSON.parse(data); 3} 4 5const user: any = getUser(); 6console.log(user.name); 7
正确示例
1function parseData(data: unknown): Record<string, unknown> { 2 if (typeof data === "string") { 3 return JSON.parse(data); 4 } 5 throw new Error("Invalid data type"); 6} 7 8interface User { 9 name: string; 10 age: number; 11} 12const user: User = getUser(); 13console.log(user.name); 14
目前有两种情况,
- stores 中没有写类型 (旧的不补类型,新的 stores 补类型) 新接口,新枚举,新常量
使用 enum 避免硬编码
所有固定集合值(角色、状态、方向等)必须使用 TypeScript 的 enum 定义,禁止使用字符串字面量或硬编码。
错误示例:硬编码字符串
1if (user.role === 'admin') { ... } 2if (status === 'PENDING') { ... } 3
正确示例:使用 enum
1enum UserRole { 2 Admin = 'admin', 3 User = 'user', 4 Guest = 'guest' 5} 6 7enum OrderStatus { 8 Pending = 'PENDING', 9 Shipped = 'SHIPPED', 10 Delivered = 'DELIVERED' 11} 12 13if (user.role === UserRole.Admin) { ... } 14if (status === OrderStatus.Pending) { ... } 15
Props、Emits 必须类型化
在 Vue3 的 SFC 中,defineProps 和 defineEmits 必须声明类型。
错误示例
1defineProps(['title', 'count']) 2defineEmits(['update']) 3
正确示例
1interface Props { 2 title: string 3 count: number 4} 5 6interface Emits { 7 (e: 'update', value: number): void 8} 9 10const props = defineProps<Props>() 11const emit = defineEmits<Emits>() 12
3.3 以上有另一种方式
泛型必须具备边界约束
使用泛型时必须加上约束,防止过宽的类型导致不安全操作。
错误示例
1function getValue<T>(obj: T, key: string) { 2 return obj[key] 3} 4
正确示例
1function getValue<T extends object, K extends keyof T>(obj: T, key: K): T[K] { 2 return obj[key] 3} 4
组合式 API 必须有返回值类型
composables API 应该明确返回值类型,方便调用处类型推断。
错误示例
1export function useUser() { 2 const user = ref<User>() 3 return { user } 4} 5
正确示例
1export function useUser(): { user: Ref<User> } { 2 const user = ref<User>() 3 return { user } 4} 5
Vue3
不要在 defineProps() 里混用类型和 runtime 校验
Vue3 允许 defineProps() 使用 runtime 声明和类型声明,但二者混用易出 bug。推荐统一使用 泛型声明类型。
错误示例
1<script setup lang="ts"> 2defineProps({ 3 title: String, 4}); 5interface Props { 6 title: string; 7} 8</script> 9
正确示例
1<script setup lang="ts"> 2interface Props { 3 title: string; 4} 5const props = defineProps<Props>(); 6</script> 7
类型声明统一放在 types 文件夹或模块中
全局类型或接口建议集中管理,避免散落在组件里难以维护。
错误示例
1// 在多个组件里重复定义 interface User { name: string; age: number } 2
正确示例
1src/types/user.d.ts 2
1export interface User { name: string age: number } 2
在模板中使用类型提示
通过 defineExpose 和 defineEmits 的泛型参数在模板中获得类型提示。
错误示例
1<template> 2 <button @click="emit('save', 123)">Save</button> 3</template> 4 5<script setup lang="ts"> 6const emit = defineEmits(["save"]); 7</script> 8
正确示例
1<script setup lang="ts"> 2const emit = defineEmits<{ 3 (e: "save", id: number): void; 4}>(); 5</script> 6
优先使用 <script setup> 而不是 defineComponent
Vue 3 的 <script setup> 更简洁、性能更好(编译优化),避免不必要的模板变量暴露。
错误示例
1<script lang="ts"> 2import { defineComponent, ref } from "vue"; 3 4export default defineComponent({ 5 setup() { 6 const count = ref(0); 7 return { count }; 8 }, 9}); 10</script> 11 12
正确示例
1<script setup lang="ts"> 2import { ref } from "vue"; 3 4const count = ref(0); 5</script> 6
在模板中避免复杂逻辑表达式
模板里只做展示,不要做复杂逻辑,逻辑应移到计算属性或方法。
错误示例
1<template> 2 <div> 3 {{ 4 users 5 .filter((u) => u.age > 18) 6 .map((u) => u.name) 7 .join(", ") 8 }} 9 </div> 10</template> 11
正确示例
1<script setup lang="ts"> 2const adultNames = computed(() => 3 users.value 4 .filter((u) => u.age > 18) 5 .map((u) => u.name) 6 .join(", ") 7); 8</script> 9 10<template> 11 <div>{{ adultNames }}</div> 12</template> 13
事件名统一使用 kebab-case
Vue 3 推荐自定义事件名用 kebab-case,避免与 DOM 属性冲突。
错误示例
1<ChildComponent @saveData="handleSave" /> 2
正确示例
1<ChildComponent @save-data="handleSave" /> 2
组件通信避免滥用 $emit,优先使用 props + v-model
小型数据通信用 props/v-model,大型数据或频繁通信建议使用 Pinia/Composable。
错误示例
1<ChildComponent @updateValue="parentValue = $event" /> 2
正确示例
1<ChildComponent v-model="parentValue" /> 2
避免复杂嵌套三元运算
三元表达式适合简单条件切换,若逻辑复杂或嵌套,应使用 if-else、computed 或方法代替。 在模板中,复杂三元表达式严重降低可读性,且容易遗漏分支,Review 时应强制重构
错误示例
1<template> 2 <div> 3 {{ status === "loading" ? "加载中" : status === "error" ? "错误" : "完成" }} 4 </div> 5</template> 6
正确示例
1<script setup lang="ts"> 2const statusText = computed(() => { 3 if (status.value === "loading") return "加载中"; 4 if (status.value === "error") return "错误"; 5 return "完成"; 6}); 7</script> 8 9<template> 10 <div>{{ statusText }}</div> 11</template> 12
定时器必须在卸载时清理
在 Vue 组件中使用 setInterval、setTimeout、requestAnimationFrame 等定时器,必须在组件卸载(onUnmounted)时清理,否则会导致内存泄漏或意外触发逻辑
错误示例
1<script setup lang="ts"> 2onMounted(() => { 3 setInterval(() => { 4 console.log("轮询接口"); 5 }, 1000); 6}); 7</script> 8
正确示例
1<script setup lang="ts"> 2let timer: ReturnType<typeof setInterval>; 3 4onMounted(() => { 5 timer = setInterval(() => { 6 console.log("轮询接口"); 7 }, 1000); 8}); 9 10onUnmounted(() => { 11 clearInterval(timer); 12}); 13</script> 14
IO(API 请求、文件处理等)必须做错误处理
网络请求(fetch/axios)、文件操作等 IO 行为容易失败,必须捕获异常并反馈用户,防止应用无响应或白屏
错误示例
1const fetchData = async () => { 2 const res = await fetch("/api/data"); 3 const data = await res.json(); 4 console.log(data); 5}; 6
正确示例
1const fetchData = async () => { 2 try { 3 const res = await fetch("/api/data"); 4 if (!res.ok) throw new Error("请求失败"); 5 const data = await res.json(); 6 console.log(data); 7 } catch (err) { 8 console.error("数据请求错误:", err); 9 alert("网络错误,请稍后重试"); 10 } 11}; 12
避免数据竞态(Race Condition)
当组件内多次发起异步请求或副作用操作(如用户快速切换选项),后发出的请求可能比先发出的请求先返回,导致数据状态错乱。必须通过请求标记、AbortController 或最新响应检查防止。
错误示例 具体场景:用户快速切换 Item 1 → Item 2 → Item 1,可能 Item 1 的旧请求最后返回,把数据覆盖成错误值。
1<script setup lang="ts"> 2const selectedId = ref(1); 3const data = ref(null); 4 5watch(selectedId, async (id) => { 6 const res = await fetch([`/api/item/${id}`](https://xplanc.org/primers/document/zh/10.Bash/90.%E5%B8%AE%E5%8A%A9%E6%89%8B%E5%86%8C/EX.id.md)); 7 data.value = await res.json(); 8}); 9</script> 10 11<template> 12 <select v-model="selectedId"> 13 <option :value="1">Item 1</option> 14 <option :value="2">Item 2</option> 15 </select> 16 <div>{{ data }}</div> 17</template> 18
解决思路
正确示例 1:使用请求标记(Token)
1<script setup lang="ts"> 2const selectedId = ref(1); 3const data = ref(null); 4let requestToken = 0; 5 6watch(selectedId, async (id) => { 7 const token = ++requestToken; 8 const res = await fetch([`/api/item/${id}`](https://xplanc.org/primers/document/zh/10.Bash/90.%E5%B8%AE%E5%8A%A9%E6%89%8B%E5%86%8C/EX.id.md)); 9 if (token !== requestToken) return; // 旧请求,丢弃 10 data.value = await res.json(); 11}); 12</script> 13
正确示例 2:使用 AbortController
1<script setup lang="ts"> 2const selectedId = ref(1); 3const data = ref(null); 4let controller: AbortController; 5 6watch(selectedId, async (id) => { 7 controller?.abort(); // 中断上一个请求 8 controller = new AbortController(); 9 10 try { 11 const res = await fetch([`/api/item/${id}`](https://xplanc.org/primers/document/zh/10.Bash/90.%E5%B8%AE%E5%8A%A9%E6%89%8B%E5%86%8C/EX.id.md), { signal: controller.signal }); 12 data.value = await res.json(); 13 } catch (err) { 14 if (err.name !== "AbortError") console.error(err); 15 } 16}); 17</script> 18
正确示例 3:封装 Composable,统一竞态处理
1// composables/useSafeFetch.ts 2export function useSafeFetch() { 3 let controller: AbortController; 4 5 return async function safeFetch(url: string) { 6 controller?.abort(); 7 controller = new AbortController(); 8 const res = await fetch(url, { signal: controller.signal }); 9 return res.json(); 10 }; 11} 12
1<script setup lang="ts"> 2const { safeFetch } = useSafeFetch(); 3const data = ref(null); 4 5watch(selectedId, async (id) => { 6 data.value = await safeFetch([`/api/item/${id}`](https://xplanc.org/primers/document/zh/10.Bash/90.%E5%B8%AE%E5%8A%A9%E6%89%8B%E5%86%8C/EX.id.md)); 7}); 8</script> 9
列表渲染中不推荐使用索引作为 key
Vue 的虚拟 DOM 需要依赖 key 来准确地跟踪节点身份,保证列表渲染的高效与正确。**key** 必须唯一且稳定,通常来自数据的唯一标识字段(如数据库 ID)。避免使用数组索引 **index** 作为 **key**,除非数据列表静态且无增删排序需求。
错误示例:使用索引作为 key
1<template> 2 <ul> 3 <li v-for="(item, index) in items" :key="index"> 4 {{ item.name }} 5 </li> 6 </ul> 7</template> 8
正确示例:使用稳定唯一标识作为 key
1<template> 2 <ul> 3 <li v-for="item in items" :key="item.id"> 4 {{ item.name }} 5 </li> 6 </ul> 7</template> 8
国际化
- 代码中的文案一定要做国际化处理 (比如中文正则表达式搜索检查)
- 国际化后的文案由 PM 提供,PM 不提供,使用 ChatGPT/Cursor 处理后与 PM 一起校对(拿不准找 Perry )
- 标点符号与语言对应,比如英文中不能出现中文括号
- 新增的国际化内容设置独立命令空间或者全文检索,避免 key 冲突
- 国际化内容的 key 是英文短语,不能是中文
- PR 的 Code Review 中涉及国际化内容必须重点 review
正确示例
1export default { 2 'Administrator has enabled Multi-Factor Authentication (MFA)': 'El administrador ha habilitado la autenticación de múltiples factores (MFA)', 3 'Open your app store': 'Abre tu tienda de aplicaciones', 4}; 5 6
在组件中这样使用
1<li>{{ t('Open your app store') }}</li> 2
Vue 组件设计
统一组件命名 / 文件命名策略
统一组件名采用 PascalCase(或一致 kebab-case),基础组件保留 Base 前缀,名称应全拼避免缩写,提高可维护性
错误示例
1components/ 2 myComp.vue 3 btn.vue 4
正确示例
1components/ 2 MyComponent.vue 3 BaseButton.vue 4
在组件中这样使用
1<BaseButton/> 2
如果是 element-plus 组件库,可以使用如下的使用方式
1<el-button/> 2
统一文件夹(目录)命名规范
项目中的所有目录名称必须遵循统一的命名风格,确保路径清晰、可预测、跨平台无大小写冲突。
错误示例:目录命名混乱
1components/ 2 UserProfile/ 3 loginForm/ 4 Account_details/ 5 auth/ 6
正确示例:统一 kebab-case
1components/ 2 user-profile/ 3 login-form/ 4 account-details/ 5 auth/ 6
TS 文件名命名
项目中的 TS 文件命名应该是小驼峰格式
错误示例
1user-list.ts 2
正确示例
1userList.ts 2
组件的状态与 UI 分离
在 Vue3 组件开发中,所有数据处理逻辑(如 API 请求、数据格式化、状态管理等)应从 UI 层(模板 & 样式)中分离,放入 Composable、Store、Utils。模板只负责展示,逻辑放在单独模块便于测试、复用和维护。
错误示例
1<script setup lang="ts"> 2import { ref, onMounted } from "vue"; 3 4const users = ref([]); 5const loading = ref(false); 6const error = ref(""); 7 8onMounted(async () => { 9 loading.value = true; 10 try { 11 const res = await fetch("/api/users"); 12 users.value = await res.json(); 13 } catch (e) { 14 error.value = "加载用户失败"; 15 } finally { 16 loading.value = false; 17 } 18}); 19 20const formatName = (user) => `${user.firstName} ${user.lastName}`; 21</script> 22 23<template> 24 <div v-if="loading">加载中...</div> 25 <div v-else-if="error">{{ error }}</div> 26 <ul v-else> 27 <li v-for="user in users" :key="user.id"> 28 {{ formatName(user) }} 29 </li> 30 </ul> 31</template> 32
上面的示例中存在的问题
- API 请求逻辑、数据状态和格式化函数都在组件里
- 组件职责太多:UI + 业务逻辑 + 状态管理
- 无法复用 fetchUsers 和 formatName
正确示例 - 数据逻辑分离到 Composable
composables/useUsers.ts
1import { ref } from "vue"; 2 3export function useUsers() { 4 const users = ref([]); 5 const loading = ref(false); 6 const error = ref(""); 7 8 const fetchUsers = async () => { 9 loading.value = true; 10 try { 11 const res = await fetch("/api/users"); 12 users.value = await res.json(); 13 } catch (e) { 14 error.value = "加载用户失败"; 15 } finally { 16 loading.value = false; 17 } 18 }; 19 20 const formatName = (user) => `${user.firstName} ${user.lastName}`; 21 22 return { users, loading, error, fetchUsers, formatName }; 23} 24
UserList.vue
1<script setup lang="ts"> 2import { onMounted } from "vue"; 3import { useUsers } from "@/composables/useUsers"; 4 5const { users, loading, error, fetchUsers, formatName } = useUsers(); 6 7onMounted(fetchUsers); 8</script> 9 10<template> 11 <div v-if="loading">加载中...</div> 12 <div v-else-if="error">{{ error }}</div> 13 <ul v-else> 14 <li v-for="user in users" :key="user.id"> 15 {{ formatName(user) }} 16 </li> 17 </ul> 18</template> 19
正确的案例中,UI 专注展示,逻辑由 useUsers 管理、useUsers 可被其他组件复用、只需要测试 useUsers 方法就行
UI组件 vs 业务组件
UI组件(Button, Modal, Table):无业务逻辑,仅负责样式和交互
业务组件(UserList, OrderForm):封装具体业务逻辑,复用 UI 组件
错误示例:业务逻辑写在 UI 组件
1<!-- Button.vue --> 2<script setup> 3const handleSaveUser = async () => { 4 await api.saveUser() 5} 6</script> 7 8<template> 9 <button @click="handleSaveUser">保存</button> 10</template> 11 12
正确示例:UI 组件尽量保证纯组件
1<!-- Button.vue --> 2<template> 3 <button><slot /></button> 4</template> 5 6<!-- UserForm.vue --> 7<Button @click="saveUser">保存</Button> 8
在写业务组件的时候,利用Composition API 分离逻辑,把 API 调用、数据处理抽离到 composable 中
避免直接操作 DOM
除非必要尽量不要使用 document.querySelector 等直接操作 DOM
错误示例
1onMounted(() => { 2 const el = document.querySelector('.btn') 3 el?.addEventListener('click', () => { ... }) 4}) 5
正确示例
1<template> 2 <button @click="handleClick" class="btn">Click</button> 3</template> 4 5<script setup lang="ts"> 6function handleClick() { 7 // 处理逻辑 8} 9</script> 10
单元测试
1. 单元测试应覆盖核心业务逻辑,避免测试无意义的渲染细节
测试应聚焦于组件的行为和业务逻辑,而非仅仅验证静态的 DOM 结构,避免脆弱且维护成本高的测试。
错误示例
1// 测试仅验证 DOM 具体标签和类名,DOM 结构细节变动即破坏测试 2test('renders exact button markup', () => { 3 const wrapper = mount(MyButton) 4 expect(wrapper.html()).toBe('<button class="btn primary">Submit</button>') 5}) 6 7
正确示例
1// 测试按钮是否存在且包含正确文本,关注业务效果而非具体标签细节 2test('renders submit button', () => { 3 const wrapper = mount(MyButton) 4 const button = wrapper.find('button') 5 expect(button.exists()).toBe(true) 6 expect(button.text()).toBe('Submit') 7}) 8 9
2. 使用 Vue Test Utils 的异步渲染工具时,要正确等待 nextTick
Vue3 组件中很多行为是异步更新的,测试中操作后必须调用 await nextTick() 或使用 flushPromises() 等方法,确保断言是在 DOM 更新完成后进行。
错误示例
1test('click increments count', () => { 2 const wrapper = mount(Counter) 3 wrapper.find('button').trigger('click') 4 expect(wrapper.text()).toContain('Count: 1') // 断言过早,失败 5}) 6 7
正确示例
1import { nextTick } from 'vue' 2 3test('click increments count', async () => { 4 const wrapper = mount(Counter) 5 await wrapper.find('button').trigger('click') 6 await nextTick() 7 expect(wrapper.text()).toContain('Count: 1') 8}) 9 10
3. 事件触发测试必须确保事件正确被捕获并处理
测试组件自定义事件或原生事件时,需确保事件被正确监听,并使用 emitted() 方法断言事件触发,避免事件未触发测试通过的假象。
错误示例
1test('emits submit event', () => { 2 const wrapper = mount(FormComponent) 3 wrapper.find('form').trigger('submit') 4 expect(wrapper.emitted('submit')).toBeTruthy() // 可能事件未触发,但断言粗略 5}) 6 7
正确示例
1test('emits submit event once', async () => { 2 const wrapper = mount(FormComponent) 3 await wrapper.find('form').trigger('submit.prevent') 4 const submitEvents = wrapper.emitted('submit') 5 expect(submitEvents).toHaveLength(1) 6}) 7 8
4. 不要在测试中硬编码组件内部状态,尽量从外部输入和输出测试
单元测试应以组件的公开接口(props、事件)为测试点,避免直接访问或修改组件内部私有数据,保持测试的稳健性和解耦。
错误示例
1test('increments count internally', () => { 2 const wrapper = mount(Counter) 3 wrapper.vm.count = 5 4 wrapper.vm.increment() 5 expect(wrapper.vm.count).toBe(6) // 依赖内部状态 6}) 7 8
正确示例
1test('increments count via user interaction', async () => { 2 const wrapper = mount(Counter) 3 await wrapper.find('button.increment').trigger('click') 4 expect(wrapper.text()).toContain('Count: 1') 5}) 6 7
5. 避免在测试中使用复杂的真实 API 请求,应使用 Mock 或 Stub
测试时不应依赖外部接口的真实请求,推荐使用 jest.mock、msw、sinon 等模拟数据,保证测试的独立性和稳定性。
错误示例
1test('fetches data and renders', async () => { 2 const wrapper = mount(DataComponent) 3 await wrapper.vm.fetchData() // 真实请求导致测试不稳定 4 expect(wrapper.text()).toContain('Data loaded') 5}) 6 7
正确示例
1import axios from 'axios' 2jest.mock('axios') 3 4test('fetches data and renders', async () => { 5 axios.get.mockResolvedValue({ data: { items: ['a', 'b'] } }) 6 const wrapper = mount(DataComponent) 7 await wrapper.vm.fetchData() 8 expect(wrapper.text()).toContain('a') 9}) 10 11
6. 组件依赖的异步行为应通过 Mock 异步函数进行控制
若组件依赖异步方法(如定时器、异步 API),应在测试中 Mock 这些异步行为,避免测试时间过长或不稳定。
错误示例
1test('auto refresh updates data', async () => { 2 const wrapper = mount(AutoRefresh) 3 await new Promise(r => setTimeout(r, 5000)) // 测试过慢且不确定 4 expect(wrapper.text()).toContain('Refreshed') 5}) 6 7
正确示例
1jest.useFakeTimers() 2 3test('auto refresh updates data', async () => { 4 const wrapper = mount(AutoRefresh) 5 jest.advanceTimersByTime(5000) 6 await nextTick() 7 expect(wrapper.text()).toContain('Refreshed') 8 jest.useRealTimers() 9}) 10 11
7. 使用快照测试时应谨慎,避免大规模快照导致维护困难
快照测试适合对关键 UI 做稳定性检测,但不应滥用,避免包含无关紧要的 DOM 变动。
错误示例
1test('renders full component snapshot', () => { 2 const wrapper = mount(ComplexComponent) 3 expect(wrapper.html()).toMatchSnapshot() // 快照过大,难维护 4}) 5 6
正确示例
1test('renders header snapshot only', () => { 2 const wrapper = mount(ComplexComponent) 3 expect(wrapper.find('header').html()).toMatchSnapshot() 4}) 5 6
8. 单元测试中避免使用全局依赖,推荐注入依赖或使用 provide/inject Mock
Vue3 组件可能依赖全局插件或 provide/inject,测试时应 Mock 这些依赖,避免测试受全局状态影响。
错误示例
1test('uses global i18n', () => { 2 const wrapper = mount(ComponentUsingI18n) 3 expect(wrapper.text()).toContain('Hello') // 依赖真实 i18n,环境复杂 4}) 5 6
正确示例
1import { createI18n } from 'vue-i18n' 2 3const i18n = createI18n({ locale: 'en', messages: { en: { hello: 'Hello' } } }) 4 5test('uses mocked i18n', () => { 6 const wrapper = mount(ComponentUsingI18n, { 7 global: { plugins: [i18n] } 8 }) 9 expect(wrapper.text()).toContain('Hello') 10}) 11
《一份实用的Vue3技术栈代码评审指南》 是转载文章,点击查看原文。
