聊完狀態管理之後,我們來聊一下前端的路由。最近各大家前端工具都有支援前端路由的功能。而 Vue-Router 是官方推出的。功能上都大同小異,你也可以自己做一個(我最喜歡自己做輪子了)。

如果你做好了記得開源出來~


前端路由

前端做這件事情應該是 Angular 起了一個頭,畢竟身為 Google 的親生兒子,所以很熱門。其實他並不是什麼很新穎的技術,主要是依靠 HTML5 History API 來做操作。在不支援 History API 的瀏覽器當中,大多都是使用 URL hash 的方式來達成。

例如說:

#/hoomepage

#/helloworld

#/uccu

然後 HTML5 History API 出現之後,我們就能直接去操作網址,看起來就比較美觀。這裡有一個大前提,我們需要將 Web Server 把所有的請求,都指向你的 index.html

Vue Router, HTML5 History Mode

而 Vue Router 的使用方式也很簡單,

import Vue from 'vue';
import Router from 'vue-router';
import App from '@/App.vue';
import HelloWorld from '@/components/HelloWorld.vue';

Vue.use(Router)

const router = new Router({
  mode: 'history',
  routes: [
    {
      path: '/helloworld',
      component: HelloWorld,
      meta: {},
      children: []
    }
  ]
});

new Vue({
  router: router,
  render: h => h(App)
}).$mount('#app');

你在 new Vue 的時候,把 Router 放進去就可以了。


Vue Router 路由設定

這個路由設定有幾個屬性可以使用:

  • mode 路由模式,可用數值有:
    • hash 瀏覽器預設,就是 URL hash 的方式。
    • history 使用 HTML5 History API 的方式。
    • abstract 所有 JavaScript 環境皆可用。如果取不到瀏覽器 API,會強制進入這模式。
  • base 路由的根路徑,預設是使用 / 這個根路徑。
  • routes 是個陣列,裡面包含了你的路由設定。
  • linkActiveClass 設定 全域<router-link> 若路由相符,會加入的 class 名稱。
  • linkExactActiveClass 跟上一個很類似,但你的路由必須要「完全相同」才會加入 class 名稱。
  • scrollBehavior 設定捲軸的屬性,例如你想要「上一頁」回到某個特定位置的時候。
  • parseQuery 解析查詢字串的自定義函式,會覆蓋預設函式。
  • stringifyQuery 反解析查詢字串的自定義函式,會覆蓋預設函式。
  • fallback 僅接受布林值,當你的路由模式不支援 History API 時,會退回去使用 URL hash 的方式,預設為 true

而這個 Router 的內建方法有:

  • beforeEach 當路由開始進入之前,會執行此函式。函式會回傳三個參數,
    • to 將要前往的 Route 物件實例。
    • from 來源的 Route 物件實例。
    • next 這形同於回呼函式,你必須要呼叫他 next() 他才會繼續往下做。
  • beforeResolve 當路由內部的所有路由防護規則都被解析之後執行,跟 beforeEach 一樣有三個參數。
  • afterEach 當路由結束操作後,會呼叫此函數。跟 beforeEach 一樣,但是沒有 next() 的回呼函式。
  • push 類似 History API 的 pushState,本身是可以接受 Promise 操作。
  • replace 類似 History API 的 replaceState,本身是可以接受 Promise 操作。
  • go 類似 History API 的 go,傳入的是步驟數字。
  • back 你可以把他想像成,把 go 傳入負值,例如 go(-1)
  • forwardgo 很類似的操作。
  • resolve 取回一個解析過的 URL,他可以返回:
    • location 回傳 Location 物件實例。
    • route 回傳 Route 物件實例。
    • href 回傳 URL 字串。
  • getMatchedComponents 回傳解析路徑後所符合的 Vue 元件。
  • addRoutes 加入路由,這個路由是陣列格式。
  • onReady 所有的路由以及路由所使用的 Vue 元件都解析完成後,會呼叫此函數。
  • onError 路由以及路由所使用的 Vue 元件解析若有錯誤,會呼叫此函數。

