實現自己的Vue Router -- Vue Router原理解析

蔣鵬飛發表於2020-01-20

前端路由和後端路由

以前的很多技術,比如PHP,路由是直接發給後端,然後由後端根據路由返回對應的頁面。但是現在的前端技術,比如Vue, React都用的前端路由了,就是使用者輸入的路由跟後端並不是對應的了,而是前端來處理路由了,然後由前端來發起對應的請求。前端路由,後端路由具體流程如下:

後端路由

1. 輸入url
2. 請求傳送到伺服器
3. 伺服器解析請求的地址
4. 拿到對應的頁面
5. 返回頁面
複製程式碼

前端路由

1. 輸入url
2. js解析地址
3. 找到地址對應的頁面
4. 執行頁面的js
5. 渲染頁面
複製程式碼

Vue-Router工作流程

vue-router的工作流程有如下幾步

1. url改變
2. 觸發監聽事件
3. 改變vue-router裡面的current變數
4. 監視current變數(變數的監視者)
5. 獲取對應的元件
6. render新元件
複製程式碼

Vue-Router的路由模式有兩種:hash和history,這兩種模式的監聽方法不一樣

監聽url改變事件

hash模式的值可以通過location.hash拿到,監聽改變可以使用onhashchange事件;history的值可以用location.pathname拿到,可以用onpopstate事件來監聽改變。

image-20200119151237561

Vue外掛

在使用Vue-Router之前我們都會呼叫下Vue.use,那Vue.use方法究竟是幹嘛的呢?Vue.use方法接收一個物件作為引數,並且會執行這個物件的install方法,如果沒有install方法會檢查這個引數是不是方法,如果是方法就執行這個方法:

function pluginA() {
  console.log(1);
}

pluginA.install = function() {
  console.log('install');
}

// pluginA如果沒有install屬性,執行本身,輸出1
// 如果有install屬性,執行install屬性對應的方法,輸出install
Vue.use(pluginA);  // console: install
複製程式碼

要實現外掛功能,關鍵是Vue.use在執行install方法的時候,會傳一個引數vue進去,這個引數是Vue的類,可以通過這個引數呼叫vue的API:

pluginA.install = function(vue) {
  console.log(vue);
}
複製程式碼

image-20200119172806418

我們要實現路由外掛功能的關鍵是使用vue.mixinAPI,這個API可以將一些變數和方法全域性混入Vue的例項,下面我們混入一個測試資料,並渲染到跟路由上:

pluginA.install = function(vue) {
    vue.mixin({
    data() {
      return {globalData: 'this is mixin data'}
    }
  })
}
複製程式碼

然後在所有路由上都可以直接使用這個變數了,跟路由也是,而不需要匯入。

image-20200119173601260

vue.mixin不僅可以混入變數和方法,還可以混入生命週期,在這裡混入的生命週期在每個元件的這個生命週期的這個階段都會呼叫:

pluginA.install = function(vue) {
    vue.mixin({
    data() {
      return {globalData: 'this is mixin data'}
    },
    // 混入生命週期
    created() {
      console.log('I am global created');
    }
  })
}
複製程式碼

需要注意的是,new VueApp.vue也算兩個元件,也會執行一次生命週期,在/test下只有一個Test元件的情況下,I am global created會列印三次,分別對應new VueApp.vueTest元件。在混入的方法或者生命週期裡面可以拿到this,這個this分別指向對應的元件例項,很多外掛特性都是靠這個實現的

image-20200119175637278

為了實現我們的route外掛,除了需要vue.mixin外,還需要vue.util,這是個工具類,裡面主要有四個方法:

1. warn: 丟擲警告
2. extend:類似於Object.assign,一層拷貝,Object.assign有相容問題,這個方法是一個for...in迴圈
3. mergeOptions:合併選項
4. defineReactive:這就是Vue實現響應式的核心程式碼,可以看看之前我講Vue響應式的文章,裡面會實現對物件get,set的監聽,現在Vue通過util類將這個方法暴露出來了,我們可以用它來監聽外部變數,這裡主要是監聽router的current變數。
複製程式碼

