vue-router原理到最佳實踐

本文是vue-router系列。這裏從瀏覽器到vue-router原理到最佳實踐都會有詳細的講解。因爲篇幅較長,建議能夠選擇感興趣的目錄看。javascript

先了解了瀏覽器的history原理,才能更好的結合vue-router源碼一步步瞭解它的實現。若是這塊已經有了解能夠直接跳過。css

  • pushState/replaceState/popstate 解析
  • vue-router 實現原理
  • route 跟 router 的區別
  • 經過路由元信息設置登陸
  • 設置滾動行爲
  • vue 路由 按需 keep-alive
  • watch 監聽路由變化
  • 如何檢測物理鍵返回
  • 如何作出翻書效果
  • 如何作一個優雅的路由分區

pushState/replaceState/popstate 解析

HTML5提供了對history棧中內容的操做。經過history.pushState/replaceState實現添加地址到history棧中。html

pushState/replaceState() 方法 pushState() 須要三個參數:vue

  • 狀態對象
  • 標題 (目前被忽略)
  • (可選的) 一個URL.

讓咱們來解釋下這三個參數詳細內容:java

  • 狀態對象 — 狀態對象state是一個JavaScript對象,經過pushState () 建立新的歷史記錄條目。不管何時用戶導航到新的狀態,popstate事件就會被觸發,且該事件的state屬性包含該歷史記錄條目狀態對象的副本。node

    狀態對象能夠是能被序列化的任何東西。緣由在於Firefox將狀態對象保存在用戶的磁盤上,以便在用戶重啓瀏覽器時使用,咱們規定了狀態對象在序列化表示後有640k的大小限制。若是你給 pushState() 方法傳了一個序列化後大於640k的狀態對象,該方法會拋出異常。若是你須要更大的空間,建議使用 sessionStorage 以及 localStorage.webpack

  • 標題 — Firefox 目前忽略這個參數,但將來可能會用到。在此處傳一個空字符串應該能夠安全的防範將來這個方法的更改。或者,你能夠爲跳轉的state傳遞一個短標題。web

  • URL — 該參數定義了新的歷史URL記錄。注意,調用 pushState() 後瀏覽器並不會當即加載這個URL,但可能會在稍後某些狀況下加載這個URL,好比在用戶從新打開瀏覽器時。新URL沒必要須爲絕對路徑。若是新URL是相對路徑,那麼它將被做爲相對於當前URL處理。新URL必須與當前URL同源,不然 pushState() 會拋出一個異常。該參數是可選的,缺省爲當前URL。vue-router

改變歷史記錄條目:數組

@clickA
history.pushState({ page: 1 }, "", "a.html");

@clickB
history.pushState({ page: 2 }, "", "b.html");
複製代碼

當歷史記錄條目更改時,將觸發popstate事件。 若是被激活的歷史記錄條目是經過對history.pushState()的調用建立的,或者受到對history.replaceState()的調用的影響,popstate事件的state屬性包含歷史條目的狀態對象的副本。

須要注意的是調用history.pushState()或history.replaceState()不會觸發popstate事件。 只有在作出瀏覽器動做時,纔會觸發該事件,如用戶點擊瀏覽器的回退按鈕(或者在Javascript代碼中調用history.back())

觸發瀏覽器回退按鈕:

window.addEventListener('popstate', ()=>{
  console.log(location.href)
})
複製代碼

vue-router 實現原理

整體來講就是使用了history的方法來控制瀏覽器的路由,結合vue實現數據與視圖更新。 上面咱們已經講了history的使用原理,接下來結合vue-router具體來看一下

安裝 vue-router

install.js

經過 Object.defineProperty 將 _router 掛載在 Vue 原型的 router 屬性的 get 函數上。
這樣能夠經過 this.router 來調用 _router。使用get的好處是,保證了安全性,只能讀不能修改 $router。

// 項目內能夠經過 this.$router 來獲取到
Object.defineProperty(Vue.prototype, '$router', {
  get () { return this._routerRoot._router }
})
複製代碼

而後,在 Vue.mixin 中注入 beforeCreate 鉤子函數,每一個組件都會調用 registerInstance , 經過 Vue.util.defineReactive 將 _route 進行監聽,這樣每次進入到新的頁面就會設置當前的路由。

