Try   HackMD

[ 想入門,我陪你 ] Re Vue 重頭說起|Day 19:路由器進階功能應用

tags: VueRe:Vue 重頭說起Alex 宅幹嘛

Passing Props to Route Components (13:00)

<body> <div id="app"> <p> <router-link></router-link> </p> </div> </body> <script> const Index = { template: ` <div>Index</div> <p>{{ $router.params.id }}</p> <p>{{ $router.query.test }}</p> ` } const router = new VueRouter({ routes: [ { path: '/:id', componens: { default: Index, }, name: 'index' } ], }) new Vue({ el: '#app', router, }) </script>

Vue 幫你自動把 $route 傳給 component,讓你方便取道 query 與 params

Using $route in your component creates a tight coupling with the route which limits the flexibility of the component as it can only be used on certain URLs.
如果你在component內用到 $route參數 代表 這個component只能for這個 $route的 url 做使用,有此參數就不用把 router的狀態根vuex做同步

$router 要把component與router做結耦

$router 是Component向上要資料
props 是 傳資料進 component

Boolean mode

<body> <div id="app"> <p> <router-link></router-link> </p> </div> </body> <script> const Index = { template: ` <div>Index</div> <p>{{ $router.params.id }}</p> <p>{{ $router.query.test }}</p> `, // 簡易模式 props: ['id', 'test'], } const router = new VueRouter({ routes: [ { path: '/:id', componens: { // props 傳 id 進去 default: Index, }, name: 'index' // 新增 Boolean mode => the route.params will be set as the component props. props: { default: true } } ], }) new Vue({ el: '#app', router, }) </script>

結果 query 拿得到,params 拿不到

Object mode

<body> <div id="app"> <p> <router-link></router-link> </p> </div> </body> <script> const Index = { template: ` <div>Index</div> <p>{{ $router.params.id }}</p> <p>{{ $router.query.test }}</p> `, // 簡易模式 props: ['id', 'test'], } const router = new VueRouter({ routes: [ { path: '/:id', componens: { default: Index, }, name: 'index' // 新增 Object mode => Useful for when the props are **static**. props: { default: { id: 'aaa', test: '123', } } } ], }) new Vue({ el: '#app', router, }) </script>

用 props 好處,只告我自己的資料與別人給的props,這樣可以降低結耦合(Ex: 每一個模組都讀 router 與 vuex 會提高結耦合)

Function mode (26:15)

<body> <div id="app"> <p> <router-link></router-link> <!--Index 模組是耦合的,也可以給自己塞參數用 --> <index uid="Alex" :test="666"></index> </p> </div> </body> <script> const Index = { template: ` <div>Index</div> <p>{{ $route.params.uid }}</p> <p>{{ $route.query.test }}</p> // 可以用 this 簡寫 <p>{{ this.uid }}</p> <p>{{ this.test }}</p> `, // 簡易模式 props: ['id', 'test'], } const router = new VueRouter({ routes: [ { path: '/:uid', componens: { // Index 模組是耦合的,可以給 router 獨立使用 default: Index, }, name: 'index' // 新增 Function mode => This allows you to cast parameters into other types, combine static values with route-based values // 當 改 url 的 params與query時,模組的props也會自動跟著修改 props: { default: (route) => ({ uid: route.params.uid, test: route.query.test, } }) } ], }) new Vue({ el: '#app', router, // 註冊 Index 模組 components: { Index } }) </script>

此法較佳,index模組不限定只能在router或是哪個網址才能使用,只要有傳 id 死 test 都可以使用

## HTML5 History Mode (33:45)

History mode > Hash mode

  1. 網址好看
  2. 矛點功能的 # 與 Hash mode 的 # 有衝突
  3. vue cli 可以用 History mode 因為 server 是 vue cli 幫你起的,所以他會幫你把每個位置都指向 index.html,Life server server端沒當你做,所以無法使用
  4. 圖文描述(38:30)

41:10 預設是 hash mode,要用 history mode要對 server做處理

如果要使用 history mode,要請後端把所有頁面的route交由前端處理,並都指向同一個檔案 index.html page,

處理server的方式如下:

Apache

<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteBase /
  RewriteRule ^index\.html$ - [L]
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule . /index.html [L]
</IfModule>

nginx

location / {
  try_files $uri $uri/ /index.html;
}

#Native Node.js

const http = require('http') const fs = require('fs') const httpPort = 80 http.createServer((req, res) => { fs.readFile('index.html', 'utf-8', (err, content) => { if (err) { console.log('We cannot open "index.html" file.') } res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }) res.end(content) }) }).listen(httpPort, () => { console.log('Server listening on: http://localhost:%s', httpPort) })

Express with Node.js

For Node.js/Express, consider using connect-history-api-fallback middleware.

Firebase hosting

Add this to your firebase.json:

"hosting": { "public": "dist", "rewrites": [ { "source": "**", "destination": "/index.html" } ] } }

