[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"post-\u002Fblog\u002Fblank-page-router-init":3},{"id":4,"title":5,"body":6,"category":680,"date":681,"description":682,"draft":683,"extension":684,"meta":685,"navigation":686,"path":687,"readingTime":124,"seo":688,"stem":689,"tags":690,"__hash__":694},"blog\u002Fblog\u002Fblank-page-router-init.md","頁面一片空白：路由 404、初始化 API 與重複登入",{"type":7,"value":8,"toc":671},"minimark",[9,13,18,21,29,134,145,165,169,172,262,265,282,285,437,442,452,468,475,537,551,555,558,561,637,640,644,647,650,667],[10,11,12],"p",{},"空白畫面是最難受的錯誤之一：沒有提示、沒有按鈕，使用者只能重新整理。我在維護一個 Vue 2 後台系統時，一次處理了幾個會造成空白畫面的情況。",[14,15,17],"h2",{"id":16},"情況一進入不存在的路由","情況一：進入不存在的路由",[10,19,20],{},"輸入錯誤的網址時，頁面只有外框，內容完全空白。",[10,22,23,24,28],{},"原因很單純：Vue Router 找不到對應路由時不會報錯，只是 ",[25,26,27],"code",{},"\u003Crouter-view>"," 什麼都不渲染。Vue Router 3 要自己加上萬用路由：",[30,31,36],"pre",{"className":32,"code":33,"language":34,"meta":35,"style":35},"language-js shiki shiki-themes github-dark","export function abort(code = 404) {\n  return {\n    name: 'FourOhFour',\n    path: '*',\n    component: () => error(code),\n  }\n}\n","js","",[25,37,38,71,80,93,104,122,128],{"__ignoreMap":35},[39,40,43,47,50,54,58,61,64,68],"span",{"class":41,"line":42},"line",1,[39,44,46],{"class":45},"snl16","export",[39,48,49],{"class":45}," function",[39,51,53],{"class":52},"svObZ"," abort",[39,55,57],{"class":56},"s95oV","(",[39,59,25],{"class":60},"s9osk",[39,62,63],{"class":45}," =",[39,65,67],{"class":66},"sDLfK"," 404",[39,69,70],{"class":56},") {\n",[39,72,74,77],{"class":41,"line":73},2,[39,75,76],{"class":45},"  return",[39,78,79],{"class":56}," {\n",[39,81,83,86,90],{"class":41,"line":82},3,[39,84,85],{"class":56},"    name: ",[39,87,89],{"class":88},"sU2Wk","'FourOhFour'",[39,91,92],{"class":56},",\n",[39,94,96,99,102],{"class":41,"line":95},4,[39,97,98],{"class":56},"    path: ",[39,100,101],{"class":88},"'*'",[39,103,92],{"class":56},[39,105,107,110,113,116,119],{"class":41,"line":106},5,[39,108,109],{"class":52},"    component",[39,111,112],{"class":56},": () ",[39,114,115],{"class":45},"=>",[39,117,118],{"class":52}," error",[39,120,121],{"class":56},"(code),\n",[39,123,125],{"class":41,"line":124},6,[39,126,127],{"class":56},"  }\n",[39,129,131],{"class":41,"line":130},7,[39,132,133],{"class":56},"}\n",[10,135,136,137,140,141,144],{},"因為 ",[25,138,139],{},"*"," 會配對所有路徑，它一定要放在路由清單的最後。這個專案把它放在初始化完成後，用 ",[25,142,143],{},"addRoute"," 註冊：",[30,146,148],{"className":32,"code":147,"language":34,"meta":35,"style":35},"router.addRoute(abort())\n",[25,149,150],{"__ignoreMap":35},[39,151,152,155,157,159,162],{"class":41,"line":42},[39,153,154],{"class":56},"router.",[39,156,143],{"class":52},[39,158,57],{"class":56},[39,160,161],{"class":52},"abort",[39,163,164],{"class":56},"())\n",[14,166,168],{"id":167},"情況二初始化-api","情況二：初始化 API",[10,170,171],{},"系統進站時要載入幾份基礎資料，原本是一支一支等：",[30,173,175],{"className":32,"code":174,"language":34,"meta":35,"style":35},"async function getData() {\n  await store.dispatch('getFormatInGroups')\n  await store.dispatch('setCityLists')\n  await store.dispatch('setPaymentType')\n  await checkLogin()\n  await getUserRoles()\n}\n",[25,176,177,190,209,224,239,249,258],{"__ignoreMap":35},[39,178,179,182,184,187],{"class":41,"line":42},[39,180,181],{"class":45},"async",[39,183,49],{"class":45},[39,185,186],{"class":52}," getData",[39,188,189],{"class":56},"() {\n",[39,191,192,195,198,201,203,206],{"class":41,"line":73},[39,193,194],{"class":45},"  await",[39,196,197],{"class":56}," store.",[39,199,200],{"class":52},"dispatch",[39,202,57],{"class":56},[39,204,205],{"class":88},"'getFormatInGroups'",[39,207,208],{"class":56},")\n",[39,210,211,213,215,217,219,222],{"class":41,"line":82},[39,212,194],{"class":45},[39,214,197],{"class":56},[39,216,200],{"class":52},[39,218,57],{"class":56},[39,220,221],{"class":88},"'setCityLists'",[39,223,208],{"class":56},[39,225,226,228,230,232,234,237],{"class":41,"line":95},[39,227,194],{"class":45},[39,229,197],{"class":56},[39,231,200],{"class":52},[39,233,57],{"class":56},[39,235,236],{"class":88},"'setPaymentType'",[39,238,208],{"class":56},[39,240,241,243,246],{"class":41,"line":106},[39,242,194],{"class":45},[39,244,245],{"class":52}," checkLogin",[39,247,248],{"class":56},"()\n",[39,250,251,253,256],{"class":41,"line":124},[39,252,194],{"class":45},[39,254,255],{"class":52}," getUserRoles",[39,257,248],{"class":56},[39,259,260],{"class":41,"line":130},[39,261,133],{"class":56},[10,263,264],{},"這段有兩個問題：",[266,267,268,276],"ul",{},[269,270,271,275],"li",{},[272,273,274],"strong",{},"串行","：總時間是每支 API 的時間相加。",[269,277,278,281],{},[272,279,280],{},"沒有錯誤處理","：任何一支失敗，後面都不會執行，也沒有人接住錯誤。",[10,283,284],{},"這幾份資料彼此獨立，不需要等前一支完成，所以改成並行，並加上錯誤處理：",[30,286,288],{"className":32,"code":287,"language":34,"meta":35,"style":35},"async function getData() {\n  try {\n    await Promise.all([\n      store.dispatch('getFormatInGroups'),\n      store.dispatch('setCityLists'),\n      store.dispatch('setPaymentType'),\n      checkLogin(),\n      getUserRoles(),\n    ])\n    router.addRoute(abort())\n  } catch (error) {\n    console.error('資料載入錯誤：', error)\n  }\n}\n",[25,289,290,300,307,324,338,350,362,370,378,384,398,410,427,432],{"__ignoreMap":35},[39,291,292,294,296,298],{"class":41,"line":42},[39,293,181],{"class":45},[39,295,49],{"class":45},[39,297,186],{"class":52},[39,299,189],{"class":56},[39,301,302,305],{"class":41,"line":73},[39,303,304],{"class":45},"  try",[39,306,79],{"class":56},[39,308,309,312,315,318,321],{"class":41,"line":82},[39,310,311],{"class":45},"    await",[39,313,314],{"class":66}," Promise",[39,316,317],{"class":56},".",[39,319,320],{"class":52},"all",[39,322,323],{"class":56},"([\n",[39,325,326,329,331,333,335],{"class":41,"line":95},[39,327,328],{"class":56},"      store.",[39,330,200],{"class":52},[39,332,57],{"class":56},[39,334,205],{"class":88},[39,336,337],{"class":56},"),\n",[39,339,340,342,344,346,348],{"class":41,"line":106},[39,341,328],{"class":56},[39,343,200],{"class":52},[39,345,57],{"class":56},[39,347,221],{"class":88},[39,349,337],{"class":56},[39,351,352,354,356,358,360],{"class":41,"line":124},[39,353,328],{"class":56},[39,355,200],{"class":52},[39,357,57],{"class":56},[39,359,236],{"class":88},[39,361,337],{"class":56},[39,363,364,367],{"class":41,"line":130},[39,365,366],{"class":52},"      checkLogin",[39,368,369],{"class":56},"(),\n",[39,371,373,376],{"class":41,"line":372},8,[39,374,375],{"class":52},"      getUserRoles",[39,377,369],{"class":56},[39,379,381],{"class":41,"line":380},9,[39,382,383],{"class":56},"    ])\n",[39,385,387,390,392,394,396],{"class":41,"line":386},10,[39,388,389],{"class":56},"    router.",[39,391,143],{"class":52},[39,393,57],{"class":56},[39,395,161],{"class":52},[39,397,164],{"class":56},[39,399,401,404,407],{"class":41,"line":400},11,[39,402,403],{"class":56},"  } ",[39,405,406],{"class":45},"catch",[39,408,409],{"class":56}," (error) {\n",[39,411,413,416,419,421,424],{"class":41,"line":412},12,[39,414,415],{"class":56},"    console.",[39,417,418],{"class":52},"error",[39,420,57],{"class":56},[39,422,423],{"class":88},"'資料載入錯誤：'",[39,425,426],{"class":56},", error)\n",[39,428,430],{"class":41,"line":429},13,[39,431,127],{"class":56},[39,433,435],{"class":41,"line":434},14,[39,436,133],{"class":56},[438,439,441],"h3",{"id":440},"改成-promiseall-要注意什麼","改成 Promise.all 要注意什麼",[10,443,444,447,448,451],{},[25,445,446],{},"Promise.all"," 不是把 ",[25,449,450],{},"await"," 換掉就好：",[266,453,454,460],{},[269,455,456,459],{},[272,457,458],{},"有依賴關係的不能並行","。如果 B 要用 A 的結果，就要維持先後順序。",[269,461,462,467],{},[272,463,464,465],{},"一支失敗就會進 ",[25,466,406],{},"，其他成功的結果也拿不到。",[10,469,470,471,474],{},"如果希望部分失敗時，其他資料還能用，可以改成 ",[25,472,473],{},"Promise.allSettled","：",[30,476,478],{"className":32,"code":477,"language":34,"meta":35,"style":35},"const results = await Promise.allSettled(tasks)\nconst failed = results.filter(r => r.status === 'rejected')\n",[25,479,480,503],{"__ignoreMap":35},[39,481,482,485,488,490,493,495,497,500],{"class":41,"line":42},[39,483,484],{"class":45},"const",[39,486,487],{"class":66}," results",[39,489,63],{"class":45},[39,491,492],{"class":45}," await",[39,494,314],{"class":66},[39,496,317],{"class":56},[39,498,499],{"class":52},"allSettled",[39,501,502],{"class":56},"(tasks)\n",[39,504,505,507,510,512,515,518,520,523,526,529,532,535],{"class":41,"line":73},[39,506,484],{"class":45},[39,508,509],{"class":66}," failed",[39,511,63],{"class":45},[39,513,514],{"class":56}," results.",[39,516,517],{"class":52},"filter",[39,519,57],{"class":56},[39,521,522],{"class":60},"r",[39,524,525],{"class":45}," =>",[39,527,528],{"class":56}," r.status ",[39,530,531],{"class":45},"===",[39,533,534],{"class":88}," 'rejected'",[39,536,208],{"class":56},[10,538,539,540,542,543,546,547,550],{},"上面的寫法還有一個可以再改進的地方：",[25,541,143],{}," 放在 ",[25,544,545],{},"try"," 裡，初始化失敗時就不會註冊 404 路由。如果重做一次，我會把它移到 ",[25,548,549],{},"finally","，或在路由建立時就註冊，讓 404 不受 API 影響。",[14,552,554],{"id":553},"情況三重複登入","情況三：重複登入",[10,556,557],{},"已經登入的使用者再次登入時，API 回傳失敗訊息，但程式沒有處理這個情況，只顯示「缺少必要的權限信息」，接著畫面就空白了。",[10,559,560],{},"修法是區分失敗的原因：",[30,562,564],{"className":32,"code":563,"language":34,"meta":35,"style":35},"if (result.message?.includes('已登入')) {\n  alert(result.message)\n  await router.push({ name: 'Dashboard' })\n} else {\n  alert('登入失敗，請稍後再試。')\n}\n",[25,565,566,585,593,612,622,633],{"__ignoreMap":35},[39,567,568,571,574,577,579,582],{"class":41,"line":42},[39,569,570],{"class":45},"if",[39,572,573],{"class":56}," (result.message?.",[39,575,576],{"class":52},"includes",[39,578,57],{"class":56},[39,580,581],{"class":88},"'已登入'",[39,583,584],{"class":56},")) {\n",[39,586,587,590],{"class":41,"line":73},[39,588,589],{"class":52},"  alert",[39,591,592],{"class":56},"(result.message)\n",[39,594,595,597,600,603,606,609],{"class":41,"line":82},[39,596,194],{"class":45},[39,598,599],{"class":56}," router.",[39,601,602],{"class":52},"push",[39,604,605],{"class":56},"({ name: ",[39,607,608],{"class":88},"'Dashboard'",[39,610,611],{"class":56}," })\n",[39,613,614,617,620],{"class":41,"line":95},[39,615,616],{"class":56},"} ",[39,618,619],{"class":45},"else",[39,621,79],{"class":56},[39,623,624,626,628,631],{"class":41,"line":106},[39,625,589],{"class":52},[39,627,57],{"class":56},[39,629,630],{"class":88},"'登入失敗，請稍後再試。'",[39,632,208],{"class":56},[39,634,635],{"class":41,"line":124},[39,636,133],{"class":56},[10,638,639],{},"用訊息文字判斷不是最理想的做法，後端有明確的錯誤代碼會更穩定，但在不動後端的情況下，這樣能先解決問題。",[14,641,643],{"id":642},"順便補上密碼逾期","順便補上：密碼逾期",[10,645,646],{},"登入時也可能遇到密碼逾期。原本使用者只會卡住，所以我新增了一個設定新密碼的視窗：後端回傳逾期狀態時打開，更新成功後關閉，讓使用者不用離開登入流程。",[14,648,649],{"id":649},"這次學到的事",[266,651,652,655,658,661],{},[269,653,654],{},"空白畫面通常代表某個情況沒有被處理，不是程式壞掉，而是流程沒有終點。",[269,656,657],{},"每個流程都要想失敗時的畫面：找不到頁面、API 失敗、狀態不符。",[269,659,660],{},"串行改並行前，先確認資料之間的依賴關係。",[269,662,663,664,666],{},"錯誤被 ",[25,665,406],{}," 住只是第一步，還要讓使用者看得到結果。",[668,669,670],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}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 .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}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);}",{"title":35,"searchDepth":73,"depth":73,"links":672},[673,674,677,678,679],{"id":16,"depth":73,"text":17},{"id":167,"depth":73,"text":168,"children":675},[676],{"id":440,"depth":82,"text":441},{"id":553,"depth":73,"text":554},{"id":642,"depth":73,"text":643},{"id":649,"depth":73,"text":649},"case","2026-03-18","後台系統在幾種情況下會顯示空白畫面。記錄如何補上 404 路由、調整初始化 API 的載入方式，以及處理重複登入與密碼逾期的流程。",false,"md",{},true,"\u002Fblog\u002Fblank-page-router-init",{"title":5,"description":682},"blog\u002Fblank-page-router-init",[691,692,693],"Vue Router","Promise","Error Handling","cXyq2tzY4HffeIZK6aIzRqcHPAONIjCw8zH76oYlYkg"]