實現一個簡化版的vue-router

本文旨在介紹vue-router的實現思路,並動手實現一個簡化版的vue-router。咱們先來看一下通常項目中對vue-router最基本的一個使用,能夠看到,這裏定義了四個路由組件,咱們只要在根vue實例中注入該router對象就可使用了.javascript

import VueRouter from 'vue-router';
import Home from '@/components/Home';
import A from '@/components/A';
import B from '@/components/B'
import C from '@/components/C'

Vue.use(VueRouter)

export default new VueRouter.Router({
  // mode: 'history',
  routes: [
    {
      path: '/',
      component: Home
    },
    {
      path: '/a',
      component: A
    },
    {
      path: '/b',
      component: B
    },
    {
      path: '/c',
      component: C
    }
  ]
})
複製代碼

vue-router提供兩個全局組件,router-viewrouter-link,前者是用於路由組件的佔位,後者用於點擊時跳轉到指定路由。此外組件內部能夠經過this.$router.push,this.$rouer.replace等api實現路由跳轉。本文將實現上述兩個全局組件以及pushreplace兩個api,調用的時候支持params傳參,而且支持hashhistory兩種模式,忽略其他api、嵌套路由、異步路由、abstract路由以及導航守衛等高級功能的實現,這樣有助於理解vue-router的核心原理。本文的最終代碼不建議在生產環境使用,只作一個學習用途,下面咱們就來一步步實現它。vue

install實現

任何一個vue插件都要實現一個install方法,經過Vue.use調用插件的時候就是在調用插件的install方法,那麼路由的install要作哪些事情呢?首先咱們知道 咱們會用new關鍵字生成一個router實例,就像前面的代碼實例同樣,而後將其掛載到根vue實例上,那麼做爲一個全局路由,咱們固然須要在各個組件中均可以拿到這個router實例。另外咱們使用了全局組件router-viewrouter-link,因爲install會接收到Vue構造函數做爲實參,方便咱們調用Vue.component來註冊全局組件。所以,在install中主要就作兩件事,給各個組件都掛載router實例,以及實現router-viewrouter-link兩個全局組件。下面是代碼:java

const install = (Vue) => {

  if (this._Vue) {
    return;
  };
  Vue.mixin({
    beforeCreate() {
      if (this.$options && this.$options.router) {
        this._routerRoot = this;
        this._router = this.$options.router;
        Vue.util.defineReactive(this, '_routeHistory', this._router.history)
      } else {
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }

      Object.defineProperty(this, '$router', {
        get() {
          return this._routerRoot._router;
        }
      })

      Object.defineProperty(this, '$route', {
        get() {
          return {
            current: this._routerRoot._routeHistory.current,
            ...this._routerRoot._router.route
          };
        }
      })
    }
  });

  Vue.component('router-view', {
    render(h) { ... }
  })

  Vue.component('router-link', {    
    props: {
      to: String,
      tag: String,
    },
    render(h) { ... }
  })
  this._Vue = Vue;
}
複製代碼

這裏的this表明的就是vue-router對象,它有兩個屬性暴露出來供外界調用,一個是install,一個是Router構造函數,這樣能夠保證插件的正確安裝以及路由實例化。咱們先忽略Router構造函數,來看install,上面代碼中的this._Vue是個開始沒有定義的屬性,他的目的是防止屢次安裝。咱們使用Vue.mixin對每一個組件的beforeCreate鉤子作全局混入,目的是讓每一個組件實例共享router實例,即經過this.$router拿到路由實例,經過this.$route拿到路由狀態。須要重點關注的是這行代碼:git

Vue.util.defineReactive(this, '_routeHistory', this._router.history)
複製代碼

這行代碼利用vue的響應式原理,對根vue實例註冊了一個_routeHistory屬性,指向路由實例的history對象,這樣history也變成了響應式的。所以一旦路由的history發生變化,用到這個值的組件就會觸發render函數從新渲染,這裏的組件就是router-view。從這裏能夠窺察到vue-router實現的一個基本思路。上述的代碼中對於兩個全局組件的render函數的實現,由於會依賴於router對象,咱們先放一放,稍後再來實現它們,下面咱們分析一下Router構造函數。github

Router構造函數

通過剛纔的分析,咱們知道router實例須要有一個history對象,須要一個保存當前路由狀態的對象route,另外很顯然還須要接受路由配置表routes,根據routes須要一個路由映射表routerMap來實現組件搜索,還須要一個變量mode判斷是什麼模式下的路由,須要實現pushreplace兩個api,代碼以下:vue-router

const Router = function (options) {
  this.routes = options.routes; // 存放路由配置
  this.mode = options.mode || 'hash';
  this.route = Object.create(null), // 生成路由狀態
  this.routerMap = createMap(this.routes) // 生成路由表
  this.history = new RouterHistory(); // 實例化路由歷史對象
  this.init(); // 初始化
}

Router.prototype.push = (options) => { ... }

Router.prototype.replace = (options) => { ... }

Router.prototype.init = () => { ... }
複製代碼

咱們看一下路由表routerMap的實現,因爲不考慮嵌套等其餘狀況,實現很簡單,以下:api

const createMap = (routes) => {
  let resMap = Object.create(null);
  routes.forEach(route => {
    resMap[route['path']] = route['component'];
  })
  return resMap;
}
複製代碼

RouterHistory的實現也很簡單,根據前面分析,咱們只須要一個current屬性就能夠,以下:瀏覽器

const RouterHistory = function (mode) {
  this.current = null; 
}
複製代碼

