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(或儲存庫的 meta-framework)並參考 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)。絕不添加第二個資料層、路由、狀態管理、表單或國際化函式庫。證據: 故事的筆記列出重複使用的元件與組合式函式/鉤子,以及任何新增的部分,並附上原因。 - 在撰寫程式碼之前撰寫狀態表。 針對每個視圖:每筆資料及其位置——伺服器(查詢)、URL(路由參數/搜尋)、共享客戶端(Pinia store、React context 或儲存庫的 store)、本地端(
ref/useState)、衍生(computed/ render 中的 const)、外部(包裝瀏覽器 API 的 composable 或useSyncExternalStore)。然後列出四種狀態(載入、空、錯誤、成功)以及每種狀態的複製鍵。證據: 故事中的表格。 - 先撰寫會失敗的測試。 Vitest +
@testing-library/vue或@testing-library/react、userEvent.setup()、按角色和可存取名稱查詢、每個測試一個行為、在服務邊界偽造網路(MSW 或儲存庫在src/test-utils中的假客戶端)。涵蓋每種狀態、鍵盤路徑和一個負面案例。證據: 運行顯示因正確原因而顯示紅色。 - 在功能資料夾內構建。
src/features/<name>/services(型別化的 API 呼叫,無框架)→composables/hooks(包裝useQuery/useMutation和 key factory 的useX)→components(使用<script setup lang="ts">的Name.vue,或Name.tsx)→ 組合它們的路由視圖。Props 與 events 有型別,無any,無非空斷言!,使用@/匯入。證據:git diff --stat保持在 feature 內,加上有意的共享更改。 - 複製與格式化。 每個可見文字字串都來自本地端檔案(vue-i18n 的
useI18n(),或 React 儲存庫的 i18n 函式庫或 copy module),且每個語言環境都有相同的鍵;測試會強制執行一致性。數字與日期透過 i18n 格式化器或Intl。證據: 一致性測試通過;無硬編碼的複製,由 lint 強制執行(@intlify/vue-i18n/no-raw-text;React:react/jsx-no-literals或 i18n 插件的規則)——引用 lint 的退出碼。 - 使用
css-design設定樣式。 僅使用代碼變數,BEM 區塊 = 元件名稱,每種狀態都有樣式;.vue樣式透過postcss-html進行 lint 檢查。證據: Stylelint 檢查通過。 - Stories(元件庫)。 每種狀態一個 story,兩個主題;Storybook 的 axe 檢查通過。證據: stories 測試與 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 與 effects 僅與外部同步(DOM API、訂閱、計時器、第三方小工具)並進行清理(
onWatcherCleanup/onUnmounted;React:effect 的回傳值)。由使用者操作導致的工作在事件處理程式中執行。 - 伺服器資料永遠不會複製到本地狀態或 store 中;查詢鍵包含請求使用的所有輸入(在 Vue 中作為 ref 或 getter,以便變更時重新請求)。
- 使用者會分享或重新載入的選取、篩選器、標籤頁與分頁,應存在於 URL 查詢中。
- 每個資料視圖都呈現載入、空、錯誤(含重試)與成功狀態;測試涵蓋全部四種。
- 每個輸入都有可見的
<label for>(兩個框架都使用useId()產生的 ID);錯誤是透過aria-describedby關聯的文字,輸入內容會保留,提交時焦點移動到第一個無效欄位。 - 優先使用原生元素(
button、a href/router link、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+ template ref;React:router 的對應方式)。 - 錯誤在每個路由和每個有風險的小工具層級被捕獲(Vue:
onErrorCaptured邊界元件;React:error boundary)並顯示重試,絕不顯示空白頁面。 - 無硬編碼的顯示字串;每個語言環境都有相同的鍵。
- 測試按角色/標籤/文字查詢、與
user-event互動,且絕不斷言類別名稱、元件內部(wrapper.vm、狀態值)或 composable/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 傳遞給 composable——請傳遞一個 getter() => id。- 變動 prop 或
useQuery的資料;將查詢資料直接綁定到v-model——請複製到表單中。 - 在同一個元素上使用
v-if和v-for;在可重新排序的清單上使用:key="index"。 - 在 composable 中,在
onWatcherCleanup或註冊生命週期鉤子之前使用await。 - 為路由元件使用
defineAsyncComponent(路由直接接受() => import(...))。 - 在導航守衛中使用
next()——請回傳false、一個位置,或不回傳任何內容。 - 測試中存取
wrapper.vm或呼叫setData。
React:
useEffect(() => setFiltered(items.filter(...)), [items])——請在 render 中計算。- 當 id 變更時,在 effect 中重置表單——請傳遞
key={id}。 - 在條件式或迴圈中呼叫鉤子;就地變動狀態而非取代。
- 在可重新排序的清單上使用
key={index};每次 render 都將新的物件或函式 prop 傳遞給一個因測量原因而被 memo 化的子元件。
兩者共通:
- 在掛載時進行請求,沒有取消、快取、重試或錯誤分支——請使用資料層。
- 只有 spinner,沒有空或錯誤分支;錯誤被
console.log吞掉。 staleTime為 0 的資料每天變更,導致每次聚焦都重新請求。- 樂觀更新沒有快照、回滾和完成後使快取失效。
- 佔位符用作標籤;僅用顏色標記錯誤;
role="button"沒有按鍵處理。 - 到處使用
getByTestId;整個頁面的快照;測試實作細節。 - 一個功能 barrel 檔將所有內容拉入第一個路由 chunk。
:href="user.url"/href={user.url}沒有檢查協定。
「完成」前的證據
- 針對每種狀態與鍵盤路徑的先失敗後通過的單元測試。
- Lint、
vue-tsc或tsc、測試鏈接,退出碼 0;覆蓋率達到或高於閾值。 - 針對生產構建的 Playwright 旅程測試,axe 嚴重/關鍵 = 0。
docs/delivery/<slug>/evidence/中的 390 與 1280 像素截圖(以及深色主題),每張都已開啟。- 故事 QA 區段中的鍵盤與螢幕閱讀器筆記。
- 然後執行
verify-app。
參考資料
references/vue-patterns.md— Vue 檢查清單:SFC 與元件 API、反應性陷阱、computed vs watch、composables、Pinia、Vue 的 TanStack Query、Vue Router(URL 狀態、守衛、焦點)、表單、i18n、無障礙細節、效能、安全性、工具;每個項目都有來源。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、不該測試的內容。