frontend — 为每位用户、每种状态提供可靠的屏幕,并易于修改
先遵循仓库的 CLAUDE.md、docs/conventions.md 和 docs/design-system.md;本技能旨在补充而非覆盖这些文档。视觉工作(设计令牌、布局、颜色、CSS)归属于 css-design;以用户身份运行应用以及最终裁决由 verify-app 负责。
选择技术栈。 Vue 3 和 React 均为一级公民,遵循相同的规则。使用仓库已有的框架;对于新应用,使用交付配置文件中的前端默认值(参见仓库 README),否则在分流阶段询问一次。Vue:TypeScript + Vite (npm create vue@latest -- --ts --router --pinia --vitest --playwright --eslint --prettier),参考 references/vue-patterns.md。React:TypeScript + Vite(或仓库的元框架),参考 references/react-patterns.md。切勿在一个应用中混合使用框架。需要 SSR 或 SSG 来优化 SEO?请在 ADR(architect)中记录 Nuxt 或 Next.js;本技能无论在哪种情况下都涵盖客户端部分。
工作流程
- 加载仓库约定和清单。 读取
CLAUDE.md、docs/conventions.md、docs/design-system.md;列出可复用部分(ls src/components src/features src/composables src/hooks src/stores)。检测数据层、路由、表单和国际化库(Vue:grep -E '"(@tanstack/vue-query|pinia|vue-router|vee-validate|vue-i18n)"' package.json;React:grep -E '"(@tanstack/react-query|@tanstack/react-router|react-router|react-hook-form|i18next|react-intl)"' package.json)。切勿添加第二个数据层、路由、状态管理、表单或国际化库。证据: 故事描述中列出已复用的组件、组合式函数/Hook 以及新增的任何内容,并说明原因。 - 在编写代码之前编写状态表。 对于每个视图:列出每一块数据及其位置——服务器(查询)、URL(路由参数/搜索)、共享客户端(Pinia store 或 React context 或仓库的 store)、本地(
ref/useState)、派生(computed/ render 中的 const)、外部(组合式函数或包装浏览器 API 的useSyncExternalStore)。然后列出四种状态(加载、空、错误、成功)以及每种状态的复制键。证据: 故事中的表格。 - 先编写会失败的测试。 Vitest +
@testing-library/vue或@testing-library/react、userEvent.setup()、按角色和可访问名称进行查询、每个测试一个行为、在服务边界伪造网络(MSW 或仓库在src/test-utils中的伪造客户端)。覆盖每种状态、键盘路径和一个负面案例。证据: 运行结果显示因正确原因而失败。 - 在 feature 文件夹内构建。
src/features/<name>/services(类型化的 API 调用,无框架)→composables/hooks(useX包装useQuery/useMutation和一个键工厂)→components(Name.vue包含<script setup lang="ts">,或Name.tsx)→ 组装它们的路由视图。属性和事件具有类型,无any,无非空!,使用@/导入。证据:git diff --stat显示更改仅限于该 feature 以及预期的共享更改。 - 复制和格式化。 所有可见字符串来自本地化文件(vue-i18n 的
useI18n(),或 React 仓库的国际化库或复制模块),每种语言环境具有相同的键;测试强制键的一致性。数字和日期通过国际化格式化器或Intl。证据: 键一致性测试通过;无硬编码复制,由 lint 强制(@intlify/vue-i18n/no-raw-text;React:react/jsx-no-literals或国际化插件的规则)——引用 lint 退出代码。 - 使用
css-design设置样式。 仅使用设计令牌,BEM 块名等于组件名,为每种状态设置样式;.vue样式通过postcss-html进行 lint。证据: Stylelint 通过。 - 故事(组件库)。 每种状态一个故事,两种主题;Storybook axe 检查通过。证据: 故事测试和 axe 检查通过。
- 门控,按退出代码链式执行。
npm run lint && npm run type-check && npm run test:unit -- --run(Vue:vue-tsc --build;React:tsc -b),或make ci;切勿仅根据| tail判断。证据: 退出代码为 0 且覆盖率等于或高于仓库阈值。 - 在真实浏览器中针对生产构建进行旅程测试。
npm run build并提供输出服务(vite preview,或仓库使用的静态服务器)。为主要路径添加或扩展 Playwright 规范,并包含@axe-core/playwright(标签wcag2a wcag2aa wcag21aa wcag22aa,严重和关键错误数为 0)。捕获docs/delivery/<slug>/evidence/<view>-390.png和-1280.png,如果应用有暗色主题,还需捕获暗色主题截图。在引用每张截图前先打开它。证据: 规范通过,PNG 图片存在。 - 键盘和屏幕阅读器通过。 使用 Tab 键浏览新视图:顺序、可见焦点、Escape 关闭覆盖层且焦点返回到触发元素;导航后焦点落在新标题上且标题已更改。使用屏幕阅读器(VoiceOver
Cmd+F5,或 NVDA)抽查名称和公告。证据: 故事 QA 部分中的三行注释。 - 移交。 运行
verify-app进行端到端验证;在deliver-loop中,独立审查对项目进行评分。此处不再重复其步骤。
核心检查清单
- 不存储可以派生的内容:使用
computed(React:render 中的 const);不编写仅用于设置其他状态的 watcher 或 effect。 - Watcher 和 effect 仅用于与外部同步(DOM API、订阅、定时器、第三方小部件)并在完成后清理(
onWatcherCleanup/onUnmounted;React:effect 的返回函数)。由用户操作引起的工作应在事件处理程序中运行。 - 服务器数据从不复制到本地状态或 store 中;查询键包含获取数据时使用的每个输入(在 Vue 中作为 ref 或 getter,以便数据变化时重新获取)。
- 用户可能分享或重新加载的选择、筛选、标签和分页应位于 URL 查询中。
- 每个数据视图都渲染加载、空、错误(带重试)和成功状态;测试覆盖所有四种状态。
- 每个输入都有可见的
<label for>(两个框架中均使用useId()生成 id);错误通过aria-describedby关联文本,保留输入值,提交时焦点移动到第一个无效字段。 - 优先使用原生元素(
button、a href/路由链接、dialog、details、input type);不使用div @click/div onClick。 - 自定义小部件遵循相应的 WAI-ARIA APG 模式,包括键盘支持。
- 不移动焦点的异步结果通过一个
role="status"/role="alert"区域宣布,该区域仅渲染一次且内容为空(Vue:teleport 到body;React:portal,如果它必须比视图存活更久)。 - 路由变化:更新
document.title并将焦点移动到页面h1或main(Vue:router.afterEach+ 模板引用;React:路由器的等效实现)。 - 每个路由和每个有风险的小部件都要捕获错误(Vue:
onErrorCaptured边界组件;React:错误边界)并显示重试,绝不显示空白页面。 - 无硬编码显示字符串;每种语言环境具有相同的键。
- 测试按角色/标签/文本查询,使用
user-event交互,并且从不断言类名、组件内部(wrapper.vm、状态值)或组合式函数/Hook 调用。 - 无
v-html/dangerouslySetInnerHTML,除非在使用前立即用 DOMPurify 进行消毒;来自数据的 URL 被列入白名单为https:/mailto:/相对路径。 - 仅在存在性能分析测量或记录的需求时才使用
shallowRef/markRaw/v-memo/KeepAlive(React:memo/useMemo/useCallback)。 - 图表和数据可视化具有可访问的名称和摘要、数据表(或文本)替代方案、色盲安全调色板(系列之间还可通过标签、形状或图案区分),以及对任何交互的键盘访问——无论仓库使用哪个图表库。
反模式
Vue:
watch(items, v => { filtered.value = v.filter(...) })——应使用computed。const { count } = reactive(state)或const { name } = store——会丢失响应性;应使用toRefs/storeToRefs,或优先使用ref。watch(props.id, ...)或将解构后的 prop 传递给组合式函数——应传递 getter() => id。- 直接修改 prop 或
useQuery的数据;直接将查询数据绑定到v-model——应复制到表单中。 - 在同一元素上使用
v-if和v-for;在可重新排序的列表中使用:key="index"。 - 在
onWatcherCleanup之前或在组合式函数中注册生命周期钩子之前使用await。 - 为路由组件使用
defineAsyncComponent(路由直接使用() => import(...))。 - 在导航守卫中调用
next()——应返回false、一个位置或不返回任何内容。 - 测试深入到
wrapper.vm或调用setData。
React:
useEffect(() => setFiltered(items.filter(...)), [items])——应在 render 中计算。- 当 id 变化时在 effect 中重置表单——应传递
key={id}。 - Hook 在条件或循环中调用;状态原地修改而非替换。
- 在可重新排序的列表中使用
key={index};在每次渲染时向记忆化子组件传递新的对象或函数 prop,而该子组件是为了测量的原因而被记忆化的。
两者通用:
- 挂载时进行没有取消、缓存、重试或错误分支的获取——应使用数据层。
- 没有空或错误分支的加载动画;错误被吞没到
console.log中。 - 对于每天变化的数据,
staleTime保留为 0,导致每次聚焦都重新获取。 - 没有快照、回滚和 settle 时失效的乐观更新。
- 使用占位符作为标签;仅用颜色表示错误;
role="button"缺少键盘支持。 - 到处使用
getByTestId;整个页面的快照;测试实现细节。 - 功能模块的 barrel 文件将所有内容拉入第一个路由块。
:href="user.url"/href={user.url}未检查协议。
"完成"前的证据
- 针对每种状态和键盘路径的失败后通过的单元测试。
- Lint、
vue-tsc或tsc、测试链式执行,退出代码为 0;覆盖率等于或高于阈值。 - 针对生产构建的 Playwright 旅程测试,axe 严重/关键错误数为 0。
- 390 和 1280 像素(以及暗色主题)的截图位于
docs/delivery/<slug>/evidence/,每张都已打开。 - 故事 QA 部分中的键盘和屏幕阅读器注释。
- 然后运行
verify-app。
参考资料
references/vue-patterns.md— Vue 检查清单:SFC 和组件 API、响应性陷阱、computed vs watch、组合式函数、Pinia、用于 Vue 的 TanStack Query、Vue Router(URL 状态、守卫、焦点)、表单、国际化、无障碍具体实现、性能、安全、工具;每个条目均有来源。references/react-patterns.md— React 检查清单:相同主题,加上静态导出(Next.js / Nuxt generate)变体。references/a11y-and-testing.md— UI 代码的 WCAG 2.2 AA 项、ARIA/APG、键盘和活动区域、图表、用于 Vue 和 React 的 Testing Library、MSW、Playwright + axe、不应测试的内容。