一份实用的Vue3技术栈代码评审指南

作者:至简简日期:2025/11/8

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

正确示例如下

  1. 语义化命名
1let isActive = true;  
2let userList: User[ ] = [ ];  
3const API_BASE_URL = "http://api.example.com";  
  1. 方法名包含动词
1function fetchData() { ... }  
2function saveUser() { ... }  
3function deleteUser() { ... }  
  1. 布尔值变量以 is/has 开头
1const isVisible = false;
2const hasError = true;
3
4

> 1. 布尔值遵循语法准确是前提 2 推荐用 has 开头

dev.to/michi/tips-… 参考写布尔值的工具

使用可选链

当访问对象的深层次属性时,如果中间某一级可能为 nullundefined, (?.) 替代传统的逐层判断,代码更简洁且避免运行时异常。

错误示例

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

目前有两种情况,

  1. 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 中,definePropsdefineEmits 必须声明类型。

错误示例

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

国际化

  1. 代码中的文案一定要做国际化处理 (比如中文正则表达式搜索检查)
  2. 国际化后的文案由 PM 提供,PM 不提供,使用 ChatGPT/Cursor 处理后与 PM 一起校对(拿不准找 Perry )
  3. 标点符号与语言对应,比如英文中不能出现中文括号
  4. 新增的国际化内容设置独立命令空间或者全文检索,避免 key 冲突
  5. 国际化内容的 key 是英文短语,不能是中文
  6. 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技术栈代码评审指南》 是转载文章,点击查看原文


相关推荐


Ant Design Landing模版使用教程-react-npm
I like Code?2025/11/4

特此鸣谢:https://github.com/ant-motion/ant-design-3.x-landing-page?tab=readme-ov-file 官网(不好用):https://landing.ant.design/docs/introduce-cn package.json代码如下 { "private": true, "entry": { "index": "./index.js" }, "dependencies": { "antd"


硬件岗位基础知识
千語萬言-2025/10/31

