iT邦幫忙

2026 iThome 鐵人賽

DAY 11
0
Modern Web

Vue 前端工程師視角看 Flutter Web系列 第 11

Day 11|vue-router 基礎回顧:history mode、路由守衛

  • 分享至 

  • xImage
  •  

模組三|Routing(Day 11–14)

先備:Day 2(widget 是建構子、不是標籤)、Day 7(頁面級 state 走 setState)。這一篇的 Dart code 只有一段,看不懂可以先跳到「差異與坑」。

路由大概是 Vue 開發裡最無感的一塊:createRouter 十分鐘設定完,之後幾個月不會再打開那個檔案。我自己的訂票 demo 就是這樣——src/router/index.ts 從第一天寫完就再也沒動過。但正因為它太順了,你可能從沒想過自己到底默認了哪些假設。而 Flutter Web 的路由,恰好把這些假設一條一條拆給你看,所以模組三的第一篇,我要先把 vue-router 的心智模型攤在桌上,之後三篇才有座標可以對。

結論先講,你可以帶著它讀下去:

vue-router 的一切都建立在「URL 是應用狀態的一等公民」這個前提上。

你宣告哪個 URL 對應哪個元件,剩下的(history 監聽、元件切換、返回鍵)框架全包。Flutter 的預設路由模型則是從 mobile 長出來的:頁面是一疊卡片,URL 只是事後補上的副產品。方向相反,這個差異是接下來四篇所有坑的源頭。

Vue 怎麼做

標準配置長這樣,十年來沒什麼大變:

import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', component: Home },
    { path: '/product/:id', component: ProductDetail, props: true },
    { path: '/admin', component: Admin, meta: { requiresAuth: true } },
  ],
})

// 全域前置守衛:最常見的登入牆
router.beforeEach((to) => {
  if (to.meta.requiresAuth && !isLoggedIn()) {
    return { path: '/login', query: { redirect: to.fullPath } }
  }
})

兩個重點值得回顧,因為它們在 Flutter 那邊都會變形:

1. history mode 的本質。 createWebHistory 用 History API(pushState / popstate)讓 URL 長得像 /product/42 而不是 /#/product/42。代價大家都踩過:使用者直接開 /product/42 或按重新整理時,伺服器上並沒有這個檔案,所以要設 rewrite 全部導回 index.html。這是 SPA 的原罪,跟 Vue 無關——記住這點,Day 14 和 Day 23 會再遇到它兩次。

2. 守衛是一條攔截管線。 beforeEach → 路由獨享的 beforeEnter → 元件內守衛,一路可以放行、改道、取消,甚至可以在裡面等一個非同步請求回來再決定。這個自由度,等 Day 12 看到 go_router 的 redirect 時你會發現被收窄了。

不過我要誠實交代對照組的規模。這是 Waypoint Air Vue 端的路由表全文(src/router/index.ts:13-24):

const routes = [
  { path: '/', name: 'home', component: HomeView },
  { path: '/login', name: 'login', component: LoginView },
  { path: '/date-select', name: 'date-select', component: DateSelectView },
  { path: '/passenger-select', name: 'passenger-select', component: PassengerView },
  { path: '/flights', name: 'flights', component: FlightListView },
  { path: '/confirm', name: 'confirm', component: FlightConfirmView },
  // …共 10 條,全部同一個形狀
]

十條扁平路由,每一條都是 path + name + component。沒有動態參數/flights 查的是 store 裡的條件,不是 URL 上的 :id)、沒有巢狀路由沒有 lazy loading(十個 view 全部在檔頭靜態 import)、沒有任何守衛沒有 404 fallback。也就是說,上面那段教科書範例裡最值錢的三個特性,我的 demo 一個都沒用到。

我把這件事寫出來,是因為它剛好說明了為什麼路由這塊「感覺很簡單」:多數 App 的路由表確實就長這樣。真正的複雜度不在宣告,在你開始需要參數、守衛、巢狀之後——而那正是兩個生態開始分岔的地方。

Flutter Web 怎麼做

先看 Flutter 的「原廠設定」,也就是 Navigator 1.0 的命名路由。我刻意先不上 go_router,因為原廠模型才能暴露心智落差:

