first commit
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
# 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 等关键状态按变更补充断言。
|
||||
|
||||
Reference in New Issue
Block a user