1. 程式人生 > 實用技巧 >vue路由的進階之拓展

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: truename: 'home'之類的選項以及任何用在router-linktoproprouter.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傳遞迴調的唯一守衛。對於beforeRouteUpdatebeforeRouteLeave來說,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方法接收tofrom路由物件。第三個引數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 會將任何一個非同步模組與相同的塊名稱組合到相同的非同步塊中。