void main() {
  runApp(MaterialApp(
    routes: {
      '/': (context) => const HomePage(),
      '/admin': (context) => const AdminPage(),
    },
    // 動態參數?沒有 '/product/:id' 這種寫法,
    // 得自己在 onGenerateRoute 手動 parse 字串。
    onGenerateRoute: (settings) {
      final uri = Uri.parse(settings.name ?? '/');
      if (uri.pathSegments.length == 2 &&
          uri.pathSegments.first == 'product') {
        final id = uri.pathSegments[1];
        return MaterialPageRoute(
          builder: (_) => ProductDetailPage(id: id),
        );
      }
      return null;
    },
  ));
}

// 導航是命令式的:把一頁「推」進堆疊
Navigator.pushNamed(context, '/product/42');

看出味道了嗎?Navigator.push 的動詞是「推」——它操作的是一個頁面堆疊,不是一個 URL。在 mobile 上這完全合理:App 沒有網址列,返回鍵就是 pop。但搬到 Web,幾件 Vue 人視為空氣的事情開始要錢:

  • URL 預設是 hash mode/#/product/42),要漂亮路徑得自己切換 path strategy,Day 14 詳談。
  • 動態路由參數要手動 parse,上面那段 onGenerateRoute 就是 vue-router 一行 :id 的等價物,十幾行換一個冒號。
  • 沒有內建守衛管線,登入牆只能自己在每個 push 之前寫 if,或者等 Day 12 的 go_router redirect

差異與坑

坑一:心智模型是 URL-first vs Stack-first。 vue-router 是「URL 變了,畫面跟著變」;Navigator 1.0 是「畫面堆疊變了,URL 盡力跟上」。方向相反。這解釋了為什麼 Flutter Web 早期最常見的抱怨是瀏覽器返回鍵行為怪異——返回鍵動的是 URL,但真正的狀態在堆疊裡,兩者只是「盡力」同步。症狀:使用者按了返回鍵,網址列變回上一頁,畫面卻停在原地;或者反過來,畫面退了但網址沒動,重新整理就跳回錯的頁。

坑二:伺服器 rewrite 的債兩邊都要還。 Vue 的 history mode 要設 rewrite,Flutter Web 拿掉 hash 之後一樣要。差別在資料量:Vue 社群踩了十年,每個託管平台都有現成教學;Flutter Web 這邊少一個量級,而且錯誤訊息完全不會提示你是這個原因。症狀:本機 flutter run 一切正常,部署上去點連結也正常,唯獨直接貼網址或按 F5 會拿到平台的 404 頁——因為請求真的送到伺服器了,而那裡沒有這個檔案。

坑三:守衛邏輯沒有地方放。 Vue 把攔截集中在 router 這一層,全站只有一個入口;Navigator 1.0 沒有這層,登入檢查會散落在每一個 push 呼叫點。這也是為什麼 Day 12 的 go_router 在 Flutter Web 幾乎是必選項,而不是可選項。症狀:新增一個需要登入的頁面,你在三個地方加了檢查、漏了第四個,未登入的使用者從某個角落的按鈕就直接進去了,而且沒有任何工具會提醒你漏了。

坑四:字串路由是整個 API 型別最弱的一環。 這週的範例 code 我依然沒有手寫 Dart,onGenerateRoute 的回傳型別是 Route<dynamic>?,agent 少接一個 null case,flutter analyze 當場報錯——這是本系列獨家論點的又一次兌現。但 '/product/42' 這個字串本身,編譯器一個字都幫不上。症狀:把 '/profile' 打成 '/profle',編譯全綠、analyze 全綠,跑起來點下去畫面一片空白或直接丟例外。這個伏筆留給 Day 12 的 typed routes 收。

小結與下一篇

一句話總結:vue-router 給你的其實是「URL 即狀態」這個世界觀,而 Flutter 原廠路由的世界觀是「堆疊即狀態」——工具可以換,世界觀的翻譯才是成本。

順帶說一件我改觀的事:寫這篇之前我以為 Navigator 1.0 只是「比較土」,看完 onGenerateRoute 才發現它不是同一個抽象層級的東西——它更像是 Express 的 handler,而不是 vue-router 的 routes 陣列。明天 Day 12,看官方答案 go_router 怎麼把宣告式路由帶回 Flutter:〈go_router 宣告式路由:對照 vue-router 設定方式〉。

參考資料

如果你卡在語法

深入原理


上一篇
Day 10|Bloc/Cubit:事件驅動狀態管理,Vue 有類似模式嗎?
下一篇
Day 12|go_router 宣告式路由:對照 vue-router 設定方式
系列文
Vue 前端工程師視角看 Flutter Web17
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言