Caveat

因為任何頁面都會導來我們這邊,所以 server 不會處理,都是 200 OK,所以前端要處理 router 404

const router = new VueRouter({ mode: 'history', routes: [ { path: '*', component: NotFoundComponent } ] })

更多 Vue SSR 知識:https://ssr.vuejs.org/

The Full Navigation Resolution Flow

  1. Navigation triggered. 按下去切換時
  2. Call beforeRouteLeave guards in deactivated components.
  3. Call global beforeEach guards. 進每個路由前,有任何機會決定要不要進去,針對每一個路由設定不一樣的任物
  4. Call beforeRouteUpdate guards in reused components. (page1?id=1 -> page1?id=2 page1元件沒有變,參數換,但是模組沒換,變化第二次時觸發beforeRouteUpdate)
  5. Call beforeEnter in route configs. 元件第一次進去會觸發 beforeEnter
  6. Resolve async route components.
  7. Call beforeRouteEnter in activated components. (page1?id=1 -> page1?id=2 page1元件沒有變,參數換,但是模組沒換,變化第一次時觸發beforeRouteEnter)
  8. Call global beforeResolve guards.
  9. Navigation confirmed.
  10. Call global fterEach hooks.
  11. DOM updates triggered.
  12. Call callbacks passed to next in beforeRouteEnter guards with instantiated instances.

watch query 或 hook 都可以實現功能

Global Before Guards - beforeEach