vue.util.extend不同於vue.extendvue.extend可以繼承單個元件,然後渲染單個元件,可以用於單元測試

實現自己的Vue Router

前置知識都講完了,下面正式開始寫一個自己的vue router。第一步我們需要建一個history類,這個類很簡單,只有一個屬性,用來儲存current

class HistoryRoute {
  constructor() {
    this.current = null;
  }
}
複製程式碼

然後建一個主要的vueRouter類,這個類會有mode,history,routes三個屬性,mode用來接收是hash模式還是history模式,history就是上面HistoryRoute的一個例項,routes是路由列表。建構函式裡面還需要呼叫一個init方法,這個方法根據mode不同,註冊不同的事件來監聽路由變化,並將變化的路由存到history.current上。

class vueRouter {
  constructor(options) {
    this.mode = options.mode || 'hash';
    this.routes = options.routes || [];
    this.history = new HistoryRoute();

    // 將陣列結構的routes轉化成一個更好查詢的物件
    this.routesMap = this.mapRoutes(this.routes);
    this.init();
  }

  // 載入事件監聽
  init() {
    if(this.mode === 'hash'){
      // 如果url沒有hash,給一個預設的根目錄hash
      location.hash ? '' : location.hash = '/';
      window.addEventListener('load', () => {
        // 頁面載入的時候初始化,儲存hash值到history的current上,並且去掉開頭的#
        this.history.current = location.hash.slice('1');
      });
      window.addEventListener('hashchange', () => {
        // hash改變的時候更新history的current
        this.history.current = location.hash.slice('1');
      })
    } else {
      // else處理history模式
      // 如果url沒有pathname,給一個預設的根目錄pathname
      location.pathname ? '' : location.pathname = '/';
      window.addEventListener('load', () => {
        // 頁面載入的時候初始化,儲存pathname值到history的current上
        this.history.current = location.pathname;
      });
      window.addEventListener('popstate', () => {
        // pathname改變的時候更新history的current
        this.history.current = location.pathname;
      })
    }
  }

  /*
  將 [{path: '/', component: Hello}]
  轉化為 {'/': Hello}
  */
  mapRoutes(routes) {
    return routes.reduce((res, current) => {
      res[current.path] = current.component;
      return res;
    }, {})
  }
}
複製程式碼

最後需要給vueRouter一個install方法,這個方法是vue.use會呼叫的外掛方法,這個方法裡面需要將路由相關資訊注入到vue裡面去

// 新增install屬性,用來執行外掛
vueRouter.install = function(vue) {
  vue.mixin({
    beforeCreate() {
      // 獲取new Vue時傳入的引數
      if(this.$options && this.$options.router) {
        this._root = this;
        this._router = this.$options.router;

        // 監聽current, defineReactive(obj, key, val)不傳第三個引數,第三個引數預設是obj[key]
        // 第三個引數傳了也會被監聽,效果相當於,第一個引數的子級
        vue.util.defineReactive(this, 'current', this._router.history);
      } else {
        // 如果不是根元件,就往上找
        this._root = this.$parent._root;
      }

      // 暴露一個只讀的$router
      Object.defineProperty(this, '$router', {
        get() {
          return this._root._router;
        }
      })
    }
  });

  // 新建一個router-view元件,這個元件根據current不同會render不同的元件
  // 最終實現路由功能
  vue.component('router-view', {
    render(h){
      const current = this._self._root._router.history.current;
      const routesMap = this._self._root._router.routesMap;
      const component = routesMap[current];

      return h(component);
    }
  })
}
複製程式碼

總結

其實上面的基礎版vue router主要包括兩部分,一部分是瀏覽器地址的監聽,將url改變監聽到並存入vueRouter類中,另一部分是將vueRouter與vue連線起來,這部分主要是靠vue的外掛機制實現的。

這個例子的完整程式碼可以看我的github: github.com/dennis-jian…


原創不易,每篇文章都耗費了作者大量的時間和心血,如果本文對你有幫助,請點贊支援作者,也讓更多人看到本文~~

更多文章請看我的掘金文章彙總



相關文章