2022/03/28

Vue Router

以下為 Vue Router 的一個例子,Vue Router 的用途,是能夠在單一網頁頁面中,在 browser 不重新載入到新的 url 的狀況下,能夠增加 url history 並調整頁面內容狀態的功能,也就是能夠實現 SPA (single page application) 的功能。

傳統的網頁,會向 web application server 要求打開一個 url 網址,server 會回傳整個網頁的 html 內容。後來為了在不轉向到新的 url 的條件下,並調整網頁的內容,就發生了 web service,server 會從某個網址回傳 XML 或 JSON,網頁透過 AJAX 方式提取資料更新網頁內容。SPA 是更進一步,可在不重新發出新的 url request 到 server 的條件下,更新網頁內容並增加 url 瀏覽歷程,也就是增加了單一網頁頁面的顯示狀態。

Vue router 實例:

<!DOCTYPE html>
<html lang="en">
<head>
  <!-- <script src="https://unpkg.com/vue@3.2.10"></script> -->
  <!-- <script src="https://unpkg.com/vue@3.2.10/dist/vue.global.js"></script> -->
  <script src="https://unpkg.com/vue@3.2.10/dist/vue.global.prod.js"></script>
  <!-- <script src="https://unpkg.com/vue-router@4.0.11"></script> -->
  <!-- <script src="https://unpkg.com/vue-router@4.0.11/dist/vue.global.js"></script> -->
  <script src="https://unpkg.com/vue-router@4.0.11/dist/vue-router.global.prod.js"></script>
<!--
  <script src="https://unpkg.com/vue/dist/vue.js"></script>
  <script src="https://unpkg.com/vue-router/dist/vue-router.js"></script> -->

</head>

<body>

<div id="app">
  <h1>Hello App!</h1>
  <p>
    <!-- use the router-link component for navigation. -->
    <!-- specify the link by passing the `to` prop. -->
    <!-- `<router-link>` will render an `<a>` tag with the correct `href` attribute -->
    <router-link to="/">Go to Home</router-link>
    <router-link to="/foo">Go to Foo</router-link>
    <router-link to="/bar">Go to Bar</router-link>
  </p>
  <!-- route outlet -->
  <!-- component matched by the route will render here -->
  <router-view></router-view>
</div>

<script>
// 1. Define route components.
// These can be imported from other files
const Home = { template: '<div>Home</div>' }
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }

// 2. Define some routes
// Each route should map to a component.
// We'll talk about nested routes later.
const routes = [
  { path: '/', component: Home },
  { path: '/foo', component: Foo },
  { path: '/bar', component: Bar }
]

// 3. Create the router instance and pass the `routes` option
// You can pass in additional options here, but let's
// keep it simple for now.
const router = VueRouter.createRouter({
  // 4. Provide the history implementation to use. We are using the hash history for simplicity here.
  history: VueRouter.createWebHashHistory(),
  routes, // short for `routes: routes
})

// 5. Create and mount the root instance.
const app = Vue.createApp({})
// Make sure to _use_ the router instance to make the
// whole app router-aware.
app.use(router)

app.mount('#app')

// Now the app has started!
</script>
</body>

</html>

以下是點擊了 foo 以後,網頁的 DOM 產生的資料。 foo 的部分,會自動加上這兩個 css class class ="router-link-exact-active router-link-active"

<div id="app" data-v-app="">
   <h1>Hello App!</h1>
   <p>
     <a href="#/" class="">Go to Home</a>
     <a href="#/foo" class="router-link-active router-link-exact-active" aria-current="page">Go to Foo</a>
     <a href="#/bar" class="">Go to Bar</a>
   </p>
   <div>foo</div>
</div>

<router-link> 相關屬性

to

<!-- 直接填寫文字字串 -->
<router-link to="/home">Home</router-link>
<!-- 結果 -->
<a href="/home">Home</a>


<!-- 使用 v-bind,省略 path -->
<router-link v-bind:to="'/home'">Home</router-link>

<!-- 不寫 v-bind 也可以,就像綁定別的屬性一樣 -->
<router-link :to="'/home'">Home</router-link>