<body> <div id="app"> <p> <router-link></router-link> <index uid="Alex" :test="666"></index> </p> </div> </body> <script> const Index = { template: ` <div>Index</div> <p>{{ this.uid }}</p> <p>{{ this.test }}</p> `, props: ['id', 'test'], //模組系列 beforeRouteEnter(to, from, next){ console.log(to, from, next); next(); }, beforeRouteUpdate(to, from, next){ console.log(to, from, next); next(); }, beforeRouteLeave(to, from, next){ console.log(to, from, next); next(); }, } const router = new VueRouter({ routes: [ { path: '/:uid', componens: { default: Index, }, name: 'index' props: { default: (route) => ({ uid: route.params.uid, test: route.query.test, }) }, // 確定進到 /:uid 前 要做什麼事 router.beforeEnter((to, from, next) => { console.log(to, from, next); next(); }) } ], }) // 進到每一個路由前要做什麼事 router.beforeEach((to, from, next) => { console.log(to, from, next); // next類似管線,這關過了,交給下一關處理,有暫停處理的效過Ex: 非同步的檢查、需要花時間的處理、轉導頁,如果不加上,頁面不會往下走 next(); }), router.afterEach((to, from) => { console.log(to, from); }) new Vue({ el: '#app', router, components: { Index } }) </script>

match 判斷是哪些與路徑有符合搭配到,目前只偵測到一個(1:05:00 不是很懂?)

to
from

1:09:27 Global Before Guards 文件參數講解

1:16:30 請問登入該坐在哪一個HOOK? A. beforeEach B.beforeEach C. beforeEnter Ans: 大型網站的話:beforeEach 搭配 判斷式,單頁網站就 beforeEnter

確保 next() 只被呼叫一次,要這樣寫:

// BAD router.beforeEach((to, from, next) => { if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' }) // if the user is not authenticated, `next` is called twice next() }) // GOOD router.beforeEach((to, from, next) => { if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' }) else next() })

Global Resolve Guards (1:19:00)

Global After Hooks(1:21:00)

Per-Route Guard(1:21:27)

In-Component Guards(1:21:35)

The beforeRouteEnter guard does NOT have access to this, because the guard is called before the navigation is confirmed, thus the new entering component has not even been created yet.

beforeRouteEnter (to, from, next) { // 在 next 內做 callback function,可以拿到 vm 參數去 access component next(vm => { }) }

beforeRouteUpdate 就可以拿到 this

beforeRouteUpdate (to, from, next) { // just use `this` this.name = to.params.name next() }

離開網頁時,被阻擋與詢問

beforeRouteLeave (to, from, next) { const answer = window.confirm('Do you really want to leave? you have unsaved changes!') if (answer) { next() } else { next(false) } }

Route Meta Fields (1:25:10)

E可以在每一個路由加一些參數與資料,例如:在beforeEach做驗證,但頁面或路由器很多,所以我們就可以針對每一個路由去判斷是否要驗證,此時可以用 meta,Ex:requiresAuth

const router = new VueRouter({ routes: [ { path: '/foo', component: Foo, children: [ { path: 'bar', component: Bar, // a meta field meta: { requiresAuth: true } } ] } ] })
router.beforeEach((to, from, next) => { if (to.matched.some(record => record.meta.requiresAuth)) { // this route requires auth, check if logged in // if not, redirect to login page. if (!auth.loggedIn()) { next({ path: '/login', query: { redirect: to.fullPath } }) } else { next() } } else { next() // make sure to always call next()! } })

動態路由 & 另一種權限管控?!

Transitions(1:29:44)

Transitions 動態效果有時候有,有時候沒有的主要原因是 Vue 沒有辦法依照現有狀態判斷你是否有切換,是以解決辦法是要加Key,所以可以用 $route.fullPath 當 key ,Ex: fullPath: /bbb?test=456,假設在同一個company或路由當資料改變時,但不想被做動畫可以用 $route.name$route.path Ex: name: index or path: '/bbb'

<body> <div id="app"> <p> <transition name="index"> <router-view></router-view> </transition> <index uid="Alex" :test="666"></index> </p> </div> </body> <script> const Index = { template: ` <div>Index</div> <p>{{ this.uid }}</p> <p>{{ this.test }}</p> `, props: ['id', 'test'], //模組系列 beforeRouteEnter(to, from, next){ console.log(to, from, next); next(); }, beforeRouteUpdate(to, from, next){ console.log(to, from, next); next(); }, beforeRouteLeave(to, from, next){ console.log(to, from, next); next(); }, } const router = new VueRouter({ routes: [ { path: '/:uid', componens: { default: Index, }, name: 'index' props: { default: (route) => ({ uid: route.params.uid, test: route.query.test, }) }, // 確定進到 /:uid 前 要做什麼事 router.beforeEnter((to, from, next) => { console.log(to, from, next); next(); }) } ], }) // 進到每一個路由前要做什麼事 router.beforeEach((to, from, next) => { console.log(to, from, next); // next類似管線,這關過了,交給下一關處理,有暫停處理的效過Ex: 非同步的檢查、需要花時間的處理、轉導頁,如果不加上,頁面不會往下走 next(); }), router.afterEach((to, from) => { console.log(to, from); }) new Vue({ el: '#app', router, components: { Index } }) </script>

Per-Route Transition (1:32:49)

Route-Based Dynamic Transition (1:33:00)

把 transition 用動態綁定,然後去 watch 這個 route,也可以用 hook 去控制 route 動畫效果 Ex: beforeRouteEnter

<!-- use a dynamic transition name --> <transition :name="transitionName"> <router-view></router-view> </transition>
watch: { '$route' (to, from) { const toDepth = to.path.split('/').length const fromDepth = from.path.split('/').length this.transitionName = toDepth < fromDepth ? 'slide-right' : 'slide-left' } }

Demo

Data Fetching (1:33:35)

討論:在進到頁面前Fetch資料,還是在進到Component在Fetch資料?

Fetching After Navigation: perform the navigation first, and fetch data in the incoming component's lifecycle hook. Display a loading state while data is being fetched.先導頁,接下來再Component內透過生命週期拿資料,適合頁面專用資料

Fetching Before Navigation: Fetch data before navigation in the route enter guard, and perform the navigation after data has been fetched.切換路由之前,hook先做資料處理,適合做驗證打API拿資料,之後再換頁

Fetching After Navigation

在元件的 created 做 fetchData()

<template> <div class="post"> <div v-if="loading" class="loading"> Loading... </div> <div v-if="error" class="error"> {{ error }} </div> <div v-if="post" class="content"> <h2>{{ post.title }}</h2> <p>{{ post.body }}</p> </div> </div> </template>
export default { data () { return { loading: false, post: null, error: null } }, created () { // fetch the data when the view is created and the data is // already being observed 一開始做第一次 fetchData this.fetchData() }, watch: { // route 改變後再做一次 fetchData // call again the method if the route changes '$route': 'fetchData' }, methods: { fetchData () { this.error = this.post = null this.loading = true const fetchedId = this.$route.params.id // replace `getPost` with your data fetching util / API wrapper getPost(fetchedId, (err, post) => { // make sure this request is the last one we did, discard otherwise if (this.$route.params.id !== fetchedId) return this.loading = false if (err) { this.error = err.toString() } else { this.post = post } }) } } }

Fetching Before Navigation

beforeRouteUpdate、beforeRouteEnter

export default { data () { return { post: null, error: null } }, beforeRouteEnter (to, from, next) { // 非同步 getPost(to.params.id, (err, post) => { // beforeRouteEnter 沒 this 所以用 vm 呼叫 setData 把資料送進去 next(vm => vm.setData(err, post)) }) }, // when route changes and this component is already rendered, // the logic will be slightly different. beforeRouteUpdate (to, from, next) { // beforeRouteUpdate表示我已經在原本模組裡,但是要重讀的情況下要清除舊的 this.post = null getPost(to.params.id, (err, post) => { // 此 hook 有提供 this this.setData(err, post) next() }) }, methods: { setData (err, post) { if (err) { this.error = err.toString() } else { this.post = post } } } }

QA: 1:41:28 在 route 去 access store的資料

The user will stay on the previous view while the resource is being fetched for the incoming view. It is therefore recommended to display a progress bar or some kind of indicator while the data is being fetched. If the data fetch fails, it's also necessary to display some kind of global warning message.

Scroll Behavior (1:45:24)

const router = new VueRouter({ routes: [...], // 切換路由捲軸的行為 scrollBehavior (to, from, savedPosition) { // return desired position } })

這段Code沒事初始化專案時可以先寫,因為當single page切頁時,當使用者捲到某個位置,但是你切了一頁後,捲軸不回彈到最上面,會停在同一個位置,所以加了 scrollBehavior ,捲軸就會置頂

Lazy Loading Routes(1:46:36)

搭配 webpack 做延遲載入

const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue') const Bar = () => import(/* webpackChunkName: "group-foo" */ './Bar.vue') const Baz = () => import(/* webpackChunkName: "group-foo" */ './Baz.vue')

webpackChunkName 不要亂命名,webpackChunkName名字要一樣 1:47:48

1:50:05 分配路由的經驗

如果用 router-link component 不會出錯,使用 router.push or router.replace 就會出錯(Uncaught (in promise) Error)

<body> <div id="app"> <p> <router-link></router-link> </p> </div> </body> <script> const Index = { template: ` <div>Index</div> <p>{{ $router.params.id }}</p> <p>{{ $router.query.test }}</p> ` } const router = new VueRouter({ routes: [ { path: '/:id', componens: { default: Index, }, name: 'index' } ], }) new Vue({ el: '#app', router, }) </script>

來看看 router-link 與 button 的差別

<body> <div id="app"> <!-- 差別: 導頁的時候不使用 router-link,反而用程式去做要注意必須自己去控管此行為(已經在/bbb,你卻點擊 bbb Common Button) --> <!-- router-link的優勢是他會自動般你判斷同頁面的錯誤,除此之外他會搭配 class的變化樣式 --> <router-link to="/bbb">Link Button</router-link> <button @click="router.push('bbb')">Common Button</button> </div> </body> <script> const Index = { template: ` <div>Index</div> <p>{{ $router.params.id }}</p> <p>{{ $router.query.test }}</p> `, // 簡易模式 props: ['id', 'test'], } const router = new VueRouter({ routes: [ { path: '/:id', componens: { // props 傳 id 進去 default: Index, }, name: 'index' // 新增 Boolean mode => the route.params will be set as the component props. props: { default: true } } ], }) new Vue({ el: '#app', router, }) </script>

router-link 是可以指定 tag 的,所以不需要很複雜的判斷式

<router-link to="/bbb" tag="span">Link Button</router-link>

tag 預設是 有 href 屬性支援,若不是 href 會自動被抽調,但是原本class樣式會保留

可以這樣設計動態切頁按鈕:router 是 /bbb 按鈕可以按,反之,按鈕無法按 (1:58:06)

<router-link to="/bbb" tag="$route.params.uid !== 'bbb' ? 'a' : 'span'">Link Button</router-link>

如果用 <button> 做此功能判斷式會比較麻煩

Detecting Navigation Failures (1:59:00)

很少人用此功能,此功能是你要如何去接這個錯誤,或對錯誤行為做處理(Ex:把按鈕關掉/做錯誤擷取),但push新的頁面時,可以把錯誤catch起來

Navigation Failures are Error instances with a few extra properties. To check if an error comes from the Router, use the isNavigationFailure function:(2:13:18)

import VueRouter from 'vue-router' const { isNavigationFailure, NavigationFailureType } = VueRouter // trying to access the admin page router.push('/admin').catch(failure => { // 換頁出錯時,確定是因為 Navigation 相關的錯誤就會進到 catch if (isNavigationFailure(failure, NavigationFailureType.redirected)) { // show a small notification to the user showToast('Login in order to access the admin panel') } })

有一個網友提出他們家的做法:把原本的push做調整,加上了 catch 共用化,任何錯誤都進入 catch

const originalPush = VueRouter.prototype.push; VueRouter.prototype.push = function(location) { return originalPush.call(this, location).catch((err) => err)); }

錯誤可以透過 toform參數觀察

<body> <div id="app"> <button @click="change"></button> </div> </body> <script> const Index = { template: ` <div>Index</div> <p>{{ $router.params.id }}</p> <p>{{ $router.query.test }}</p> `, } const router = new VueRouter({ routes: [ { path: '/:id', componens: { default: Index, }, name: 'index' } ], }) new Vue({ el: '#app', router, methods: { change() { this.$router.push('/bbb').catch(err => { // isNavigationFailure有兩個參數,第一個error是把錯誤丟進去給他確認 console.log(VueRouter.isNavigationFailure(failure, checkType)) // true,代表確實有錯,但是console不會噴紅字,而是觸發你的 callback // 如果它不是我們在轉導頁時因為Vue造成的錯誤,我們就再繼續丟出來,讓紅字出現,這樣比較好 DEBUG if (VueRouter.isNavigationFailure(err)) { throw err; } }) }, // 可以針對錯誤類別做不同的 Error handling checkType() { } }, }) </script>

TIP: 如果你省略 isNavigationFailure 第一個的參數 failure,他只會幫你確定這是不是Navigation的錯誤,不會幫你分類類別(NavigationFailureType)

* redirected: next(newLocation) was called inside of a navigation guard to redirect somewhere else. next
* aborted: next(false) was called inside of a navigation guard to the navigation.
* cancelled: A new navigation completely took place before the current navigation could finish. e.g. router.push was called while waiting inside of a navigation guard.
* duplicated: The navigation was prevented because we are already at the target location

Ex: 剛剛例子在 /bbb 下,點擊 button 進入 /bbb 就會噴 duplicated type 的路由 error,如下圖