// 在 `beforeCreate` 中調用了 `registerInstance`
// 其實就是調用了 router-view 組件中的 registerRouteInstance 方法
const registerInstance = (vm, callVal) => {
  let i = vm.$options._parentVnode
  if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
    i(vm, callVal)
  }
}
Vue.mixin({
  beforeCreate () {
    if (isDef(this.$options.router)) {
        this._routerRoot = this
        this._router = this.$options.router
        // 初始化設置監聽 popstate
        // 並將 this._route = route
        this._router.init(this)
        // 亮點在這!!!
        // 將 _route 添加監聽,當修改 history.current 時就能夠觸發更新了
        Vue.util.defineReactive(this, '_route', this._router.history.current)
    } else {
      this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
    }
    // 註冊實例,調用 router-view 中的方法,修改 route 值,從而更新視圖
    registerInstance(this, this)
  },
  destroyed () {
    // 銷燬註冊實例,由於註冊的實例是 undefined
    registerInstance(this)
  }
})
複製代碼

router-view 實現視圖更新

router-view 是一個函數式組件,頁面中 beforeCreate 鉤子調用registerRouteInstance 來修改當前 route 實例,因爲 _route 已經被監聽了,因此當 matched.instances[name] 發生變化的時候,會從新觸發 render 更新視圖。

components/view.js

data.registerRouteInstance = (vm, val) => {
  const current = matched.instances[name]
  // 註冊路由實例,若是與當前路由與原來路由相等則不變,若是不相等則更新實例
  if (
    (val && current !== vm) ||
    (!val && current === vm)
  ) {
    // 修改當前路由實例
    matched.instances[name] = val
  }
}
複製代碼

建立 route 對象

建立路由 createRoute,經過解析location等操做,返回一個route對象

src/util/route.js

export function createRoute ( record: ?RouteRecord, location: Location, redirectedFrom?: ?Location, router?: VueRouter ): Route {
  const stringifyQuery = router && router.options.stringifyQuery

  let query: any = location.query || {}
  try {
    query = clone(query)
  } catch (e) {}

  const route: Route = {
    name: location.name || (record && record.name),
    meta: (record && record.meta) || {},
    path: location.path || '/',
    hash: location.hash || '',
    query,
    params: location.params || {},
    fullPath: getFullPath(location, stringifyQuery),
    matched: record ? formatMatch(record) : []
  }
  if (redirectedFrom) {
    route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery)
  }
  return Object.freeze(route)
}
複製代碼

這裏主要講了,vue-router 的 install,router-view 實現視圖渲染,create-route 建立路由實例,還有如何實現與vue的結合,實現數據綁定等。因爲篇幅的問題,再多細節的東西就沒有講了,有興趣你們能夠翻翻源碼。

route 跟 router 的區別

講完原理給你們捋一下 route 跟 router 的區別,經過源碼很容易看出他們的不一樣

  • $router 是VueRouter的一個對象,經過Vue.use(VueRouter)和VueRouter構造函數獲得一個router的實例對象,這個對象中是一個全局的對象,他包含了全部的路由包含了許多關鍵的對象和屬性。
  • $route 就是一個路由的對象,咱們經過 createRoute 建立出來的 route 對象,裏面包括:
    • path字符串,等於當前路由對象的路徑,會被解析爲絕對路徑,如 "/home/news" 。
    • params對象,包含路由中的動態片斷和全匹配片斷的鍵值對
    • query對象,包含路由中查詢參數的鍵值對。例如,對於 /home/news/detail/01?favorite=yes ,會獲得$route.query.favorite == 'yes' 。
    • router路由規則所屬的路由器(以及其所屬的組件)。
    • matched數組,包含當前匹配的路徑中所包含的全部片斷所對應的配置參數對象。
    • name當前路徑的名字,若是沒有使用具名路徑,則名字爲空。

經過路由元信息,設置登陸

原理是在路由的 meta 裏設置 auth 屬性,進入路由以前判斷 meta.auth 是否爲 true ,若是爲 true 再判斷,是否已經登錄,沒有登錄的話調 login 方法去登錄,登錄成功後 回調 code === 0 繼續進入頁面

const beforeEnter = (to, from, next) => {
  if (to.meta && to.meta.auth) {
    // 未登錄走登錄邏輯
    if (!isLogin()) {
      const nextPage = (res) => {
        if (res.code === 0) {
          next(true)
        } else {
          next(false)
        }
      };
      let targetUrl = location.href.split('#')[0] + '#' + to.fullPath
      // 這裏是你的登錄邏輯
      login({
        // 回調後進入頁面
        callback: nextPage,
        // 目標頁面,登錄成功後進入目標頁面
        targetUrl: targetUrl
      });
    } else {
      next(true)
    }
  } else {
    next(true)
  }
}
複製代碼