<!-- 同上 -->
<router-link :to="{ path: '/home' }">Home</router-link>

<!-- 命名的路由 -->
<router-link :to="{ name: 'user', params: { userId: 123 }}">User</router-link>

<!-- 帶查詢參數,下面的結果為 /register?plan=private -->
<router-link :to="{ path: '/register', query: { plan: 'private' }}">Register</router-link>

replace

如果不希望在 browser 留下 url history,可加上 replace

<router-link to="/home" replace>Home</router-link>

當點擊時,會呼叫 router.replace() 而不是 router.push()

active-class

設定當 link 啟用時,DOM 節點使用的 css class

<style>
   ._active{
      background-color : red;
   }
</style>
<p>
   <router-link v-bind:to = "{ path: '/route1'}" active-class = "_active">Router Link 1</router-link>
   <router-link v-bind:to = "{ path: '/route2'}">Router Link 2</router-link>
</p>

aria-current-value

預設為 "page",可能的值

'page' | 'step' | 'location' | 'date' | 'time' | 'true' | 'false' (string)

當 link 為 active 時,傳送給 aria-current 屬性的值

custom

預設為 "false"

決定 <router-link> 是不是"不要"產生到 <a> 裡面。如果使用 v-slot 產生 custom router link,預設要包裝在 <a> 裡面,如果增加 custom 屬性,就取消這個限制

<router-link to="/home" custom v-slot="{ navigate, href, route }">
  <a :href="href" @click="navigate">{{ route.fullPath }}</a>
</router-link>

會 render 為

<a href="/home">/home</a>
<router-link to="/home" v-slot="{ route }">
  <span>{{ route.fullPath }}</span>
</router-link>

會 render 為

<a href="/home"><span>/home</span></a>

exact-active-class

當 link 被精確匹配時,要啟用的 css class

<router-link> 的 v-slot

可產生自訂的 html tag,記得一定要加上 custom

ex:

<router-link
  to="/foo"
  custom
  v-slot="{ href, route, navigate, isActive, isExactActive }"
>
  <NavLink :active="isActive" :href="href" @click="navigate">
    {{ route.fullPath }}
  </NavLink>
</router-link>
  • href: resolved url,類似 a tag 的 href
  • route: resolved normalized location
  • navigate: 驅動 navigation 的 function,必要時,會自動 prevent events
  • isActive: 如果被 apply active class ,就會是 true
  • isExactActive: 如果被 apply exact active class,就會是 true

會 render 為

<navlink active="true" href="#/foo">/foo</navlink>

ex:

    <ul>
      <router-link
        to="/foo"
        custom
        v-slot="{ href, route, navigate, isActive, isExactActive }"
      >
        <li
          :class="[isActive && 'router-link-active', isExactActive && 'router-link-exact-active']"
        >
          <a :href="href" @click="navigate">{{ route.fullPath }}</a>
        </li>
      </router-link>
    </ul>

會 render 為

<ul>
  <li class="router-link-active router-link-exact-active"><a href="#/foo">/foo</a></li>
</ul>

Dynamic Route Matching

Vue 需要將某個 pattern 對應到一個 component 的方法,例如不同 userid 的 user 資料

$route.params 可用來協助對應 route 的 pattern 上的參數

const User = {
  template: '<div>User {{ $route.params.id }}</div>',
}

// these are passed to `createRouter`
const routes = [
  // dynamic segments start with a colon
  { path: '/users/:id', component: User },
]
pattern matching $route.params
/users/:id /users/john { id: 'john' }
/users/:id/posts/:postid /users/john/posts/123 { id: 'john', posted: '123' }

Reacting to Params Changes

因為 /users/john/users/mary 這兩個路徑會使用同一個 component,比較有效率的方法,是不重建 component,直接更新內容,但這樣也不會呼叫到 component 的 lifecycle hooks

const User = {
  template: '<div>User {{ $route.params.id }}</div>',
  created() {
    this.$watch(
      () => this.$route.params,
      (toParams, previousParams) => {
        // react to route changes...
        console.log("created toParams=", toParams, ", previousParams=",previousParams);
      }
    )
  },
  // navigation guard,可在這邊檢查並取消 navigation
  async beforeRouteUpdate(to, from) {
    // react to route changes...
    // this.userData = await fetchUser(to.params.id)
    console.log("beforeRouteUpdate to=", to, ", from=",from);
    this.userData = to.params.id
  },
}

