vue路由的進階之拓展
#導航守衛《“導航”表示路由正在發生改變。》
正如其名,vue-router
提供的導航守衛主要用來通過跳轉或取消的方式守衛導航。有多種機會植入路由導航過程中:全域性的, 單個路由獨享的, 或者元件級的。記住引數或查詢的改變並不會觸發進入/離開的導航守衛。你可以通過觀察$route
物件來應對這些變化,或使用beforeRouteUpdate
的元件內守衛。
1:全域性前置守衛(router.beforeEach)
const router = new VueRouter({ ... })
router.beforeEach((to, from, next) => {
// ...
})
注:當一個導航觸發時,全域性前置守衛按照建立順序呼叫。守衛是非同步解析執行,此時導航在所有守衛 resolve 完之前一直處於等待中。
每個守衛方法接收三個引數:
1:to: Route
: 即將要進入的目標路由物件
2:from: Route
: 當前導航正要離開的路由
3:next: Function
: 一定要呼叫該方法來resolve這個鉤子。執行效果依賴next
方法的呼叫引數。
---next()
: 進行管道中的下一個鉤子。如果全部鉤子執行完了,則導航的狀態就是confirmed(確認的)。
---next(false)
: 中斷當前的導航。如果瀏覽器的 URL 改變了 (可能是使用者手動或者瀏覽器後退按鈕),那麼 URL 地址會重置到from
---next('/')
或者next({ path: '/' })
: 跳轉到一個不同的地址。當前的導航被中斷,然後進行一個新的導航。你可以向next
傳遞任意位置物件,且允許設定諸如replace: true
、name: 'home'
之類的選項以及任何用在router-link
的to
prop或router.push
中的選項。
---next(error)
: (2.4.0+) 如果傳入next
的引數是一個Error
例項,則導航會被終止且該錯誤會被傳遞給router.onError()
註冊過的回撥。
**確保next
函式在任何給定的導航守衛中都被嚴格呼叫一次。它可以出現多於一次,但是隻能在所有的邏輯路徑都不重疊的情況下,否則鉤子永遠都不會被解析或報錯。**這裡有一個在使用者未能驗證身份時重定向到/login
// BAD router.beforeEach((to, from, next) => { if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' }) // 如果使用者未能驗證身份,則 `next` 會被呼叫兩次 next() }) // GOOD router.beforeEach((to, from, next) => { if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' }) else next() })
2:全域性解析守衛
注:在 2.5.0+ 你可以用router.beforeResolve
註冊一個全域性守衛。這和router.beforeEach
類似,區別是在導航被確認之前,同時在所有元件內守衛和非同步路由元件被解析之後,解析守衛就被呼叫。
3:全域性後置鉤子
注:你也可以註冊全域性後置鉤子,然而和守衛不同的是,這些鉤子不會接受next
函式也不會改變導航本身:
router.afterEach((to, from) => { // ... })
4:路由獨享的守衛(beforeEnter)
const router = new VueRouter({ routes: [ { path: '/foo', component: Foo, beforeEnter: (to, from, next) => { // ... } } ] })
注:這些守衛與全域性前置守衛的方法引數是一樣的。
5:元件內的守衛
最後,你可以在路由元件內直接定義以下路由導航守衛:
1:beforeRouteEnter
2:beforeRouteUpdate(2.2 新增)
3:beforeRouteLeave
const Foo = { template: `...`, beforeRouteEnter (to, from, next) { // 在渲染該元件的對應路由被 confirm 前呼叫 // 不!能!獲取元件例項 `this` // 因為當守衛執行前,元件例項還沒被建立 }, beforeRouteUpdate (to, from, next) { // 在當前路由改變,但是該元件被複用時呼叫 // 舉例來說,對於一個帶有動態引數的路徑 /foo/:id,在 /foo/1 和 /foo/2 之間跳轉的時候, // 由於會渲染同樣的 Foo 元件,因此元件例項會被複用。而這個鉤子就會在這個情況下被呼叫。 // 可以訪問元件例項 `this` }, beforeRouteLeave (to, from, next) { // 導航離開該元件的對應路由時呼叫 // 可以訪問元件例項 `this` } }
beforeRouteEnter
守衛不能訪問this
,因為守衛在導航確認前被呼叫,因此即將登場的新元件還沒被建立。
不過,你可以通過傳一個回撥給next
來訪問元件例項。在導航被確認的時候執行回撥,並且把元件例項作為回撥方法的引數。
beforeRouteEnter (to, from, next) { next(vm => { // 通過 `vm` 訪問元件例項 }) }
注意beforeRouteEnter
是支援給next
傳遞迴調的唯一守衛。對於beforeRouteUpdate
和beforeRouteLeave
來說,this
已經可用了,所以不支援傳遞迴調,因為沒有必要了。
beforeRouteUpdate (to, from, next) { // just use `this` this.name = to.params.name next() }
這個離開守衛通常用來禁止使用者在還未儲存修改前突然離開。該導航可以通過next(false)
來取消。
beforeRouteLeave (to, from, next) { const answer = window.confirm('Do you really want to leave? you have unsaved changes!') if (answer) { next() } else { next(false) } }
*****完整的導航解析流程
1:導航被觸發。
2:在失活的元件裡呼叫beforeRouteLeave
守衛。
3:呼叫全域性的beforeEach
守衛。
4:在重用的元件裡呼叫beforeRouteUpdate
守衛 (2.2+)。
5:在路由配置裡呼叫beforeEnter
。
6:解析非同步路由元件。
7:在被啟用的元件裡呼叫beforeRouteEnter
。
8:呼叫全域性的beforeResolve
守衛 (2.5+)。
9:導航被確認。
10:呼叫全域性的afterEach
鉤子。
11:觸發 DOM 更新。
12:呼叫beforeRouteEnter
守衛中傳給next
的回撥函式,建立好的元件例項會作為回撥函式的引數傳入。
#路由元資訊
定義路由的時候可以配置meta
欄位:
const router = new VueRouter({ routes: [ { path: '/foo', component: Foo, children: [ { path: 'bar', component: Bar, // a meta field meta: { requiresAuth: true } } ] } ] })
那麼如何訪問這個meta
欄位呢?
首先,我們稱呼routes
配置中的每個路由物件為路由記錄。路由記錄可以是巢狀的,因此,當一個路由匹配成功後,他可能匹配多個路由記錄
例如,根據上面的路由配置,/foo/bar
這個 URL 將會匹配父路由記錄以及子路由記錄。
一個路由匹配到的所有路由記錄會暴露為$route
物件 (還有在導航守衛中的路由物件) 的$route.matched
陣列。因此,我們需要遍歷$route.matched
來檢查路由記錄中的meta
欄位。
下面例子展示在全域性導航守衛中檢查元欄位:
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() // 確保一定要呼叫 next() } })
#過渡動效(transition)
<transition> <router-view></router-view> </transition>
1:單個路由的過渡
上面的用法會給所有路由設定一樣的過渡效果,如果你想讓每個路由元件有各自的過渡效果,可以在各路由元件內使用<transition>
並設定不同的 name。
const Foo = { template: ` <transition name="slide"> <div class="foo">...</div> </transition> ` } const Bar = { template: ` <transition name="fade"> <div class="bar">...</div> </transition> ` }
2:基於路由的動態過渡
還可以基於當前路由與目標路由的變化關係,動態設定過渡效果:
<!-- 使用動態的 transition name --> <transition :name="transitionName"> <router-view></router-view> </transition> // 接著在父元件內 // watch $route 決定使用哪種過渡 watch: { '$route' (to, from) { const toDepth = to.path.split('/').length const fromDepth = from.path.split('/').length this.transitionName = toDepth < fromDepth ? 'slide-right' : 'slide-left' } }
#資料獲取
有時候,進入某個路由後,需要從伺服器獲取資料。例如,在渲染使用者資訊時,你需要從伺服器獲取使用者的資料。我們可以通過兩種方式來實現:
-
導航完成之後獲取:先完成導航,然後在接下來的元件生命週期鉤子中獲取資料。在資料獲取期間顯示“載入中”之類的指示。
-
導航完成之前獲取:導航完成前,在路由進入的守衛中獲取資料,在資料獲取成功後執行導航。
從技術角度講,兩種方式都不錯 —— 就看你想要的使用者體驗是哪種。
1:導航完成後獲取資料
當你使用這種方式時,我們會馬上導航和渲染元件,然後在元件的created
鉤子中獲取資料。這讓我們有機會在資料獲取期間展示一個 loading 狀態,還可以在不同檢視間展示不同的 loading 狀態。
假設我們有一個Post
元件,需要基於$route.params.id
獲取文章資料:
<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 () { // 元件建立完後獲取資料, // 此時 data 已經被 observed 了 this.fetchData() }, watch: { // 如果路由有變化,會再次執行該方法 '$route': 'fetchData' }, methods: { fetchData () { this.error = this.post = null this.loading = true // replace getPost with your data fetching util / API wrapper getPost(this.$route.params.id, (err, post) => { this.loading = false if (err) { this.error = err.toString() } else { this.post = post } }) } } }
2:在導航完成前獲取資料
通過這種方式,我們在導航轉入新的路由前獲取資料。我們可以在接下來的元件的beforeRouteEnter
守衛中獲取資料,當資料獲取成功後只調用next
方法。
export default { data () { return { post: null, error: null } }, beforeRouteEnter (to, from, next) { getPost(to.params.id, (err, post) => { next(vm => vm.setData(err, post)) }) }, // 路由改變前,元件就已經渲染完了 // 邏輯稍稍不同 beforeRouteUpdate (to, from, next) { this.post = null getPost(to.params.id, (err, post) => { this.setData(err, post) next() }) }, methods: { setData (err, post) { if (err) { this.error = err.toString() } else { this.post = post } } } }
在為後面的檢視獲取資料時,使用者會停留在當前的介面,因此建議在資料獲取期間,顯示一些進度條或者別的指示。如果資料獲取失敗,同樣有必要展示一些全域性的錯誤提醒。
#滾動行為
使用前端路由,當切換到新路由時,想要頁面滾到頂部,或者是保持原先的滾動位置,就像重新載入頁面那樣。vue-router
能做到,而且更好,它讓你可以自定義路由切換時頁面如何滾動。
注意: 這個功能只在支援history.pushState
的瀏覽器中可用。
當建立一個 Router 例項,你可以提供一個scrollBehavior
方法:
const router = new VueRouter({ routes: [...], scrollBehavior (to, from, savedPosition) { // return 期望滾動到哪個的位置 } })
scrollBehavior
方法接收to
和from
路由物件。第三個引數savedPosition
當且僅當popstate
導航 (通過瀏覽器的 前進/後退 按鈕觸發) 時才可用。
這個方法返回滾動位置的物件資訊,長這樣:
{ x: number, y: number }
{ selector: string, offset? : { x: number, y: number }}
(offset 只在 2.6.0+ 支援)
如果返回一個 falsy (注:falsy 不是false
,參考這裡)的值,或者是一個空物件,那麼不會發生滾動。
舉例:
scrollBehavior (to, from, savedPosition) { return { x: 0, y: 0 } }
對於所有路由導航,簡單地讓頁面滾動到頂部。
返回savedPosition
,在按下 後退/前進 按鈕時,就會像瀏覽器的原生表現那樣:
scrollBehavior (to, from, savedPosition) { if (savedPosition) { return savedPosition } else { return { x: 0, y: 0 } } }
如果你要模擬“滾動到錨點”的行為:
scrollBehavior (to, from, savedPosition) { if (to.hash) { return { selector: to.hash } } }
非同步滾動 2.8新增
你也可以返回一個 Promise 來得出預期的位置描述:
scrollBehavior (to, from, savedPosition) { return new Promise((resolve, reject) => { setTimeout(() => { resolve({ x: 0, y: 0 }) }, 500) }) }
將其掛載到從頁面級別的過渡元件的事件上,令其滾動行為和頁面過渡一起良好執行是可能的。但是考慮到用例的多樣性和複雜性,我們僅提供這個原始的介面,以支援不同使用者場景的具體實現。
#路由懶載入
當打包構建應用時,JavaScript 包會變得非常大,影響頁面載入。如果我們能把不同路由對應的元件分割成不同的程式碼塊,然後當路由被訪問的時候才載入對應元件,這樣就更加高效了。
結合 Vue 的非同步元件和 Webpack 的程式碼分割功能,輕鬆實現路由元件的懶載入。
首先,可以將非同步元件定義為返回一個 Promise 的工廠函式 (該函式返回的 Promise 應該 resolve 元件本身):
const Foo = () => Promise.resolve({ /* 元件定義物件 */ })
第二,在 Webpack 2 中,我們可以使用動態 import語法來定義程式碼分塊點 (split point):
import('./Foo.vue') // 返回 Promise
注意
如果您使用的是 Babel,你將需要新增syntax-dynamic-import
外掛,才能使 Babel 可以正確地解析語法。
結合這兩者,這就是如何定義一個能夠被 Webpack 自動程式碼分割的非同步元件。
const Foo = () => import('./Foo.vue')
在路由配置中什麼都不需要改變,只需要像往常一樣使用Foo
:
const router = new VueRouter({ routes: [ { path: '/foo', component: Foo } ] })
把元件按組分塊
有時候我們想把某個路由下的所有元件都打包在同個非同步塊 (chunk) 中。只需要使用命名 chunk,一個特殊的註釋語法來提供 chunk name (需要 Webpack > 2.4)。
const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue') const Bar = () => import(/* webpackChunkName: "group-foo" */ './Bar.vue') const Baz = () => import(/* webpackChunkName: "group-foo" */ './Baz.vue')
Webpack 會將任何一個非同步模組與相同的塊名稱組合到相同的非同步塊中。