在 Foo 組件設置登錄

const routes = [
  {
    path: '/Foo',
    name: 'Foo',
    meta: {
      auth: true,
    },
    component: () => ('Foo.vue'),
  },
  {
    path: '/Bar',
    name: 'Bar',
    component: () => ('Bar.vue'),
  },
]
複製代碼

設置滾動行爲

設置滾動行爲,並添加路由,若是有 savedPosition 說明是第二次進入並已經觸發過滾動,因此會滾動到以前打開的位置,若是是第一次進入沒有savedPosition則滾動到最頂層。

const router = new Router({
  scrollBehavior(to, from, savedPosition) {
    if (savedPosition) {
      return savedPosition
    } else {
      return { x: 0, y: 0 }
    }
  },
  routes
})
複製代碼

vue 路由 按需 keep-alive

<keep-alive> 包裹動態組件時,會緩存不活動的組件實例,而不是銷燬它們。和 <transition> 類似,<keep-alive> 是一個抽象組件:它自身不會渲染一個 DOM 元素,也不會出如今父組件鏈中。

當組件在<keep-alive> 內被切換,它的 activated 和 deactivated 這兩個生命週期鉤子函數將會被對應執行。

<!-- 須要緩存的視圖組件 -->
<router-view v-if="$route.meta.keepAlive">
  </router-view>
</keep-alive>

<!-- 不須要緩存的視圖組件 -->
<router-view v-if="!$route.meta.keepAlive">
</router-view>
複製代碼

路由配置

const routers = [
  {
    path: '/list',
    name: 'list',
    component: () => import('./views/keep-alive/list.vue'),
    meta: {
      keepAlive: true
    }
  }
]
複製代碼

由於在咱們項目裏面常常會有列表跳詳情,而後又詳情返回列表的狀況,因此咱們能夠根據項目需求來判斷是否須要被緩存,若是被緩存了就會出現下面的狀況須要注意

watch 監聽路由變化

有時咱們須要經過給頁面傳參來判斷頁面展現什麼內容,好比詳情頁 #/detail?infoId=123456,咱們須要根據 infoId 來展現不一樣的內容

咱們通常習慣會這樣寫

async created() {
  const res = await this.pullData()
}

async pullData () {
  return this.$http.get('http://test.m.com/detail', { infoId })
}
複製代碼

當咱們經過列表再次進入詳情頁時,雖然infoId已經變了infoId=234567,可是頁面並無改變,是由於該頁面被keep-alive了,created不會再次觸發,created只在建立的時候執行一次。

爲了解決這個問題,咱們就須要對 $route 進行監聽,只要 route 發生變化咱們就更新頁面

watch: {
  '$route': {
    // 頁面初始化時當即觸發一次
    immediate: true,
    handler(to, from) {
      // 只有進入當前頁面的時候,拉取數據
      if(to.path === '/detail') {
        this.pullData();
      }
    }
  }
}
複製代碼

這樣還會帶來下面的問題,就是物理鍵返回的時候也會刷新頁面,下面是對物理鍵返回的處理

如何檢測物理鍵返回

爲何要檢測物理返回鍵?好比你有這樣列表頁,點擊進去是一個詳情頁,而後返回的時候列表刷新了,找不到原來的位置,這種時候對用戶的體驗很是很差。咱們看一下例子。

那麼咱們如何去優化它? 思路就是在用戶返回到列表頁的時候不刷新數據,只有在用戶主動進入列表的時候纔會刷新數據,咱們看一下效果

下面是實現的代碼,原理就是監聽 popstate,當瀏覽器返回的時候會觸發 popstate,這時咱們標記 isBack 爲 ture。在 setTimeout 0 以後判斷 isBack(是否爲瀏覽器返回),若是不是瀏覽器返回的再刷新數據。

@Component
export default {
  data() {
    return {
      // 用來判斷是不是經過返回鍵返回的
      isBack: false
    }
  },
  created () {
    // 若是是物理鍵返回的就設置 isBack = true
    this.$_onBack(()=>{
      this.isBack = true;
    });
  },
  watch: {
    '$route': {
      immediate: true,
      handler(to, from) {
        // 每次進入路由重置 isBack = false
        this.isBack = false;
        if(to.path === '/list') {
          // 等待路由的 popstate 監聽結束
          setTimeout(()=>{
            !this.isBack && this.pullData();
          })
        }
      }
    }
  }
}
複製代碼