Catch /404 Not Found Route

const routes = [
  // will match everything and put it under `$route.params.pathMatch`
  { path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound },
  // will match anything starting with `/user-` and put it under `$route.params.id`
  { path: '/user-:id(.*)', component: User },
]

如果直接在 brwoser 網址連結到

1-2-param.html#/not

就會是用 NotFound 這個 Component 處理

如果直接在 brwoser 網址連結到

1-2-param.html#/user-test/12345

會收到 $route.params.id 的值為 test/12345

Routes' Matching Syntax

大部分的 application 使用 static route 以及類似 /users/:userId 這樣的 route

custom regex

如果要在 url 分辨 orderId 及 productName,最簡單的方法就是加上 /o /p 靜態的部分用來區別

const routes = [
  // matches /o/3549
  { path: '/o/:orderId' },
  // matches /p/books
  { path: '/p/:productName' },
]

如果 orderId 跟 productName 可用數字/字串區分,可將 route 改為

const routes = [
  // /:orderId -> matches only numbers
  { path: '/:orderId(\\d+)' },
  // /:productName -> matches anything else
  { path: '/:productName' },
]

repeatable params

如果是 /first/second/third 這樣的 route

* 為 0 or more, + 為 1 or more

const routes = [
  // /:chapters -> matches /one, /one/two, /one/two/three, etc
  { path: '/:chapters+' },
  // /:chapters -> matches /, /one, /one/two, /one/two/three, etc
  { path: '/:chapters*' },
]

可用以下方式,將 array 參數轉換為 path

// given { path: '/:chapters*', name: 'chapters' },
router.resolve({ name: 'chapters', params: { chapters: [] } }).href
// produces /
router.resolve({ name: 'chapters', params: { chapters: ['a', 'b'] } }).href
// produces /a/b

// given { path: '/:chapters+', name: 'chapters' },
router.resolve({ name: 'chapters', params: { chapters: [] } }).href
// throws an Error because `chapters` is empty
const routes = [
  // only match numbers
  // matches /1, /1/2, etc
  { path: '/:chapters(\\d+)+' },
  // matches /, /1, /1/2, etc
  { path: '/:chapters(\\d+)*' },
]

optional param

? 代表 0 or 1

const routes = [
  // will match /users and /users/posva
  { path: '/users/:userId?' },
  // will match /users and /users/42
  { path: '/users/:userId(\\d+)?' },
]

Nested Routes

在 application 裡面常見到 nested components

ex:

/user/johnny/profile                  /user/johnny/posts
+------------------+                  +-----------------+
| User             |                  | User            |
| +--------------+ |                  | +-------------+ |
| | Profile      | |  +------------>  | | Posts       | |
| |              | |                  | |             | |
| +--------------+ |                  | +-------------+ |
+------------------+                  +-----------------+
<div id="app">
 <h1>Nested Views</h1>
  <p>
    <router-link to="/users/eduardo">/users/eduardo</router-link>
    <br />
    <router-link to="/users/eduardo/profile"
      >/users/eduardo/profile</router-link
    >
    <br />
    <router-link to="/users/eduardo/posts">/users/eduardo/posts</router-link>
  </p>
  <router-view></router-view>
</div>

<script>

const User = {
  template: '<h2>User {{ $route.params.username }}</h2><router-view></router-view>',
}
const UserHome = {
  template: '<div>Home</div>',
}
const UserProfile = {
  template: '<div>UserProfile</div>',
}
const UserPosts = {
  template: '<div>UserPosts</div>',
}

const routes = [
  {
    path: '/users/:username',
    component: User,
    children: [
      // UserHome will be rendered inside User's <router-view>
      // when /users/:username is matched
      { path: '', component: UserHome },

      // UserProfile will be rendered inside User's <router-view>
      // when /users/:username/profile is matched
      { path: 'profile', component: UserProfile },

      // UserPosts will be rendered inside User's <router-view>
      // when /users/:username/posts is matched
      { path: 'posts', component: UserPosts },
    ],
  }
]

