[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"post-\u002Fblog\u002Ffrontend-quality":3},{"id":4,"title":5,"body":6,"category":629,"date":630,"description":631,"draft":632,"extension":633,"meta":634,"navigation":153,"path":635,"readingTime":222,"seo":636,"stem":637,"tags":638,"__hash__":642},"blog\u002Fblog\u002Ffrontend-quality.md","前端工程化與品質：測試策略、無障礙與 Web Vitals",{"type":7,"value":8,"toc":620},"minimark",[9,13,18,21,89,92,298,303,306,310,318,364,418,425,429,432,482,485,521,581,584,610,613,616],[10,11,12],"p",{},"「品質」不是上線前才補的一道工序，而是讓每次修改都能被驗證、被理解、被安全地回復。這篇整理前端常見的三個面向：測試策略、無障礙，以及效能指標。",[14,15,17],"h2",{"id":16},"測試策略測行為不測實作","測試策略：測行為，不測實作",[10,19,20],{},"前端測試常見分三層：",[22,23,24,43],"table",{},[25,26,27],"thead",{},[28,29,30,34,37,40],"tr",{},[31,32,33],"th",{},"層級",[31,35,36],{},"工具範例",[31,38,39],{},"驗證什麼",[31,41,42],{},"成本",[44,45,46,61,75],"tbody",{},[28,47,48,52,55,58],{},[49,50,51],"td",{},"單元測試",[49,53,54],{},"Vitest",[49,56,57],{},"純函式、工具函式、狀態邏輯",[49,59,60],{},"低",[28,62,63,66,69,72],{},[49,64,65],{},"元件測試",[49,67,68],{},"Vue Test Utils",[49,70,71],{},"使用者看得到的互動結果",[49,73,74],{},"中",[28,76,77,80,83,86],{},[49,78,79],{},"端對端測試",[49,81,82],{},"Playwright",[49,84,85],{},"完整流程（登入、下單…）",[49,87,88],{},"高",[10,90,91],{},"元件測試的原則是「像使用者一樣操作，檢查使用者看得到的結果」，而不是檢查元件內部的變數名稱。這樣重構內部實作時，測試不會跟著壞。",[93,94,99],"pre",{"className":95,"code":96,"language":97,"meta":98,"style":98},"language-ts shiki shiki-themes github-dark","import { mount } from '@vue\u002Ftest-utils'\nimport { describe, expect, it } from 'vitest'\nimport Counter from '.\u002FCounter.vue'\n\ndescribe('Counter', () => {\n  it('點擊按鈕後，顯示的數字加一', async () => {\n    const wrapper = mount(Counter)\n\n    await wrapper.get('button').trigger('click')\n\n    expect(wrapper.text()).toContain('1')\n  })\n})\n","ts","",[100,101,102,122,135,148,155,177,201,220,225,256,261,286,292],"code",{"__ignoreMap":98},[103,104,107,111,115,118],"span",{"class":105,"line":106},"line",1,[103,108,110],{"class":109},"snl16","import",[103,112,114],{"class":113},"s95oV"," { mount } ",[103,116,117],{"class":109},"from",[103,119,121],{"class":120},"sU2Wk"," '@vue\u002Ftest-utils'\n",[103,123,125,127,130,132],{"class":105,"line":124},2,[103,126,110],{"class":109},[103,128,129],{"class":113}," { describe, expect, it } ",[103,131,117],{"class":109},[103,133,134],{"class":120}," 'vitest'\n",[103,136,138,140,143,145],{"class":105,"line":137},3,[103,139,110],{"class":109},[103,141,142],{"class":113}," Counter ",[103,144,117],{"class":109},[103,146,147],{"class":120}," '.\u002FCounter.vue'\n",[103,149,151],{"class":105,"line":150},4,[103,152,154],{"emptyLinePlaceholder":153},true,"\n",[103,156,158,162,165,168,171,174],{"class":105,"line":157},5,[103,159,161],{"class":160},"svObZ","describe",[103,163,164],{"class":113},"(",[103,166,167],{"class":120},"'Counter'",[103,169,170],{"class":113},", () ",[103,172,173],{"class":109},"=>",[103,175,176],{"class":113}," {\n",[103,178,180,183,185,188,191,194,197,199],{"class":105,"line":179},6,[103,181,182],{"class":160},"  it",[103,184,164],{"class":113},[103,186,187],{"class":120},"'點擊按鈕後，顯示的數字加一'",[103,189,190],{"class":113},", ",[103,192,193],{"class":109},"async",[103,195,196],{"class":113}," () ",[103,198,173],{"class":109},[103,200,176],{"class":113},[103,202,204,207,211,214,217],{"class":105,"line":203},7,[103,205,206],{"class":109},"    const",[103,208,210],{"class":209},"sDLfK"," wrapper",[103,212,213],{"class":109}," =",[103,215,216],{"class":160}," mount",[103,218,219],{"class":113},"(Counter)\n",[103,221,223],{"class":105,"line":222},8,[103,224,154],{"emptyLinePlaceholder":153},[103,226,228,231,234,237,239,242,245,248,250,253],{"class":105,"line":227},9,[103,229,230],{"class":109},"    await",[103,232,233],{"class":113}," wrapper.",[103,235,236],{"class":160},"get",[103,238,164],{"class":113},[103,240,241],{"class":120},"'button'",[103,243,244],{"class":113},").",[103,246,247],{"class":160},"trigger",[103,249,164],{"class":113},[103,251,252],{"class":120},"'click'",[103,254,255],{"class":113},")\n",[103,257,259],{"class":105,"line":258},10,[103,260,154],{"emptyLinePlaceholder":153},[103,262,264,267,270,273,276,279,281,284],{"class":105,"line":263},11,[103,265,266],{"class":160},"    expect",[103,268,269],{"class":113},"(wrapper.",[103,271,272],{"class":160},"text",[103,274,275],{"class":113},"()).",[103,277,278],{"class":160},"toContain",[103,280,164],{"class":113},[103,282,283],{"class":120},"'1'",[103,285,255],{"class":113},[103,287,289],{"class":105,"line":288},12,[103,290,291],{"class":113},"  })\n",[103,293,295],{"class":105,"line":294},13,[103,296,297],{"class":113},"})\n",[299,300,302],"h3",{"id":301},"mock-的邊界","Mock 的邊界",[10,304,305],{},"Mock 的位置越靠近系統邊界越好：mock 網路請求（例如使用 MSW），而不是 mock 自己的內部函式。內部函式被 mock 掉，測試就只是在驗證 mock 本身。",[14,307,309],{"id":308},"無障礙從鍵盤開始","無障礙：從鍵盤開始",[10,311,312,313,317],{},"最容易檢查、效益也最大的一項是",[314,315,316],"strong",{},"鍵盤操作","。不用滑鼠，只用鍵盤走過一遍頁面：",[319,320,321,328,339,346],"ul",{},[322,323,324,327],"li",{},[100,325,326],{},"Tab"," 能依合理順序走遍所有可互動元素嗎？",[322,329,330,331,334,335,338],{},"目前焦點的位置看得出來嗎？（不要移除 ",[100,332,333],{},"outline","，改用 ",[100,336,337],{},":focus-visible"," 自訂樣式）",[322,340,341,342,345],{},"彈窗打開時，焦點有進入彈窗嗎？按 ",[100,343,344],{},"Esc"," 能關閉嗎？關閉後焦點有回到觸發按鈕嗎？",[322,347,348,349,352,353,352,356,359,360,363],{},"用 ",[100,350,351],{},"\u003Cbutton>","、",[100,354,355],{},"\u003Ca>",[100,357,358],{},"\u003Clabel>"," 等語意化元素，而不是在 ",[100,361,362],{},"\u003Cdiv>"," 上綁點擊事件。",[93,365,369],{"className":366,"code":367,"language":368,"meta":98,"style":98},"language-css shiki shiki-themes github-dark",":focus-visible {\n  outline: 2px solid #1d4ed8;\n  outline-offset: 2px;\n}\n","css",[100,370,371,377,400,413],{"__ignoreMap":98},[103,372,373,375],{"class":105,"line":106},[103,374,337],{"class":160},[103,376,176],{"class":113},[103,378,379,382,385,388,391,394,397],{"class":105,"line":124},[103,380,381],{"class":209},"  outline",[103,383,384],{"class":113},": ",[103,386,387],{"class":209},"2",[103,389,390],{"class":109},"px",[103,392,393],{"class":209}," solid",[103,395,396],{"class":209}," #1d4ed8",[103,398,399],{"class":113},";\n",[103,401,402,405,407,409,411],{"class":105,"line":137},[103,403,404],{"class":209},"  outline-offset",[103,406,384],{"class":113},[103,408,387],{"class":209},[103,410,390],{"class":109},[103,412,399],{"class":113},[103,414,415],{"class":105,"line":150},[103,416,417],{"class":113},"}\n",[10,419,420,421,424],{},"自動化工具（例如 axe）能抓到缺少 ",[100,422,423],{},"alt","、對比不足、表單欄位沒有標籤等問題，但無法判斷操作流程是否合理，所以仍需要搭配手動檢查。",[14,426,428],{"id":427},"效能core-web-vitals","效能：Core Web Vitals",[10,430,431],{},"Google 的 Core Web Vitals 目前有三項，「良好」的門檻是：",[22,433,434,447],{},[25,435,436],{},[28,437,438,441,444],{},[31,439,440],{},"指標",[31,442,443],{},"衡量",[31,445,446],{},"良好",[44,448,449,460,471],{},[28,450,451,454,457],{},[49,452,453],{},"LCP（Largest Contentful Paint）",[49,455,456],{},"最大內容多快出現",[49,458,459],{},"≤ 2.5 秒",[28,461,462,465,468],{},[49,463,464],{},"INP（Interaction to Next Paint）",[49,466,467],{},"互動後多快有畫面回應",[49,469,470],{},"≤ 200 毫秒",[28,472,473,476,479],{},[49,474,475],{},"CLS（Cumulative Layout Shift）",[49,477,478],{},"版面在載入過程中跳動多少",[49,480,481],{},"≤ 0.1",[10,483,484],{},"常見的改善方向：",[319,486,487,497,503],{},[322,488,489,492,493,496],{},[314,490,491],{},"LCP","：最大的圖片使用適當格式與尺寸，並提高載入優先權（例如 ",[100,494,495],{},"fetchpriority=\"high\"","）；減少阻塞渲染的資源。",[322,498,499,502],{},[314,500,501],{},"INP","：把長時間執行的 JavaScript 拆小，避免佔住主執行緒。",[322,504,505,508,509,512,513,516,517,520],{},[314,506,507],{},"CLS","：圖片與嵌入內容事先指定 ",[100,510,511],{},"width"," \u002F ",[100,514,515],{},"height","（或 ",[100,518,519],{},"aspect-ratio","），讓瀏覽器先保留空間。",[93,522,526],{"className":523,"code":524,"language":525,"meta":98,"style":98},"language-html shiki shiki-themes github-dark","\u003Cimg src=\"\u002Fcover.webp\" width=\"1600\" height=\"728\" alt=\"專案封面\" fetchpriority=\"high\">\n","html",[100,527,528],{"__ignoreMap":98},[103,529,530,533,537,540,543,546,549,551,554,557,559,562,565,567,570,573,575,578],{"class":105,"line":106},[103,531,532],{"class":113},"\u003C",[103,534,536],{"class":535},"s4JwU","img",[103,538,539],{"class":160}," src",[103,541,542],{"class":113},"=",[103,544,545],{"class":120},"\"\u002Fcover.webp\"",[103,547,548],{"class":160}," width",[103,550,542],{"class":113},[103,552,553],{"class":120},"\"1600\"",[103,555,556],{"class":160}," height",[103,558,542],{"class":113},[103,560,561],{"class":120},"\"728\"",[103,563,564],{"class":160}," alt",[103,566,542],{"class":113},[103,568,569],{"class":120},"\"專案封面\"",[103,571,572],{"class":160}," fetchpriority",[103,574,542],{"class":113},[103,576,577],{"class":120},"\"high\"",[103,579,580],{"class":113},">\n",[14,582,583],{"id":583},"大型專案的維護思維",[319,585,586,592,598,604],{},[322,587,588,591],{},[314,589,590],{},"讓錯誤盡早出現","：TypeScript、ESLint、型別化的 API 回傳，把錯誤擋在編譯與提交階段。",[322,593,594,597],{},[314,595,596],{},"小步修改、可回復","：每次變更範圍小，才容易 review，也容易回退。",[322,599,600,603],{},[314,601,602],{},"把規則寫成工具","：能由 CI 自動檢查的（格式、型別、測試），就不要靠人記得。",[322,605,606,609],{},[314,607,608],{},"留下「為什麼」","：註解與文件記錄決策原因，接手的人才能判斷何時可以改。",[14,611,612],{"id":612},"小結",[10,614,615],{},"測試守住「行為」、無障礙守住「每個人都能用」、Web Vitals 守住「用起來順不順」。三者都可以從很小的習慣開始：寫一個行為測試、用鍵盤走一遍頁面、替圖片加上尺寸。",[617,618,619],"style",{},"html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}",{"title":98,"searchDepth":124,"depth":124,"links":621},[622,625,626,627,628],{"id":16,"depth":124,"text":17,"children":623},[624],{"id":301,"depth":137,"text":302},{"id":308,"depth":124,"text":309},{"id":427,"depth":124,"text":428},{"id":583,"depth":124,"text":583},{"id":612,"depth":124,"text":612},"quality","2026-10-29","測試、自動化驗證、無障礙設計、效能優化與大型專案的維護思維。",false,"md",{},"\u002Fblog\u002Ffrontend-quality",{"title":5,"description":631},"blog\u002Ffrontend-quality",[639,640,641],"Testing","Accessibility","Web Vitals","NxcOha3EK8H-KqYX2pKqLo1EFXTqQ8zz5I7qR9j2saY"]