有了路由表和historyrouter-view的實現就很容易了,以下:異步

Vue.component('router-view', {
    render(h) {
      let routerMap = this._self.$router.routerMap;
      return h(routerMap[this._self.$route.current])
    }
  })
複製代碼

這裏的this是一個renderProxy實例,他有一個屬性_self能夠拿到當前的組件實例,進而訪問到routerMap,能夠看到路由實例historycurrent本質上就是咱們配置的路由表中的path函數

接下來咱們看一下Router要作哪些初始化工做。對於hash路由而言,url上hash值的改變不會引發頁面刷新,可是能夠觸發一個hashchange事件。因爲路由history.current初始爲null,所以匹配不到任何一個路由,因此會致使頁面刷新加載不出任何路由組件。基於這兩點,在init方法中,咱們須要實現對頁面加載完成的監聽,以及hash變化的監聽。對於history路由,爲了實現瀏覽器前進後退時準確渲染對應組件,還要監聽一個popstate事件。代碼以下:

Router.prototype.init = function () {

  if (this.mode === 'hash') {
    fixHash()
    window.addEventListener('hashchange', () => {
      this.history.current = getHash();
    })
    window.addEventListener('load', () => {
      this.history.current = getHash();
    })
  }

  if (this.mode === 'history') {
    removeHash(this);
    window.addEventListener('load', () => {
      this.history.current = location.pathname;
    })
    window.addEventListener('popstate', (e) => {
      if (e.state) {
        this.history.current = e.state.path;
      }
    })
  }

}
複製代碼

當啓用hash模式的時候,咱們要檢測url上是否存在hash值,沒有的話強制賦值一個默認pathhash路由時會根據hash值做爲key來查找路由表。fixHashgetHash實現以下:

const fixHash = () => {
  if (!location.hash) {
    location.hash = '/';
  }
}
const getHash = () => {
  return location.hash.slice(1) || '/';
}
複製代碼

這樣在刷新頁面和hash改變的時候,current能夠獲得賦值和更新,頁面能根據hash值準確渲染路由。history模式也是同樣的道理,只是它經過location.pathname做爲key搜索路由組件,另外history模式須要去除url上可能存在的hash,removeHash實現以下:

const removeHash = (route) => {
  let url = location.href.split('#')[1]
  if (url) {
    route.current = url;
    history.replaceState({}, null, url)
  }
}
複製代碼

咱們能夠看到當瀏覽器後退的時候,history模式會觸發popstate事件,這個時候是經過state狀態去獲取path的,那麼state狀態從哪裏來呢,答案是從window.history對象的pushStatereplaceState而來,這兩個方法正好能夠用來實現routerpush方法和replace方法,咱們看一下這裏它們的實現:

Router.prototype.push = function (options) {
  this.history.current = options.path;
  if (this.mode === 'history') {
    history.pushState({
      path: options.path
    }, null, options.path);
  } else if (this.mode === 'hash') {
    location.hash = options.path;
  }
  this.route.params = {
    ...options.params
  }
}

Router.prototype.replace = function (options) {
  this.history.current = options.path;
  if (this.mode === 'history') {
    history.replaceState({
      path: options.path
    }, null, options.path);
  } else if (this.mode === 'hash') {
    location.replace(`#${options.path}`)
  }
  this.route.params = {
    ...options.params
  }
}
複製代碼

pushStatereplaceState可以實現改變url的值但不引發頁面刷新,從而不會致使新請求發生,pushState會生成一條歷史記錄而replaceState不會,後者只是替換當前url。在這兩個方法執行的時候將path存入state,這就使得popstate觸發的時候能夠拿到路徑從而觸發組件渲染了。咱們在組件內按照以下方式調用,會將params寫入router實例的route屬性中,從而在跳轉後的組件B內經過this.$route.params能夠訪問到傳參。

this.$router.push({
    path: '/b',
    params: {
      id: 55
    }
 });

複製代碼

router-link實現

router-view的實現很簡單,前面已經說過。最後,咱們來看一下router-link的實現,先放上代碼:

Vue.component('router-link', {    
    props: {
      to: String,
      tag: String,
    },

    render(h) {
      let mode = this._self.$router.mode;
      let tag = this.tag || 'a';
      let routerHistory = this._self.$router.history;
      return h(tag, {
        attrs: tag === 'a' ? {
          href: mode === 'hash' ? '#' + this.to : this.to,

        } : {},
        on: {
          click: (e) => {
            if (this.to === routerHistory.current) {
              e.preventDefault();
              return;
            }
            routerHistory.current = this.to;
            switch (mode) {
              case 'hash':
                if (tag === 'a') return;
                location.hash = this.to;
                break;
              case 'history':
                history.pushState({
                  path: this.to
                }, null, this.to);
                break;
              default:
            }
            e.preventDefault();
          }
        },
        style: {
          cursor: 'pointer'
        }
      }, this.$slots.default)
    }
  })
複製代碼

router-link能夠接受兩個屬性,to表示要跳轉的路由路徑,tag表示router-link要渲染的標籤名,默認a爲標籤。若是是a標籤,咱們爲其添加一個href屬性。咱們給標籤綁定click事件,若是檢測到本次跳轉爲當前路由的話什麼都不作直接返回,而且阻止默認行爲,不然根據to更換路由。hash模式下而且是a標籤時候能夠直接利用瀏覽器的默認行爲完成url上hash的替換,否者從新爲location.hash賦值。history模式下則利用pushState去更新url。

以上實現就是一個簡單的vue-router,完整代碼參見vue-router-simple

相關文章
相關標籤/搜索