1. 为什么 I2C 要上拉电阻? 因为 I2C 芯片的物理输出是“开漏输出”,它自身无法输出高电平,需要上拉电阻来提供高电平。 a) 电气结构:开漏输出 I2C 总线上的每一个设备(主设备和从设备),其 SDA(数据线)和 SCL(时钟线)的物理输出级都是一个 开漏输出 或 开集输出 的电路。 b) 上拉电阻的作用 提供高电平、限流保护 2.解释 交流电 和 直流电 直流电 (DC - Direct Current) 含义:电流的方向和大小不随时间变化。 交流电 (A


C#.NET DbContext 池化机制深入解析:提升 EF Core 性能的关键
唐青枫2025/10/29

简介 DbContext 池是 Entity Framework Core 中的高性能数据库连接管理机制,通过重用已初始化的 DbContext 实例,显著减少创建和销毁上下文对象的开销,特别适合高并发场景。尤其在高并发场景(如 Web API)中,频繁创建和释放 DbContext 会导致: 性能瓶颈:实例化 DbContext 涉及反射、元数据初始化和连接池分配。 内存压力:频繁创建和释放会导致垃圾回收(GC)压力。 连接管理问题:不恰当的 DbContext 生命周期可能导致数


Stream flatMap详解与应用实战
IT橘子皮2025/10/26

Stream API 中的 flatMap方法是一个功能强大但有时会让人感到困惑的工具。它专为处理嵌套结构或"一对多"元素映射场景而设计,能将复杂的集合层次"拍平"为单一流。下面我们深入解析其核心原理、典型应用及实战技巧。 ​核心原理:先映射,后扁平​ flatMap的核心思想是 ​​"先映射(Map),后扁平化(Flatten)"​​ 。 ​映射(Map)​​:它对输入流 Stream<T>中的每个元素应用一个映射函数。这个函数的关键在于,它不接受一个普通的对象,而是必须返回一个 Strea


Python 的内置函数 compile
IMPYLH2025/10/23

Python 内建函数列表 > Python 的内置函数 compile Python 的内置函数 compile() 是一个强大的工具,它允许将源代码编译为代码对象或 AST(抽象语法树)对象。该函数主要用于动态执行 Python 代码,常见于需要运行时编译代码的场景。 基本语法 compile(source, filename, mode, flags=0, dont_inherit=False, optimize=-1) ''' 将字符串或文件编译成代码或 AST 对


驾校管理系统|基于java和小程序的驾校管理系统设计与实现(源码+数据库+文档)
伟庭大师兄2025/10/22

驾校管理系统平台 目录 基于java和小程序的驾校管理系统设计与实现 一、前言 二、系统设计 三、系统功能设计 四、数据库设计 五、核心代码  六、论文参考 七、最新计算机毕设选题推荐 八、源码获取: 博主介绍:✌️大厂码农|毕设布道师,阿里云开发社区乘风者计划专家博主,CSDN平台Java领域优质创作者,专注于大学生项目实战开发、讲解和毕业答疑辅导。✌️ 主要项目:小程序、SpringBoot、SSM、Vue、Html、Jsp、Nodejs等


C#.NET FluentValidation 全面解析:优雅实现对象验证
唐青枫2025/10/21

简介 FluentValidation 是一个基于“流式 API”(Fluent API)的 .NET 验证框架,用于在应用层对模型(DTO、ViewModel、Entity 等)进行声明式验证。 核心优势: 高可读性:通过链式方法配置验证规则,逻辑清晰; 可复用:将验证代码从业务逻辑中分离,易于单元测试; 丰富的内置规则:邮箱、长度、正则、多字段联动、集合验证等; 可扩展:支持自定义验证器、异步验证、跨属性验证。 适用场景: Web API 模型验证 复


解密prompt系列62. Agent Memory一览 - MATTS & CFGM & MIRIX
风雨中的小七2025/10/20

今天我们再来聊聊AI智能体中至关重要的组件——记忆系统,它能有效避免的Agent像只只有7秒记忆的金鱼,不断重复错误,循环往复。 记忆的两种面孔:LLM Memory vs Agent Memory 之前我们探讨过Mem0和LlamaIndex对大模型记忆的工程化实现,但这两个库更侧重于LLM Memory而非Agent Memory。这两者有何不同?本质上Agent Memory是包含了LLM Memory的。那增量的差异来自 LLM Memory:更像是事实备忘录,记录对话中的具体事实和场


Docker 实战教程(7) | 镜像管理和仓库操作
致宏Rex2025/10/18

前边篇章我们已经介绍了 Docker 基础概念和安装Docker 常用命令实践Docker 网络机制详解Docker 数据卷和挂载Dockerfile 编写和镜像构建Docker Compose 多容器编排 本篇为系列最后一章,介绍 Docker 的镜像管理和仓库操作。 本教程侧重于命令实践和理解,提供可在本地环境测试的实例,每章结束都有总结要点。 7.1 Docker 镜像管理基础 我们之前介绍过一期自建镜像站的若干方案: Docker 管理 | 代理配置、内网共享和 Ha


Redis(67)Redis的SETNX命令是如何工作的?
Victor3562025/10/17

SETNX 是 Redis 提供的一个原子性命令,用于在键不存在时设置键值对。SETNX 是 "SET if Not eXists" 的缩写,主要应用于实现分布式锁等场景。下面是详细的工作原理以及如何在 Java 中使用 SETNX 命令的示例。 SETNX 命令的工作原理 检查键是否存在:Redis 首先检查给定的键是否存在。 设置键值对:如果键不存在,Redis 将设置键值对,并返回 1。 不做任何操作:如果键已经存在,Redis 不会设置新的值,并返回 0。 Java 使用 SETNX

首页编辑器站点地图

Copyright © 2025 聚合阅读

License: CC BY-SA 4.0