[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"post-\u002Fblog\u002Fredirect-after-login":3},{"id":4,"title":5,"body":6,"category":693,"date":694,"description":695,"draft":696,"extension":697,"meta":698,"navigation":378,"path":699,"readingTime":84,"seo":700,"stem":701,"tags":702,"__hash__":706},"blog\u002Fblog\u002Fredirect-after-login.md","登入後回到原本的頁面：把一堆旗標收斂成一個機制",{"type":7,"value":8,"toc":685},"minimark",[9,13,16,21,24,88,91,178,181,184,188,191,279,282,516,519,571,574,578,581,595,598,601,644,647,650,657,664,667,670,681],[10,11,12],"p",{},"使用者點進需要登入的頁面時，會先被帶去登入。登入完應該回到原本要去的頁面，而不是回首頁。",[10,14,15],{},"這個需求很常見，但我接手的 Vue 2 專案只處理了其中幾個頁面，而且做法不好擴充。這篇記錄我怎麼重構它。",[17,18,20],"h2",{"id":19},"原本的做法每個頁面一個旗標","原本的做法：每個頁面一個旗標",[10,22,23],{},"Vuex 裡有一組旗標，記錄使用者是從哪個功能過來的：",[25,26,31],"pre",{"className":27,"code":28,"language":29,"meta":30,"style":30},"language-js shiki shiki-themes github-dark","state: {\n  loginToCart: false,\n  loginToApplication: false,\n  \u002F\u002F ...\n}\n","js","",[32,33,34,47,63,75,82],"code",{"__ignoreMap":30},[35,36,39,43],"span",{"class":37,"line":38},"line",1,[35,40,42],{"class":41},"svObZ","state",[35,44,46],{"class":45},"s95oV",": {\n",[35,48,50,53,56,60],{"class":37,"line":49},2,[35,51,52],{"class":41},"  loginToCart",[35,54,55],{"class":45},": ",[35,57,59],{"class":58},"sDLfK","false",[35,61,62],{"class":45},",\n",[35,64,66,69,71,73],{"class":37,"line":65},3,[35,67,68],{"class":41},"  loginToApplication",[35,70,55],{"class":45},[35,72,59],{"class":58},[35,74,62],{"class":45},[35,76,78],{"class":37,"line":77},4,[35,79,81],{"class":80},"sAwPA","  \u002F\u002F ...\n",[35,83,85],{"class":37,"line":84},5,[35,86,87],{"class":45},"}\n",[10,89,90],{},"進入頁面前設定旗標，登入成功後再逐一判斷：",[25,92,94],{"className":27,"code":93,"language":29,"meta":30,"style":30},"if (state.loginToCart) {\n  router.push({ name: 'Cart' })\n} else if (state.loginToApplication) {\n  router.push({ name: 'Application' })\n} else {\n  router.push({ name: 'Home' })\n}\n",[32,95,96,105,123,137,150,159,173],{"__ignoreMap":30},[35,97,98,102],{"class":37,"line":38},[35,99,101],{"class":100},"snl16","if",[35,103,104],{"class":45}," (state.loginToCart) {\n",[35,106,107,110,113,116,120],{"class":37,"line":49},[35,108,109],{"class":45},"  router.",[35,111,112],{"class":41},"push",[35,114,115],{"class":45},"({ name: ",[35,117,119],{"class":118},"sU2Wk","'Cart'",[35,121,122],{"class":45}," })\n",[35,124,125,128,131,134],{"class":37,"line":65},[35,126,127],{"class":45},"} ",[35,129,130],{"class":100},"else",[35,132,133],{"class":100}," if",[35,135,136],{"class":45}," (state.loginToApplication) {\n",[35,138,139,141,143,145,148],{"class":37,"line":77},[35,140,109],{"class":45},[35,142,112],{"class":41},[35,144,115],{"class":45},[35,146,147],{"class":118},"'Application'",[35,149,122],{"class":45},[35,151,152,154,156],{"class":37,"line":84},[35,153,127],{"class":45},[35,155,130],{"class":100},[35,157,158],{"class":45}," {\n",[35,160,162,164,166,168,171],{"class":37,"line":161},6,[35,163,109],{"class":45},[35,165,112],{"class":41},[35,167,115],{"class":45},[35,169,170],{"class":118},"'Home'",[35,172,122],{"class":45},[35,174,176],{"class":37,"line":175},7,[35,177,87],{"class":45},[10,179,180],{},"這樣的問題是，新增一個要登入的頁面，就要多一個旗標、多一個 mutation、多一段判斷，還要記得在對的地方清除。沒有列進去的頁面，登入後就只能回首頁。",[10,182,183],{},"而且它只記得「哪個頁面」，記不住網址上的參數。",[17,185,187],{"id":186},"改成記住使用者要去的路由","改成：記住使用者要去的路由",[10,189,190],{},"其實這些旗標都在表達同一件事：登入後要去哪裡。所以我把它們換成一個狀態：",[25,192,194],{"className":27,"code":193,"language":29,"meta":30,"style":30},"state: {\n  redirectAfterLogin: null,\n},\nmutations: {\n  setRedirectAfterLogin(state, route) {\n    state.redirectAfterLogin = route\n  },\n  clearRedirectAfterLogin(state) {\n    state.redirectAfterLogin = null\n  },\n}\n",[32,195,196,202,214,219,226,234,245,250,259,269,274],{"__ignoreMap":30},[35,197,198,200],{"class":37,"line":38},[35,199,42],{"class":41},[35,201,46],{"class":45},[35,203,204,207,209,212],{"class":37,"line":49},[35,205,206],{"class":41},"  redirectAfterLogin",[35,208,55],{"class":45},[35,210,211],{"class":58},"null",[35,213,62],{"class":45},[35,215,216],{"class":37,"line":65},[35,217,218],{"class":45},"},\n",[35,220,221,224],{"class":37,"line":77},[35,222,223],{"class":41},"mutations",[35,225,46],{"class":45},[35,227,228,231],{"class":37,"line":84},[35,229,230],{"class":41},"  setRedirectAfterLogin",[35,232,233],{"class":45},"(state, route) {\n",[35,235,236,239,242],{"class":37,"line":161},[35,237,238],{"class":45},"    state.redirectAfterLogin ",[35,240,241],{"class":100},"=",[35,243,244],{"class":45}," route\n",[35,246,247],{"class":37,"line":175},[35,248,249],{"class":45},"  },\n",[35,251,253,256],{"class":37,"line":252},8,[35,254,255],{"class":41},"  clearRedirectAfterLogin",[35,257,258],{"class":45},"(state) {\n",[35,260,262,264,266],{"class":37,"line":261},9,[35,263,238],{"class":45},[35,265,241],{"class":100},[35,267,268],{"class":58}," null\n",[35,270,272],{"class":37,"line":271},10,[35,273,249],{"class":45},[35,275,277],{"class":37,"line":276},11,[35,278,87],{"class":45},[10,280,281],{},"記錄的時機也不用分散在各個頁面，全部放進 Router Guard。發現使用者沒有登入時，把目標路由存下來：",[25,283,285],{"className":27,"code":284,"language":29,"meta":30,"style":30},"router.beforeEach((to, from, next) => {\n  const isPageRefresh = from.name && from.path === to.path\n  const skip = ['Login', 'Register', 'Home']\n\n  if (\n    !isLoggedIn() &&\n    !store.state.redirectAfterLogin &&\n    !skip.includes(to.name) &&\n    from.name !== undefined &&\n    !isPageRefresh\n  ) {\n    store.commit('setRedirectAfterLogin', {\n      name: to.name,\n      params: to.params,\n      query: to.query,\n    })\n  }\n\n  \u002F\u002F ...\n})\n",[32,286,287,321,347,374,380,388,402,411,426,440,447,452,470,476,482,488,494,500,505,510],{"__ignoreMap":30},[35,288,289,292,295,298,302,305,308,310,313,316,319],{"class":37,"line":38},[35,290,291],{"class":45},"router.",[35,293,294],{"class":41},"beforeEach",[35,296,297],{"class":45},"((",[35,299,301],{"class":300},"s9osk","to",[35,303,304],{"class":45},", ",[35,306,307],{"class":300},"from",[35,309,304],{"class":45},[35,311,312],{"class":300},"next",[35,314,315],{"class":45},") ",[35,317,318],{"class":100},"=>",[35,320,158],{"class":45},[35,322,323,326,329,332,335,338,341,344],{"class":37,"line":49},[35,324,325],{"class":100},"  const",[35,327,328],{"class":58}," isPageRefresh",[35,330,331],{"class":100}," =",[35,333,334],{"class":45}," from.name ",[35,336,337],{"class":100},"&&",[35,339,340],{"class":45}," from.path ",[35,342,343],{"class":100},"===",[35,345,346],{"class":45}," to.path\n",[35,348,349,351,354,356,359,362,364,367,369,371],{"class":37,"line":65},[35,350,325],{"class":100},[35,352,353],{"class":58}," skip",[35,355,331],{"class":100},[35,357,358],{"class":45}," [",[35,360,361],{"class":118},"'Login'",[35,363,304],{"class":45},[35,365,366],{"class":118},"'Register'",[35,368,304],{"class":45},[35,370,170],{"class":118},[35,372,373],{"class":45},"]\n",[35,375,376],{"class":37,"line":77},[35,377,379],{"emptyLinePlaceholder":378},true,"\n",[35,381,382,385],{"class":37,"line":84},[35,383,384],{"class":100},"  if",[35,386,387],{"class":45}," (\n",[35,389,390,393,396,399],{"class":37,"line":161},[35,391,392],{"class":100},"    !",[35,394,395],{"class":41},"isLoggedIn",[35,397,398],{"class":45},"() ",[35,400,401],{"class":100},"&&\n",[35,403,404,406,409],{"class":37,"line":175},[35,405,392],{"class":100},[35,407,408],{"class":45},"store.state.redirectAfterLogin ",[35,410,401],{"class":100},[35,412,413,415,418,421,424],{"class":37,"line":252},[35,414,392],{"class":100},[35,416,417],{"class":45},"skip.",[35,419,420],{"class":41},"includes",[35,422,423],{"class":45},"(to.name) ",[35,425,401],{"class":100},[35,427,428,431,434,437],{"class":37,"line":261},[35,429,430],{"class":45},"    from.name ",[35,432,433],{"class":100},"!==",[35,435,436],{"class":58}," undefined",[35,438,439],{"class":100}," &&\n",[35,441,442,444],{"class":37,"line":271},[35,443,392],{"class":100},[35,445,446],{"class":45},"isPageRefresh\n",[35,448,449],{"class":37,"line":276},[35,450,451],{"class":45},"  ) {\n",[35,453,455,458,461,464,467],{"class":37,"line":454},12,[35,456,457],{"class":45},"    store.",[35,459,460],{"class":41},"commit",[35,462,463],{"class":45},"(",[35,465,466],{"class":118},"'setRedirectAfterLogin'",[35,468,469],{"class":45},", {\n",[35,471,473],{"class":37,"line":472},13,[35,474,475],{"class":45},"      name: to.name,\n",[35,477,479],{"class":37,"line":478},14,[35,480,481],{"class":45},"      params: to.params,\n",[35,483,485],{"class":37,"line":484},15,[35,486,487],{"class":45},"      query: to.query,\n",[35,489,491],{"class":37,"line":490},16,[35,492,493],{"class":45},"    })\n",[35,495,497],{"class":37,"line":496},17,[35,498,499],{"class":45},"  }\n",[35,501,503],{"class":37,"line":502},18,[35,504,379],{"emptyLinePlaceholder":378},[35,506,508],{"class":37,"line":507},19,[35,509,81],{"class":80},[35,511,513],{"class":37,"line":512},20,[35,514,515],{"class":45},"})\n",[10,517,518],{},"登入成功後只要看這一個值：",[25,520,522],{"className":27,"code":521,"language":29,"meta":30,"style":30},"const route = store.state.redirectAfterLogin\nstore.commit('clearRedirectAfterLogin')\nrouter.push(route ?? { name: 'Home' })\n",[32,523,524,537,552],{"__ignoreMap":30},[35,525,526,529,532,534],{"class":37,"line":38},[35,527,528],{"class":100},"const",[35,530,531],{"class":58}," route",[35,533,331],{"class":100},[35,535,536],{"class":45}," store.state.redirectAfterLogin\n",[35,538,539,542,544,546,549],{"class":37,"line":49},[35,540,541],{"class":45},"store.",[35,543,460],{"class":41},[35,545,463],{"class":45},[35,547,548],{"class":118},"'clearRedirectAfterLogin'",[35,550,551],{"class":45},")\n",[35,553,554,556,558,561,564,567,569],{"class":37,"line":65},[35,555,291],{"class":45},[35,557,112],{"class":41},[35,559,560],{"class":45},"(route ",[35,562,563],{"class":100},"??",[35,565,566],{"class":45}," { name: ",[35,568,170],{"class":118},[35,570,122],{"class":45},[10,572,573],{},"現在新增要登入的頁面，不用再改任何跳轉邏輯。",[17,575,577],{"id":576},"為什麼要存-params-和-query","為什麼要存 params 和 query",[10,579,580],{},"只存路徑名稱的話，帶參數的頁面回來後內容會不見。例如從訂單詳情或搜尋結果被帶去登入，回來應該還是同一筆資料。",[10,582,583,584,587,588,587,591,594],{},"所以我把 ",[32,585,586],{},"name","、",[32,589,590],{},"params",[32,592,593],{},"query"," 都存起來，回來時就能還原完整的頁面狀態。",[17,596,597],{"id":597},"要排除的邊界情況",[10,599,600],{},"機制本身很簡單，大部分的條件都在處理例外：",[602,603,604,612,618,632,638],"ul",{},[605,606,607,611],"li",{},[608,609,610],"strong",{},"登入、註冊、首頁不記錄","：不然使用者登入後又跳回登入頁。",[605,613,614,617],{},[608,615,616],{},"已經有值就不覆蓋","：使用者被帶去登入的途中，還可能經過其他路由，保留第一個才是原本的目的地。",[605,619,620,623,624,627,628,631],{},[608,621,622],{},"第一次進站不記錄","：",[32,625,626],{},"from.name"," 是 ",[32,629,630],{},"undefined"," 時，代表直接開啟網址，不是站內導覽。",[605,633,634,637],{},[608,635,636],{},"重新整理不記錄","：這時來源和目標是同一個路徑。",[605,639,640,643],{},[608,641,642],{},"登出時要清除","：不然下一個人登入後，會跳到上一個使用者的頁面。",[10,645,646],{},"最後一點最容易忘記。兩個人共用一台電腦時，這不只是體驗問題，也可能看到不該看的頁面。",[17,648,649],{"id":649},"為什麼不用網址參數",[10,651,652,653,656],{},"另一種常見做法是把目的地放在網址，例如 ",[32,654,655],{},"\u002Flogin?redirect=\u002Forders\u002F123","。它的好處是重新整理不會消失，也方便分享。",[10,658,659,660,663],{},"但要注意 Open Redirect：如果直接跳轉網址參數的值，攻擊者可以做出 ",[32,661,662],{},"?redirect=https:\u002F\u002Fevil.example"," 的連結，用你的網域騙使用者。使用這種做法時，要檢查目的地只能是站內路徑。",[10,665,666],{},"這個專案把路由物件存在 Vuex，目的地不來自外部輸入，所以沒有這個問題；代價是整頁重新整理後狀態會消失，回到首頁。",[17,668,669],{"id":669},"這次學到的事",[602,671,672,675,678],{},[605,673,674],{},"看到一排相似的旗標時，先想它們是不是在表達同一件事。",[605,676,677],{},"共用邏輯放在 Router Guard，比散落在各頁面好維護。",[605,679,680],{},"重構後程式變少了，但邊界條件才是真正要花時間想清楚的地方。",[682,683,684],"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 .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}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 .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":30,"searchDepth":49,"depth":49,"links":686},[687,688,689,690,691,692],{"id":19,"depth":49,"text":20},{"id":186,"depth":49,"text":187},{"id":576,"depth":49,"text":577},{"id":597,"depth":49,"text":597},{"id":649,"depth":49,"text":649},{"id":669,"depth":49,"text":669},"case","2026-03-10","原本每個頁面各用一個旗標記錄登入後要去哪裡，新增頁面就要多一個。記錄如何改用 Router Guard 統一處理，以及需要排除的邊界情況。",false,"md",{},"\u002Fblog\u002Fredirect-after-login",{"title":5,"description":695},"blog\u002Fredirect-after-login",[703,704,705],"Vue Router","Vuex","Refactoring","zIJg6fkodYDyme14VbBYVysujYdVuyM7zQbmgUHG-E0"]