# [ 想入門,我陪你 ] Re Vue 重頭說起|Day 19:路由器進階功能應用 ###### tags: `Vue`、`Re:Vue 重頭說起`、`Alex 宅幹嘛` ## [Passing Props to Route Components](https://router.vuejs.org/guide/essentials/passing-props.html#boolean-mode) (13:00) ```htmlmixed= <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 :::info 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 ```htmlmixed= <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 拿不到 ![](https://i.imgur.com/EZ3rdQv.png) ### Object mode ```htmlmixed= <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) ```htmlmixed= <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 都可以使用 ![](https://i.imgur.com/ScPAKAc.png) ##[ HTML5 History Mode](https://router.vuejs.org/guide/essentials/history-mode.html#example-server-configurations) (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 ```javascript= 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](https://github.com/bripkens/connect-history-api-fallback). #### Firebase hosting Add this to your `firebase.json`: ```json= "hosting": { "public": "dist", "rewrites": [ { "source": "**", "destination": "/index.html" } ] } } ``` #### Caveat 因為任何頁面都會導來我們這邊,所以 server 不會處理,都是 200 OK,所以前端要處理 router 404 ```javascript= const router = new VueRouter({ mode: 'history', routes: [ { path: '*', component: NotFoundComponent } ] }) ``` 更多 Vue SSR 知識:https://ssr.vuejs.org/ ## [Navigation Guards](https://router.vuejs.org/guide/advanced/navigation-guards.html#global-before-guards) ### The Full Navigation Resolution Flow 1. Navigation triggered. 按下去切換時 1. Call **beforeRouteLeave** guards in deactivated components. 1. Call global **beforeEach** guards. 進每個路由前,有任何機會決定要不要進去,針對每一個路由設定不一樣的任物 3. Call **beforeRouteUpdate** guards in reused components. (page1?id=1 -> page1?id=2 page1元件沒有變,參數換,但是模組沒換,變化第二次時觸發**beforeRouteUpdate**) 4. Call **beforeEnter** in route configs. 元件第一次進去會觸發 **beforeEnter** 5. Resolve async route components. 6. Call **beforeRouteEnter** in activated components. (page1?id=1 -> page1?id=2 page1元件沒有變,參數換,但是模組沒換,變化第一次時觸發**beforeRouteEnter**) 7. Call global **beforeResolve** guards. 8. Navigation confirmed. 9. Call global **fterEach** hooks. 10. DOM updates triggered. 11. Call callbacks passed to next in **beforeRouteEnter** guards with instantiated instances. > watch query 或 hook 都可以實現功能 ### Global Before Guards - beforeEach ```htmlmixed= <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](https://i.imgur.com/TYkxOLg.png) ![from](https://i.imgur.com/nWrc4Dp.png) 1:09:27 Global Before Guards 文件參數講解 1:16:30 請問登入該坐在哪一個HOOK? A. beforeEach B.beforeEach C. beforeEnter Ans: 大型網站的話:beforeEach 搭配 判斷式,單頁網站就 beforeEnter 確保 next() 只被呼叫一次,要這樣寫: ```javascript= // 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) :::danger 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. ::: ```javascript= beforeRouteEnter (to, from, next) { // 在 next 內做 callback function,可以拿到 vm 參數去 access component next(vm => { }) } ``` beforeRouteUpdate 就可以拿到 this ```javascript= beforeRouteUpdate (to, from, next) { // just use `this` this.name = to.params.name next() } ``` 離開網頁時,被阻擋與詢問 ```javascript= 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](https://router.vuejs.org/guide/advanced/meta.html) (1:25:10) E可以在每一個路由加一些參數與資料,例如:在beforeEach做驗證,但頁面或路由器很多,所以我們就可以針對每一個路由去判斷是否要驗證,此時可以用 meta,Ex:requiresAuth ```javascript= const router = new VueRouter({ routes: [ { path: '/foo', component: Foo, children: [ { path: 'bar', component: Bar, // a meta field meta: { requiresAuth: true } } ] } ] }) ``` ```javascript= 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'` ```htmlmixed= <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 ```javascript= <!-- use a dynamic transition name --> <transition :name="transitionName"> <router-view></router-view> </transition> ``` ```javascript= 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](https://github.com/vuejs/vue-router/blob/dev/examples/transitions/app.js) ## 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() ```javascript= <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> ``` ```javascript= 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 ```javascript= 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) ```javascript= const router = new VueRouter({ routes: [...], // 切換路由捲軸的行為 scrollBehavior (to, from, savedPosition) { // return desired position } }) ``` 這段Code沒事初始化專案時可以先寫,因為當single page切頁時,當使用者捲到某個位置,但是你切了一頁後,捲軸不回彈到最上面,會停在同一個位置,所以加了 scrollBehavior ,捲軸就會置頂 ## Lazy Loading Routes(1:46:36) 搭配 webpack 做延遲載入 ```javascript= 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 分配路由的經驗 ## Navigation Failures (1:50:40) 如果用 `router-link` component 不會出錯,使用 `router.push` or `router.replace` 就會出錯(`Uncaught (in promise) Error`) ```htmlmixed= <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 的差別 ```htmlmixed= <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 的,所以不需要很複雜的判斷式 ```htmlembedded= <router-link to="/bbb" tag="span">Link Button</router-link> ``` > tag 預設是 <a> 有 href 屬性支援,若不是 <a> href 會自動被抽調,但是原本class樣式會保留 可以這樣設計動態切頁按鈕:router 是 /bbb 按鈕可以按,反之,按鈕無法按 (1:58:06) ```htmlembedded= <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) ```javascript= 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 ```javascript= const originalPush = VueRouter.prototype.push; VueRouter.prototype.push = function(location) { return originalPush.call(this, location).catch((err) => err)); } ``` ### Navigation Failures's properties (2:04:45) 錯誤可以透過 `to`與`form`參數觀察 ```htmlmixed= <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) #### NavigationFailureType: (2:12:12) ``` * 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,如下圖 ![](https://i.imgur.com/qQoRvD6.jpg)