_onBack 實現,就是監聽了 popstate ,由於vue-router是操做了history的狀態,而瀏覽器返回的時候就會觸發 popstate ,利用這個特性來判斷是否爲瀏覽器返回鍵返回

_onBack(cb) {
  window.addEventListener(
    "popstate",
    (e) => {
      if(typeof cb  === 'function') {
        if(e.state) {
          cb(true)
        }
      }
    },
    false
  );
};
複製代碼

如何作出翻書效果

利用的是 vue 的 transition 組件,結合 vue-router,在路由上作一些過渡效果。先看圖說話

<template>
  <div class="wrap">
    <transition :name="transitionName">
      <router-view class="child-view"></router-view>
    </transition>
  </div>
</template>
<script> export default { data() { return { transitionName: 'turning-down' } }, watch: { '$route' (to, from) { if(to.path > from.path) { // 進入下一頁 this.transitionName = 'turning-up'; }else{ // 返回上一頁 this.transitionName = 'turning-down'; } } } } </script>

<style scoped lang="scss"> .child-view { position: absolute; left: 0; top: 0; width: 100%; height: 100%; transition: all 4s ease; transform-origin: 0% center; } .turning-down-enter{ opacity: 1; transform-origin: left; transform-style: preserve-3d; -webkit-transform: perspective(1000px) rotateY(-180deg); transform: perspective(1000px) rotateY(-180deg); } .turning-up-leave-active { transform-style: preserve-3d; transform: perspective(1000px) rotateY(-180deg); z-index: 100; } </style>
複製代碼

配置路由

export default [
  {
    path: '/Home',
    name: 'home',
    component: () =>
      import('../views/vue/vue-router/Home.vue'),
    children: [
      {
        path: '/Home/First',
        name: 'Home-First',
        component: () =>
          import('../views/vue/vue-router/First.vue'),
      },
      {
        path: '/Home/Second',
        name: 'Home-Second',
        component: () =>
          import('../views/vue/vue-router/Second.vue'),
      }
    ]
  }
]
複製代碼

經過監聽 Home 頁面的路由變化,來改變 transitionName,路由切換時切換 transition 組件的 enter/leave-active 樣式,所以能夠在路由切換時作到翻書效果。

如何作一個優雅的路由分區

隨着項目的增大,項目中的頁面可能達到好幾十個,甚至更多,那麼如何將這些頁面進行管理呢?咱們的作法就是,將路由按照功能進行區分。

好比咱們分了5個區間,每一個區間有個數不一樣的路由

-- a.js
-- b.js
-- c.js
-- d.js
-- e.js
複製代碼

咱們須要將這五個路由分別引進來,並進行結合

import a from 'routers/a'
import b from 'routers/b'
import c from 'routers/c'
import d from 'routers/d'
import e from 'routers/e'

const routers = [].concat(a, b, c, d, e)
複製代碼

之後咱們每次建一個新的分區,都要手動加上相應的邏輯,這樣看起來很不方便,那麼咱們有沒有好的解決辦法呢?

下面是我作的路由分區,利用 webpack 的 require.context 方法,將全部須要的路徑導出來,require.context 有三個參數:

  • 第一個參數,匹配的路徑目錄,(從當前目錄開始算起)
  • 第二個參數,是否須要深層遍歷
  • 第三個參數,正則匹配,匹配出你須要的路徑
  • 須要注意的點,require 不能直接導出變量名

例如,下面的例子會報錯

const a = './route/a.js'
// 會報錯,a 不是一個模塊
require(a)
複製代碼

因此 require 中只能加字符串或者使用字符串拼接

const a = 'a.js'
require('./route/' + a)
複製代碼

這樣webpack會把 ./route/ 下全部文件打包成模塊,你纔可使用 require 去引用

下面是一個完整的例子

import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

const routes = []
const context = require.context('./router', true, /\/[\w]+\.(js|ts)$/)

context.keys().forEach(_ => {
  const path = _.replace('./', '')
  routes.push(...require('./router/' +  path).routes)
})

export default new Router({
  routes: [
    { path: '/', redirect: '/Home' },
    ...routes
  ]
})
複製代碼

本文轉至: mp.weixin.qq.com/s?__biz=MzA…

相關文章
相關標籤/搜索