# [ 想入門,我陪你 ] 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)