const router = VueRouter.createRouter({
  history: VueRouter.createWebHashHistory(),
  routes,
})

const app = Vue.createApp({})
app.use(router)

app.mount('#app')

</script>

Programmatic Navigation

除了用 <router-link> 產生 url anchor tags 以外,也可以用程式處理

在 vue application 中,可用 $router 為 route instance,故要呼叫 this.$router.push

<router-link :to="..."> 就等同於 router.push(...)

// literal string path
router.push('/users/eduardo')

// object with path
router.push({ path: '/users/eduardo' })

// named route with params to let the router build the url
router.push({ name: 'user', params: { username: 'eduardo' } })

// 如果有 path,就會忽略 params
// with query, resulting in /register?plan=private
router.push({ path: '/register', query: { plan: 'private' } })

// with hash, resulting in /about#team
router.push({ path: '/about', hash: '#team' })

可用 uername 變數

const username = 'eduardo'
// we can manually build the url but we will have to handle the encoding ourselves
router.push(`/user/${username}`) // -> /user/eduardo
// same as
router.push({ path: `/user/${username}` }) // -> /user/eduardo
// if possible use `name` and `params` to benefit from automatic URL encoding
router.push({ name: 'user', params: { username } }) // -> /user/eduardo
// `params` cannot be used alongside `path`
router.push({ path: '/user', params: { username } }) // -> /user

replace current location

切換網址,但不會 push 到 history

<router-link :to="..." replace> 等同 router.replace(...)

router.push({ path: '/home', replace: true })
// equivalent to
router.replace({ path: '/home' })

history

// go forward by one record, the same as router.forward()
router.go(1)

// go back by one record, the same as router.back()
router.go(-1)

// go forward by 3 records
router.go(3)

// fails silently if there aren't that many records
router.go(-100)
router.go(100)

named route

為 route 命名,優點:

  • 沒有 hardcoded url
  • 可自動 encode/decode params
  • 填寫 url 不會發生 typo
  • bypass path ranking
const routes = [
  {
    path: '/user/:username',
    name: 'user',
    component: User
  }
]

用以下方式使用 named route

<router-link :to="{ name: 'user', params: { username: 'erina' }}">
  User
</router-link>

router.push({ name: 'user', params: { username: 'erina' } })

named views

有時候,需要一次顯示多個 views,但不是 nested view

/settings/emails                                       /settings/profile
+-----------------------------------+                  +------------------------------+
| UserSettings                      |                  | UserSettings                 |
| +-----+-------------------------+ |                  | +-----+--------------------+ |
| | Nav | UserEmailsSubscriptions | |  +------------>  | | Nav | UserProfile        | |
| |     +-------------------------+ |                  | |     +--------------------+ |
| |     |                         | |                  | |     | UserProfilePreview | |
| +-----+-------------------------+ |                  | +-----+--------------------+ |
+-----------------------------------+                  +------------------------------+
<div id="app">
  <h1>Nested Named Views</h1>
  <router-view></router-view>
</div>

<script>

const About = {
  template: '<h1>About</h1>',
}
const Home = {
  template: '<div>Home</div>',
}
const UserEmailsSubscriptions = {
  template: '<div><h3>Email Subscriptions</h3></div>',
}
const UserProfile = {
  template: '<div><h3>UserProfile</h3></div>',
}
const UserProfilePreview = {
  template: '<div><h3>UserProfilePreview</h3></div>',
}


const UserSettingsNavTemplate = `
  <div class="us__nav">
    <router-link to="/settings/emails">emails</router-link>
    <br />
    <router-link to="/settings/profile">profile</router-link>
  </div>
  `
// const UserSettingsNav = {
//   template: UserSettingsNavTemplate
// }

const UserSettingsTemplate = `
  <div class="us">
    <h2>User Settings</h2>
    <user-settings-nav />
    <router-view class="us__content" />
    <router-view name="helper" class="us__content us__content--helper" />
  </div>
  `
