Files
2026-07-25 23:45:09 +08:00

182 lines
4.0 KiB
Markdown

# Vue 代码范式
仅在项目版本和既有约定支持时使用以下范式。复制前替换领域名称、错误处理和组件库 API。
## 类型安全组件契约
```vue
<script setup lang="ts">
import { computed } from 'vue'
interface UserSummary {
id: string
displayName: string
}
interface Props {
user: UserSummary
selected?: boolean
}
const props = withDefaults(defineProps<Props>(), {
selected: false,
})
const emit = defineEmits<{
select: [userId: string]
}>()
const accessibleName = computed(() => `选择用户 ${props.user.displayName}`)
function handleSelect(): void {
emit('select', props.user.id)
}
</script>
<template>
<button
type="button"
:aria-pressed="selected"
:aria-label="accessibleName"
@click="handleSelect"
>
{{ user.displayName }}
</button>
</template>
```
要点:
- 将 props 和 emits 作为公共契约。
- 使用稳定业务 id 发出事件,不把完整可变内部对象泄露出去。
- 使用原生 button 获得键盘和语义能力。
- 若项目 Vue 版本支持且团队采用响应式 props 解构,可用等价的解构默认值写法。
## 带竞态保护的 composable
```ts
import { readonly, ref, toValue, watch, type MaybeRefOrGetter } from 'vue'
interface User {
id: string
name: string
}
interface UserService {
getUser(userId: string, signal: AbortSignal): Promise<User>
}
export function useUser(
userId: MaybeRefOrGetter<string>,
service: UserService,
) {
const data = ref<User | null>(null)
const error = ref<Error | null>(null)
const isLoading = ref(false)
watch(
() => toValue(userId),
async (currentUserId, _previousUserId, onCleanup) => {
const controller = new AbortController()
onCleanup(() => controller.abort())
isLoading.value = true
error.value = null
try {
data.value = await service.getUser(currentUserId, controller.signal)
} catch (cause: unknown) {
if (!controller.signal.aborted) {
error.value = cause instanceof Error
? cause
: new Error('Unknown user loading error')
}
} finally {
if (!controller.signal.aborted) {
isLoading.value = false
}
}
},
{ immediate: true },
)
return {
data: readonly(data),
error: readonly(error),
isLoading: readonly(isLoading),
}
}
```
要点:
- 通过依赖参数隔离网络边界,便于测试。
- 对参数变化取消旧请求,避免过期结果覆盖新状态。
- 只暴露 readonly 状态。
- 实际项目应使用已有错误归一化,不重复定义通用错误层。
## 最小 Pinia Store
```ts
import { computed, ref } from 'vue'
import { defineStore } from 'pinia'
export const useCartStore = defineStore('cart', () => {
const itemIds = ref<string[]>([])
const itemCount = computed(() => itemIds.value.length)
function addItem(itemId: string): void {
if (!itemIds.value.includes(itemId)) {
itemIds.value.push(itemId)
}
}
function clear(): void {
itemIds.value = []
}
return {
itemIds,
itemCount,
addItem,
clear,
}
})
```
要点:
- 只保存不可推导的最小 state。
- 用 computed 表达 getter。
- 通过 action 表达状态变化。
- 真实 store 若执行请求,需补齐 loading、error、并发与恢复策略。
## 行为导向组件测试
```ts
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import UserSelectButton from './UserSelectButton.vue'
describe('UserSelectButton', () => {
it('emits the selected user id when activated', async () => {
const wrapper = mount(UserSelectButton, {
props: {
user: { id: 'user-42', displayName: 'Ada' },
},
})
await wrapper.get('button').trigger('click')
expect(wrapper.emitted('select')).toEqual([['user-42']])
})
})
```
要点:
- 通过用户可操作元素交互。
- 验证公开事件和业务结果,不访问组件内部 ref。
- 对无障碍名称、loading、disabled 和 error 等关键状态按变更补充断言。