我們從比較基礎的部分來講,最主要是 routes 的相關設定,他僅能接受 Route 設定資料物件,這個物件本身包含了以下幾個屬性跟方法:

  • path 路徑,從 base 開始算。
  • name 這一個路由的名稱,他會在一些路由方法中可以使用。
  • component 這個路由配置的 Vue 元件。
  • components 設置有命名規則的 Vue 元件。
  • redirect 重新導向路徑,可以是字串﹑Location 物件或是函式。
  • props 將路由上面的正規配置當作屬性傳入元件當中,可以是布林值、物件或函式。
  • alias 路由別名,可以是字串或是字串組成的陣列。
  • children 嵌套路由,就是這個路由的兒子(你可以這麼理解沒問題)。
  • meta 可傳入任何值,用於路由本身的資料集。
  • beforeEnter 進入此路由前,會先執行的防禦路由方法函式,自身會帶入 to, fromnext() 參數。
  • caseSensitive 路由本身是否區身大小寫,僅接受布林值,預設為 false
  • Path-to-RegExp 可接受 Path-to-RegExp 的設定。

官方說明 RouteConfig

我們用文章一開始的例子來擴充解釋:

import Router from 'vue-router'

import NotFound from '@/components/NotFound.vue'
import HelloKittyWithId from '@/components/HelloKittyWithId.vue'
import HelloKittyWithName from '@/components/HelloKittyWithName.vue'
import HelloKitty from '@/components/HelloKitty.vue'
import HelloWorld from '@/components/HelloWorld.vue'
import PathRegex from '@/components/PathRegex.vue'

const router = new Router({
  mode: 'history',
  routes: [
    {
      path: '/helloworld',
      name: 'HelloWorld',
      component: HelloWorld,
      beforeEnter (to, from, next) {
        if (from.name === 'HelloKitty') {
          next({ to: 'NotFound' })
        }
        next()
      },
      meta: {
        pageNeedLogin: true,
        showFooter: false
      },
      children: [
        {
          path: 'dark',
          name: 'HelloDarkWorld',
          component: HelloDarkWorld
        }
      ]
    },
    {
      path: '/hellokitty',
      name: 'HelloKitty',
      component: HelloKitty,
      children: [
        {
          path: 'id/:id',
          name: 'HelloKittyWithId',
          components: {
            a: HelloKittyWithId,
            b: HelloKittyWithName
          },
          props: true
        }
      ],
      alias: [
        'hellokitty-alias',
        'hellokitty-ok'
      ]
    },
    {
      path: '/path-to-regex',
      name: 'PathRegex',
      component: PathRegex,
      pathToRegexpOptions: {
        sensitive: true,
        strict: true
      }
    },
    {
      path: '/hellokitty2',
      redirect: '/hellokitty'
    },
    {
      path: '/hellokitty3',
      redirect: { name: 'HelloKitty' }
    },
    {
      path: '*?',
      name: 'NotFound',
      component: NotFound
    }
  ]
});

以上是大部分使用上的例子,不過由於 pathToRegexpOptions 比較特別,這裡就不多做解釋。

  • path 當中,你可以使用 * 來做萬用匹配。
  • path 當中使用 :id 表示接受一個變數為 id 的數值。
  • 如果你有設定 props: true 的話,可以將剛剛的 id 傳入元件中。
  • 原本的 :id 也可以加入正規,例如說 :id([0-9]+) 這樣的作法。
  • redirect 就是轉頁的概念,他後面也可以接函式。
  • alias 的概念就是設定好的路徑,會被導向同一組路由設定。
  • beforeEnter 要記得呼叫 next() 他才會繼續往下做。
  • children 的路徑就會接續他老爸再繼續往後,記得不要在 path/,不然他會回到根目錄去。
  • components 建議要有一個預設值 default,不然你的樣版內,非命名規則的樣版會無法顯示。

另外一點,最上層的 Route 的開頭就一定要有 / 的設定,不然會被警告,且會失效!


關於 Router 元件應用

每個路由元件的載入,扣除最根元件以外,路由需要搭配一組元件來做應用。而在 App.vue 裡面,需要使用 <router-view> 來當作一個 Route 的進入點。而,若是你使用嵌套路由,那麼你的嵌套路由父元件也是需要一組 <router-view> 來作為進入點。

<template>
  <section>
    <router-view></router-view>
  </section>
</template>

然而,<router-view> 有一個屬性 name 可以用,他的應用範圍,是將 Route 設定當中,有命名的元件來套用。在你的 Route 設定中,要使用 components 來設定,舉例來說,

<template>
  <section>
    <!-- 這個區段會渲染 `default` 的元件 -->
    <router-view></router-view>
    <!-- 這個區段會渲染命名為 `a` 的元件 -->
    <router-view name="a"></router-view>
  </section>
</template>

那麼你的路由設定可能是這樣,

import Router from 'vue-router'

import HelloKitty from '@/components/HelloKitty.vue'
import HelloWorld from '@/components/HelloWorld.vue'