const UserSettings = {
  template: UserSettingsTemplate
}

const routes = [
    {
      path: '/settings',
      // You could also have named views at tho top
      component: UserSettings,
      children: [
        {
          path: 'emails',
          component: UserEmailsSubscriptions,
        },
        {
          path: 'profile',
          components: {
            default: UserProfile,
            helper: UserProfilePreview,
          },
        },
      ],
    },
  ]

const router = VueRouter.createRouter({
  history: VueRouter.createWebHashHistory(),
  routes,
})

const app = Vue.createApp({})
app.use(router)

app.component('user-settings-nav', {
  template: UserSettingsNavTemplate
})

app.mount('#app')

</script>

Redirect and Alias

直接在 routes 做 redirect

const routes = [{ path: '/home', redirect: '/' }]

const routes = [{ path: '/home', redirect: { name: 'homepage' } }]

用 redirect function 做 dynamic redirect

const routes = [
  {
    // /search/screens -> /search?q=screens
    path: '/search/:searchText',
    redirect: to => {
      // the function receives the target route as the argument
      // we return a redirect path/location here.
      return { path: '/search', query: { q: to.params.searchText } }
    },
  },
  {
    path: '/search',
    // ...
  },
]

可 redirect 到相對路徑

const routes = [
  {
    // will always redirect /users/123/posts to /users/123/profile
    path: '/users/:id/posts',
    redirect: to => {
      // the function receives the target route as the argument
      // a relative location doesn't start with `/`
      // or { path: 'profile'}
      return 'profile'
    },
  },
]

alias

/ alias 為 /home ,代表瀏覽 /home/ 都是一樣的 component

const routes = [{ path: '/', component: Homepage, alias: '/home' }]

在 nested view 也可以用

const routes = [
  {
    path: '/users',
    component: UsersLayout,
    children: [
      // this will render the UserList for these 3 URLs
      // - /users
      // - /users/list
      // - /people
      { path: '', component: UserList, alias: ['/people', 'list'] },
    ],
  },
]

如果有參數

const routes = [
  {
    path: '/users/:id',
    component: UsersByIdLayout,
    children: [
      // this will render the UserDetails for these 3 URLs
      // - /users/24
      // - /users/24/profile
      // - /24
      { path: 'profile', component: UserDetails, alias: ['/:id', ''] },
    ],
  },
]

Passing Props to Route Components

在 component 使用 $route 時,會跟 route 綁定在一起。可利用 propsoption

可將以下語法

const User = {
  template: '<div>User {{ $route.params.id }}</div>'
}
const routes = [{ path: '/user/:id', component: User }]

替換為

const User = {
  // make sure to add a prop named exactly like the route param
  props: ['id'],
  template: '<div>User {{ id }}</div>'
}
const routes = [{ path: '/user/:id', component: User, props: true }]

Boolean mode

props 設定為 true,就表示 route.params 會指定為 component props

Named views

可為每一個 named view 定義 props

const routes = [
  {
    path: '/user/:id',
    components: { default: User, sidebar: Sidebar },
    props: { default: true, sidebar: false }
  }
]

Object mode

const routes = [
  {
    path: '/promotion/from-newsletter',
    component: Promotion,
    props: { newsletterPopup: false }
  }
]

Function mode

const routes = [
  {
    path: '/search',
    component: SearchUser,
    props: route => ({ query: route.query.q })
  }
]

URL /search?q=vue 會將 {query: 'vue'} 以 props 傳給 SearchUser component


History Mode

Hash Mode

在網址後面加上 #,不利於 search engine

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

const router = createRouter({
  history: createWebHashHistory(),
  routes: [
    //...
  ],
})

HTML5 Mode

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

const router = createRouter({
  history: createWebHistory(),
  routes: [
    //...
  ],
})

在 url 會看到 https://example.com/user/id ,但實際上如果直接在 browser 對 server 發送 https://example.com/user/id 這個 request,會出現 404 Error

解決方式就是單純地將該網址用 index.html 服務

References

Vue router v4.x

Vue router v4.x API Reference

Vue.js 路由

Vue router 與前端路由管理

沒有留言:

張貼留言