手寫簡易版vue-router

 

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

vue-router是開發vue項目中必不可少的依賴,爲了能更好的理解其實現原理,而源碼閱讀起來又過於複雜和枯燥,筆者這裏實現一個簡易版本的vue-rouer,幫助本身來更好的理解源碼。javascript

其功能以下:vue

  • 經過Vue插件形式使用
  • 支持hash模式
  • 支持嵌套路由
  • router-view組件
  • router-link組件
  • 路由守衛

基本使用

基礎demo單獨新建了一個分支 ,方便學習和查看java

在實現本身的router以前,咱們先使用官方的包來書寫一個基礎demo,以後咱們會以這個demo爲需求,一步步實現咱們本身的vue-router。vue-router

demo的代碼邏輯以下:數組

  • App頁面中擁有Home和About倆個連接
  • 點擊Home會跳轉到Home頁面
  • 點擊About會跳轉到About頁面
  • 而About又有to a和to b倆個連接,分別跳轉到a和b頁面

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

下面開始使用咱們本身寫的vue-router來實現上邊展現的功能。app

intall方法

vue-router使用方式以下:ide

import Vue from 'vue';
import VueRouter from '../my-router';
import Home from '../views/Home.vue';
import About from '@/views/About';
Vue.use(VueRouter);
const routes = [
  {
    path: '/home',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About
  }
];
const router = new VueRouter({
  routes
});
export default router;

以後會在main.js中將router做爲配置項傳入:函數

import Vue from 'vue';
import App from './App.vue';
import router from './router';

Vue.config.productionTip = false;

new Vue({
  router,
  render: h => h(App)
}).$mount('#app');

由用法咱們能夠知道vue-router是一個類,而且它有一個靜態方法install:學習

import install from '@/my-router/install';

class VueRouter {
  constructor (options) {
  
  }
  init(app) {
  
  }
}

VueRouter.install = install;
export default VueRouter;

install方法中會爲全部組件添加$router以及$route屬性,而且會全局註冊router-view以及router-link組件。咱們在install.js中來單獨書寫install的邏輯:this

import RouterView from '@/my-router/components/view';
import RouterLink from '@/my-router/components/link';

const install = (Vue) => {
  Vue.mixin({
    beforeCreate () {
      const { router } = this.$options;
      // mount $router property for all components
      if (router) {
        // 用_rootRouter來存儲根實例
        this._rootRouter = this;
        // 爲根實例添加$router屬性
        this.$router = router;
        // 在實例上定義響應式屬性,可是這個API可能會發生變化,因此Vue並無在文檔中提供
        Vue.util.defineReactive(this, '$route', this.$router.history.current);
        // 初始化路由
        router.init(this);
      } else {
        this._rootRouter = this.$parent && this.$parent._rootRouter;
        if (this._rootRouter) {
          this.$router = this._rootRouter.$router;
          // 在爲$route賦值時,會從新指向新的地址,致使子組件的$route再也不更新
          // this.$route = this._rootRouter.$route;
          Object.defineProperty(this, '$route', {
            get () {
              return this._rootRouter.$route;
            }
          });
        }
      }
    }
  });
  Vue.component('RouterView', RouterView);
  Vue.component('RouterLink', RouterLink);
};

在install方法中作了以下幾件事:

  • 爲全部組件的實例添加_rootRouter,值爲根實例,方便獲取根實例上的屬性和方法
  • 在根實例執行beforeCreate鉤子時執行VueRouter實例的init方法
  • 爲全部組件的實例添加$router屬性,值爲VueRouter實例
  • 爲全部組件添加$route屬性,值爲當前的路由信息(以後會介紹它的由來)

hashchange事件

vue-router在hash模式下能夠利用hash值的切換來渲染對應的組件,原理實際上是利用頁面地址hash值發生改變不會刷新頁面,而且會觸發hashchange事件。

在history目錄下,新建hash.js來存放hash值變化,組件進行切換的邏輯:

import { getHash } from '@/my-router/util';
import { createRoute } from '@/my-router/create-matcher';

const ensureSlash = () => {
  if (!location.hash) {
    location.hash = '/';
  }
};

class HashHistory {
  constructor (router) {
    this.router = router;
    // 綁定this指向
    this.onHashchange = this.onHashchange.bind(this);
    // 默認hash值爲'/'
    ensureSlash();
  }

  listenEvent () {
    window.addEventListener('hashchange', this.onHashchange);
  }
  
  onHashchange () {
  }
}
export default HashHistory;

在VueRouter實例執行init方法時,監聽hashchange事件:

class VueRouter {
  constructor (options) {
    this.history = new HashHistory(this);
  }

  init (app) {
    // 第一次渲染時也須要手動執行一次onHashchange方法
    this.history.onHashchange();
    this.history.listenEvent();
  }
}

在onHashchange方法中,須要根據當前頁面地址的hash值來找到其對應的路由信息:

class HashHistory {
  // some code ...
  onHashchange () {
    const path = getHash();
    const route = this.router.match(path);
  }
}

匹配路由信息

爲了找到當前的路由信息,HashHistory中調用了VueRouter的match方法。match方法放到了create-matcher.js中來實現:

// create-matcher.js
export const createRoute = (route, path) => {
  const matched = [];
  // 遞歸route的全部父路由,生成matched數組,並和path一塊兒返回,做爲當前的路由信息
  while (route) {
    matched.unshift(route);
    route = route.parent;
  }
  return {
    path,
    matched
  };
};

function createMatcher (routes) {
  const pathMap = createRouteMap(routes);
  // need to get all matched route, then find current routes by matched and router-view
  const match = (path) => {
    const route = pathMap[path];
    return createRoute(route, path);
  };
  return {
    match
  };
}
// create-route-map.js
function addRouteRecord (routes, pathMap, parent) {
  routes.forEach(route => {
    const { path, children, ...rest } = route;
    // 拼接子路由path
    const normalizedPath = parent ? parent.path + '/' + path : path;
    // 將parent也放入到屬性中,方便以後生成matched數組
    pathMap[normalizedPath] = { ...rest, path: normalizedPath, parent };
    if (children) {
      // 繼續遍歷子路由
      addRouteRecord(children, pathMap, route);
    }
  });
}

const createRouteMap = (routes, pathMap = {}) => {
  addRouteRecord(routes, pathMap);
  return pathMap;
};

createMatcher會經過createRouteMap生成hash值和路由的映射關係:

const pathMap = {
  '/about': {
    path: '/about',
    name: 'About',
    children: [
      // ...  
    ],
    parent: undefined
  }
  // ...  

這樣咱們能夠很方便的經過hash值來獲取路由信息。

最終咱們調用match方法獲得的路由信息結構以下:

{
  "path": "/about/a",
  "matched": [
    {
      "path": "/about",
      "name": "About",
      "component": About,
      "children": [
        {
          "path": "a",
          "name": "AboutA",
          "component": A
        },
        {
          "path": "b",
          "name": "AboutB",
          "component": B
        }
      ]
    },
    // ...  
  ]
}

須要注意的是對象中的matched屬性,它裏面存放的是當前hash匹配的全部路由信息組成的數組。在實現嵌套路由時會用到matched數組,由於嵌套路由本質上是router-view組件的嵌套,因此能夠根據router-view在組件中的深度在matched中找到對應的匹配項,而後進行展現。

如今咱們回到hashHistory的onHashchange方法,它會調用VueRouter實例的match方法,代碼以下:

class VueRouter {
  constructor (options) {
    this.matcher = createMatcher(options.routes);
    this.history = new HashHistory(this);
  }

  init (app) {
    this.history.onHashchange();
    this.history.listenEvent();
  }

  match (path) {
    return this.matcher.match(path);
  }
}

在hashHistory中將其賦值給實例中的current屬性:

class HashHistory {
  constructor (router) {
    // pass instance of VueRoute class, can call methods and properties of instance directly
    this.router = router;
    // 當前的路由信息,在current更新後,因爲其不具備響應性,因此儘管值更新了,可是不會觸發頁面渲染
    // 須要將其定義爲響應式的數據
    this.current = createRoute(null, '/');
    this.onHashchange = this.onHashchange.bind(this);
    ensureSlash();
  }

  listenEvent () {
    window.addEventListener('hashchange', this.onHashchange);
  }

  onHashchange () {
    const path = getHash();
    const route = this.router.match(path);
    this.current = route
  }
}

爲了方便用戶訪問當前路由信息,而且讓其具備響應性,會經過Vue.util.defineReactive來爲vue的根實例提供響應性的$route屬性,並在每次頁面初始化以及路徑更新時更新$route:

class HashHistory {
  constructor (router) {
    // pass instance of VueRoute class, can call methods and properties of instance directly
    this.router = router;
    // 當前的路由信息,在current更新後,因爲其不具備響應性,因此儘管值更新了,可是不會觸發頁面渲染
    // 須要將其定義爲響應式的數據
    this.current = createRoute(null, '/');
    this.onHashchange = this.onHashchange.bind(this);
  }
  // some code ...
  onHashchange () {
    const path = getHash();
    const route = this.router.match(path);
    // 將當前路由賦值給根實例,app會在router.init方法中進行初始化
    this.router.app.$route = this.current = route
  }
}

在install方法中爲根實例定義$route屬性,並將全部子組件實例的$route屬性賦值爲根實例的$route屬性:

const install = (Vue) => {
  Vue.mixin({
    beforeCreate () {
      const { router } = this.$options;
      // mount $router property for all components
      if (router) {
        this._rootRouter = this;
        this.$router = router;
        // 定義響應性$route屬性
        Vue.util.defineReactive(this, '$route', this.$router.history.current);
        router.init(this);
      } else {
        this._rootRouter = this.$parent && this.$parent._rootRouter;
        if (this._rootRouter) {
          this.$router = this._rootRouter.$router;
          // 這樣直接賦值會致使引用刷新而沒法改變$route
          // this.$route = this._rootRouter.$route;
          // 獲取根組件實例的$route屬性,其具備響應性
          Object.defineProperty(this, '$route', {
            get () {
              return this._rootRouter.$route;
            }
          });
        }
      }
    }
  });
};

到這裏,咱們已經能夠在地址切換時獲取到對應的路由信息,接下來咱們實現router-view來展現對應的組件。

實現router-view組件

router-view組件須要展現當前匹配的hash所對應的component,這裏採用函數式組件來實現:

export default {
  name: 'RouterView',
  render (h) {
    // 記錄組件的深度,默認爲0
    let depth = 0;
    const route = this.$parent.$route;
    let parent = this.$parent;
    // 遞歸查找父組件,若是父組件是RouterView組件,深度++
    // 最終的深度即爲路由的嵌套層數
    while (parent) {
      if (parent.$options.name === 'RouterView') {
        depth++;
      }
      parent = parent.$parent;
    }
    // 根據深度從matched中找到對應的記錄
    const record = route.matched[depth];
    if (record) { // /about會匹配About頁面,會渲染About中的router-view,此時record爲undefined
      return h(record.component);
    } else {
      return h();
    }
  }
};

這裏必需要爲組件指定name屬性,方便進行遞歸查找,進行深度標識。

到這裏,咱們爲路由信息中添加的matched數組,終於派上了用場,其與router-view組件的深度depth進行巧妙結合,最終展現出了全部匹配到的路由組件。

實現router-link組件

router-link主要支持如下幾個功能:

  • 進行路由跳轉
  • 經過傳入的tag渲染不一樣標籤
  • 爲當前激活的router-link添加router-link-active類名

在頁面中vue-router也支持經過router-link來進行路由跳轉,其實現比較簡單:

export default {
  props: {
    to: {
      type: String,
    },
    tag: {
      type: String,
      default: () => 'a'
    }
  },
  computed: {
    active () {
      return this.$route.matched.map(item => item.path).includes(this.to);
    }
  },
  methods: {
    onClick () {
      this.$router.push(this.to);
    }
  },
  render () {
    return (
      <this.tag
        onClick={this.onClick}
        href="javascript:;"
        class={{ 'router-link-active': this.active }}
      >
        {this.$slots.default}
      </this.tag>
    );
  }
};

router-link能夠接受tag來渲染不一樣的標籤,默認會渲染a標籤。當點擊router-link的時候,其內部會調用VueRouter實例的push方法:

class VueRouter {
  // some code ...
  push (path) {
    location.hash = path;
  }
  // some code ...
}
// some code ...

push方法會切換頁面的hash,當hash發生變化後,就會觸發hashchange事件,執行事件處理函數onHashchange,從新經過path匹配對應的路由信息。

在代碼中咱們經過計算屬性active來計算當前的router-link是否激活,須要注意的是當子路由激活時父路由也會激活。若是matched的path屬性組成的數組中包含this.to,說明該router-link被激活。用戶能夠經過router-link-active類來設置激活樣式。

路由beforeEach鉤子

在平常開發中,常常會用到beforeEach全局前置守衛,讓咱們在進入頁面以前執行一些邏輯:

// some code ....
const router = new VueRouter({
  routes
});
// 在每次進入頁面以前,都會先執行全部的beforeEach中的回調函數
router.beforeEach((to, from, next) => {
  console.log(1);
  setTimeout(() => {
    // 調用下一個回調函數
    next();
  }, 1000);
});
router.beforeEach((to, from, next) => {
  console.log(2);
  next();
});

在每次進入頁面以前,vue-router會先執行beforeEach中的回調函數,而且只有當用戶調用回調函數中傳入的next函數後,纔會執行以後的beforeEach中的回調。

當全部beforeEach中的回調執行完畢後,調用next函數會更新路由信息,而後經過router-view來顯示對應的組件。其實現以下:

// my-router/index.js
class VueRouter {
  constructor (options) {
    // some code ...
    this.beforeEachs = [];
  }

  // cache in global, execute before get matched route record
  beforeEach (fn) {
    this.beforeEachs.push(fn);
  }
}

// my-router/history/hash.js
class HashHistory {
  // some code ...
  onHashchange () {
    const path = getHash();
    const route = this.router.match(path);
    // 當用戶手動調用next時,會執行下一個beforeEach鉤子,在全部的鉤子執行完畢後,會更新當前路由信息
    const next = (index) => {
      const { beforeEachs } = this.router;
      if (index === beforeEachs.length) {
        //update route after executed all beforeEach hook
        this.router.app.$route = this.current = route;
        return;
      }
      const hook = beforeEachs[index];
      hook(route, this.current, () => next(index + 1));
    };
    next(0);
  }
}

上述代碼的執行流程以下:

  • 將beforeEach中傳入的函數放到全局的數組beforeEachs中
  • 在根據路徑匹配最新的路由信息時,先執行beforeEachs中存儲的函數
  • 根據一個遞增的index來讀取beforeEachs中的函數,執行時傳入新的路由信息route、舊的路由信息this.current,以及須要用戶調用的回調函數
  • 當用戶調用回調後,index+1繼續執行next函數,進而執行beforeEachs中的下一個函數
  • 當執行完beforeEachs中的全部函數後,爲$route賦值最新的路由信息

慶祝一下????,這裏咱們已經完成了文章開頭定下的全部目標!

結語

但願在讀完文章以後,能讓讀者對vue-router的底層實現有更深刻的瞭解,明白平常使用的API是怎麼來的,從而更加熟練的使用vue-router。

相關文章
相關標籤/搜索