const router = new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'HelloWorld',
      components: {
        default: HelloKitty,
        a: HelloWorld
      }
    }
  ]
});

若是使用嵌套路由的話,就如同上述例子一樣,你必須提供一個 <router-view> 來當作進入點。


我們有了進入點之後,Vue Router 提供了一個樣版方法 <router-link> 用來渲染出對應的連結,這個樣版方法可以接受下列屬性:

  • to 接受命名的路由名稱,或是路由物件。路由物件包含了:
    • path 直接填寫路徑,不要與 name 混用。
    • name 填入命名的路由名稱,不要與 path 混用。
    • params 路由若有使用正規,則可填入路由正規所指定的變數數值,使用 Object 模式傳入。
    • query 設定網址搜尋資料,亦即 ? 後面的設定值,使用 Object 模式傳入。
  • replace 原本的 to 指令預設是使用 router.push() 的方式,若加了這個屬性,則會適用 router.replace() 的方式執行。若使用 replace,則你的 History 不會留下記錄。
  • append 意思就是把現在的路徑 加入 這個 <router-link> 所設定的路徑。
  • tag 原本 <router-link> 預設使用 A 標籤,這個設定可以讓你指定其他標籤。
  • active-class 當路由正規規則符合時,會套用這個屬性所指定的類別名稱。預設會使用 router-link-active 這個名稱。
  • exact 如果加入這個屬性,則需要正規 完全符合 的時候,才會套用 exact-active-class 指定的類別名稱。
  • exact-active-class 當路由正規規則 完全符合 時,會套用這個屬性所指定的類別名稱。預設會使用 router-link-exact-active 這個名稱。
  • event 觸發該路由的事件,預設是使用 click 事件。

關於上述的 params,所謂路由正規其實就是文章上面範例中,所提到的 id/:id 這樣的例子,而這個例子在 <router-link> 當中的設定,看起來就會像這個樣子:

<template>
  <section>
    <router-link
      :to="{ name: 'HelloKittyWithId', params: { id: 123 } }"
    ></router-link>
  </section>
</template>

而在 Vue Router 3.1.0 版本之後,對於 <router-link> 則提供了一個插槽( Slot )的新功能。這個功能可以讓你替換調原本 <router-link> 的結構,這個插槽提供了幾個數值讓你使用:

  • href 解析過後的網址,提供給 A 標籤的 href 使用的字串。
  • route 傳回一個 Route 的完整物件。
  • navigate 觸發路由的事件函式, 必要時會阻止此事件執行
  • isActive 回傳路由正規是否符合,回傳值為布林值。
  • isExactActive 回傳路由正規是否 完全符合 ,回傳值為布林值。

這個新功能可以讓你自己決定你的 <router-link> 會長成什麼樣子。舉例來說:

<template>
  <section>
    <router-link
      v-slot="{ href, route, navigate, isActive, isExactActive }"
    >
      <a :href="href" @click="navigate">
        <img src="./assets/img/logo.png" alt="Logo">
        <span>{{ route.fullPath }}</span>
      </a>
    </router-link>
  </section>
</template>

請注意,若你的 A 標籤有使用 target="_blank",則 navigate 會被忽略。

https://router.vuejs.org/api/#v-slot-api-3-1-0

另外有一件事情,由於 <router-link> 實際上只是幫你做路由的動作,所以,如果你在 <router-link> 上面綁定了 click 的時候,你會發現沒有被觸發:

<template>
  <section>
    <router-link to="hello" @click="hello">Hello</router-link>
    <router-link to="kitty" @click.native="kitty">Kitty</router-link>
  </section>
</template>

上述的例子,只有 kitty 這個事件會被觸發。亦即,你必須要在 click 事件的後方,加入 .native 修飾子才會有效。有沒有加入 .native 的差異在於,一般在 Vue 的事件綁定中,是以 Vue-Event 為主,而加上了 .native 則代表,你想要觸發的是 DOM 的原生事件。

是的,在 <router-link> 這個元件上,需要用到 .native 來確保事件發生。不然,<router-link> 基本上都會直接忽略你所綁定的事件。


小結

前端路由確實是一個風潮,只是說不知道能持續多久?倘若以 SPA 來看,大家都做成一頁了,好像也沒必要使用前端路由了?

但是,

你覺得我會這麼安分的就這樣放過路由這件事嗎?

剩下的,就待下回分曉了(燦笑)。

ITHome 鐵人賽同步刊登 Router 基本入門 Day 9