手写vue-Router源码

775 阅读6分钟

前言

个人学习过程中的记录,方便后续的复习,如果有错误或者更好的方法,希望大佬指出

路由跳转过程

页面的url变化(点击浏览器的箭头,直接输入地址,或者根据mode的不同,如history模式通过window.history.pushState,hash模式通过location.hash=xxxx来改变)被监听器监听到(window.addEventListener("popstate",function(){})或者window.onhashchange=function(){}),需要注意的是pushState不会触发popstate事件,然后找到路由表中对应的路由,改变响应式的_route,最后通过RouterView组件渲染出来

文件组成


hash和history有共同的方法,所以写一个base类,他们都可以继承base
routes.js是Router类 index.js则是使用router

RouterTable和Routers基础

先将RouterView和RouterLink变成全局组件,因为需要使用Vue.use()所以Router上面需要有一个install方法,RouterTable用来储存路由表,HistoryMode 则是history模式下相关的方法(为了更加直观,就不加上mode判断了)

import HistoryMode from "./mode/history";
import Vue from "vue";
import RouterView from "./components/RouterView.vue";
import RouterLink from "./components/RouterLink.vue";
Vue.component("RouterView", RouterView);
Vue.component("RouterLink", RouterLink);
class RouterTable {
  constructor(routes) {
    this.pathMap = new Map()
    this.init(routes)
  }
  init(routes) {
    const addRoute = (route) => {
      this.pathMap.set(route.path, route)
    }
    routes.forEach(route => addRoute(route))
  }
  math(path) {
    let find = this.pathMap.get(path)
    if (find) return find
  }
}

export default class Router {
  constructor({ routes = [] }) {
    this.routerTable = new RouterTable(routes)
    this.history = new HistoryMode()
  }

}

Router.install = () => {
  Vue.mixin({
    beforeCreate() {

    },
  });
};

history和hash模式的异同

搞清楚这两个模式有哪些异同,就知道了base hash history 这三个文件下面大概需要写那些东西,具体就不展开讲了,大概就是监听的方式不一致,获取url的方法不一样,跳转的处理不一样(但是跳转要改变的_route是一样的)

HistoryMode基础

先着重讲解history,hash只需要把上述不一样的地方改一改就可以了 首先HistoryMode 的实例需要先调用监听方法,如果url变化,则需要获取url,并且调用跳转方法改变_route
transitionTo继承自BaseMode ,因为上面说了两种模式跳转页面都需要改变_route,这个方法可以直接写在BaseMode里面

import BaseMode from "./base";

export default class HistoryMode extends BaseMode {
  constructor(options) {
    super(options);
    this.initListener()
  }
  //监听
  initListener() {
    window.addEventListener('popstate', () => {
      this.transitionTo(this.getCurrentLocation())
    })
  }
  //获取url
  getCurrentLocation() {
    let path = window.location.pathname
    return path + window.location.search + window.location.hash
  }
}

BaseMode基础

那么怎么知道url改变的时候要改变那个实例上的_route呢,可以在install的时候,调用router实例中的init方法,传入一个回调,然后调用transitionTo的时候,调用回调,改变_route

export default class BaseMode {
//需要在Router中this.history = new HistoryMode()的时候将this传入
//this.history = new HistoryMode(this)
  constructor(router) {
    this.routerTable = router.routerTable
  }
  listen(cb) {
    this.cb = cb
  }
  transitionTo(route) {
  //跳转之前需要通过path获取路由信息,然后通过回调改变`_route`
    this.current =this.routerTable.math(route)
    this.cb(this.current)
  }
}

Router类中的init方法

  init(app) {
    const { history } = this
    history.listen((route) => {
      app._route = route
    })
  }

install

所有的vue component的_routerRoot、_router 都是一样的

vue router实例调用init方法,初始化

Router.install = () => {
  Vue.mixin({
    beforeCreate() {
      if (this.$options.router !== undefined) {
        this._routerRoot = this //可以让组件访问到vue实例以及上面的东西,如_router,_route
        this._router = this.$options.router
        this._router.init(this)
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        this._routerRoot = this.$parent && this.$parent._routerRoot || this
      }
    },
  });
};

RouterView

rener函数中获取当年路由信息,来渲染页面

<script>
export default {
  name: "RouterView",
  render() {
    const route = this._routerRoot._route;
    if (!route) return;
    const hook = {
      init(vnode) {
        route.instance = vnode.componentInstance;
      },
    };
    const { component } = route;
    return <component hook={hook} />;
  },
};
</script>

直接改变url展示

这个时候路由已经初具效果,可以通过改变url展示了

RouterLink

接下来要通过RouterLink改变,接下来就要写Router 上面的push方法

<template>
  <div>
    <a style="cursor: pointer" @click="jump">
      <slot></slot>
    </a>
  </div>
</template>

<script>
export default {
  props: {
    to: {
      type: String,
      required: true,
    },
  },
  data() {
    return {
      test: "123",
    };
  },
  methods: {
    jump(to) {
      const router = this._routerRoot._router;
      router.push(this.to);
    },
  },
};
</script>

Router补充

  push(to) {
    this.history.push(to)
  }

HistoryMode补充

  push(target) {
    this.transitionTo(target)
    window.history.pushState({ key: +new Date() }, "", target)
  }

完整演示

到这儿,history模式的低配版路由就写好了

HashMode

Router 中也需要进行少量的修改,这里就不贴代码了,可以优化成将mode作为参数传入,更具不同的mode进行不同的操作

import BaseHistory from "./base";
export default class HashHistory extends BaseHistory {
  constructor(options) {
    super(options)
    this.initListener()
  }
  initListener() {
    window.onhashchange = () => {
      this.transitionTo(this.getCurrentLocation())
    }
  }
  getCurrentLocation() {
    let path = decodeURI(window.location.hash) || "#/";
    let address = path.split('#')[1]
    //console.log(address, 'path')
    return address;
  }
  push(target) {
    this.transitionTo(target);
    window.location.hash = target
  }
}

路由导航守卫

全局守卫

//全局前置守卫
router.beforeEach((to, from, next) => {
  console.log("router.beforeEach", to, from);
  next();
});
//全局解析守卫
router.beforeResolve((to, from, next) => {
  console.log("router.beforeResolve", to, from);
  next();
});
//全局后置守卫
router.afterEach((to, from) => {
  console.log("router.afterEach", to, from);
});

路由独享守卫 beforeEnter

{
    path: "/",
    name: "Home",
    component: Home,
    beforeEnter: (to, from, next) => {
      console.log("/home.beforeEnter", to, from);
      next();
    },
  },

组件内守卫

组件类守卫就不写他的源码了

  beforeRouteEnter(to, from, next) {
    console.log("home-beforeRouteEnter", to, from);
    next();
  },
  beforeRouteUpdate(to, from, next) {
    console.log("home-beforeRouteUpdate", to, from);
    next();
  },
  beforeRouteLeave(to, from, next) {
    console.log("home-beforeRouteLeave", to, from);
    next();
  },

Router

写一个函数,触发全局路由守卫的时候调用这个函数,将需要执行的函数存入Hooks中

function registerHook(list, fn) {
  list.push(fn);
  return () => {
    let i = list.indexOf(fn);
    if (i > -1) list.splice(i, 1);
  };
}
export default class Router {
  constructor({ routes = [] }) {
   //......
    this.beforeHooks = [];
    this.resolveHooks = [];
    this.afterHooks = [];
  }
  beforeEach(fn) {
    registerHook(this.beforeHooks, fn);
  }
  beforeResolve(fn) {
    registerHook(this.resolveHooks, fn);
  }
  afterEach(fn) {
    registerHook(this.afterHooks, fn);
  }
}

BaseMode

因为路由守卫不管那个MODE都会有,所以在BaseMode中写相关方法
跳转的时候,先执行confirmTransition,在路由导航中进行确认,如果通过则执行onComplete,调用updateRoute,更新界面,执行afterEach,否则执行onAbort进行想要的操作

export default class BaseMode {
  constructor(router) {
    this.routerTable = router.routerTable;
    this.router = router;
  }
  listen(cb) {
    this.cb = cb;
  }
  transitionTo(target) {
    const route = this.routerTable.math(target);
    this.confirmTransition(route, () => {
      this.updateRoute(route);
    });
  }
  confirmTransition(route, onComplete, onAbort) {
   if (route == this.current) return;
  //由于路由导航传入的参数(函数)需要按顺序执行,所以将他们按执行顺序都放入一个数组中,使用类似迭代器来一次调用
    const queue = [
      ...this.router.beforeHooks,
      route.beforeEnter,
      ...this.router.resolveHooks,
    ];
    runQueue(queue, iterator, () => onComplete());
  }
  updateRoute(route) {
    let from = this.current;
    this.current = route;
    this.cb(this.current);
    this.router.afterHooks.forEach((hook) => {
      hook && hook(this.current, from);
    });
  }
}

runQueue

queue是传入的导航守卫传入的参数(函数)组成的数组,iter是一个类似迭代器的东西,end为queue里面所有函数执行完毕之后执行,更新路由,渲染页面

export function runQueue(queue, iter, end) {
  const step = (index) => {
    if (index >= queue.length) {
      end();
    } else {
      if (queue[index]) {
        iter(queue[index], () => {
          step(index + 1);
        });
      } else {
        step(index + 1);
      }
    }
  };
  step(0);
}

iterator

hook 就是beforeEach传入的参数(函数),to 是route(接下来要跳转的页面),from是this.current(离开的页面,此时this.current还没有更新,所以仍然是上一个页面的路由,next(beforeEach里的next)是一个回调,可以传false,error等参数,这里只演示false)

const iterator = (hook, next) => {
      hook(route,this.current,  (to) => {
        if (to === false) {
          onAbort && onAbort();
        } else {
          next();
        }
      });
    };

完整代码

BaseMode

import { runQueue } from "../../util/async";
export default class BaseMode {
  constructor(router) {
    this.routerTable = router.routerTable;
    this.router = router;
  }
  listen(cb) {
    this.cb = cb;
  }
  transitionTo(target) {
    const route = this.routerTable.math(target);
    this.confirmTransition(route, () => {
      this.updateRoute(route);
    });
  }
  confirmTransition(route, onComplete, onAbort) {
    if (route == this.current) return;
    const queue = [
      ...this.router.beforeHooks,
      route.beforeEnter,
      ...this.router.resolveHooks,
    ];
    const iterator = (hook, next) => {
      hook(this.current, route, (to) => {
        if (to === false) {
          onAbort && onAbort();
        } else {
          next();
        }
      });
    };
    runQueue(queue, iterator, () => onComplete());
  }
  updateRoute(route) {
    let from = this.current;
    this.current = route;
    this.cb(this.current);
    this.router.afterHooks.forEach((hook) => {
      hook && hook(this.current, from);
    });
  }
}

history和hash

import BaseMode from "./base";

export default class HistoryMode extends BaseMode {
  constructor(options) {
    super(options);
    this.initListener()
  }
  initListener() {
    window.addEventListener('popstate', () => {
      this.transitionTo(this.getCurrentLocation())
    })
  }
  getCurrentLocation() {
    let path = window.location.pathname
    return path + window.location.search + window.location.hash
  }
  push(target) {
    this.transitionTo(target)
    window.history.pushState({ key: +new Date() }, "", target)
  }
}

import BaseHistory from "./base";
export default class HashHistory extends BaseHistory {
  constructor(options) {
    super(options)
    this.initListener()
  }
  initListener() {
    window.onhashchange = () => {
      this.transitionTo(this.getCurrentLocation())
    }
  }
  getCurrentLocation() {
    let path = decodeURI(window.location.hash) || "#/";
    let address = path.split('#')[1]
    //console.log(address, 'path')
    return address;
  }
  push(target) {
    this.transitionTo(target);
    window.location.hash = target
  }
}

Router

import HistoryMode from "./mode/history";
import Vue from "vue";
import RouterView from "./components/RouterView.vue";
import RouterLink from "./components/RouterLink.vue";
Vue.component("RouterView", RouterView);
Vue.component("RouterLink", RouterLink);
class RouterTable {
  constructor(routes) {
    this.pathMap = new Map();
    this.init(routes);
  }
  init(routes) {
    const addRoute = (route) => {
      this.pathMap.set(route.path, route);
    };
    routes.forEach((route) => addRoute(route));
  }
  math(path) {
    let find = this.pathMap.get(path);
    if (find) return find;
  }
}
function registerHook(list, fn) {
  list.push(fn);
  return () => {
    let i = list.indexOf(fn);
    if (i > -1) list.splice(i, 1);
  };
}
export default class Router {
  constructor({ routes = [] }) {
    this.routerTable = new RouterTable(routes);
    this.history = new HistoryMode(this);
    this.beforeHooks = [];
    this.resolveHooks = [];
    this.afterHooks = [];
  }
  init(app) {
    const { history } = this;
    history.listen((route) => {
      app._route = route;
    });
    history.transitionTo(history.getCurrentLocation());
  }
  push(to) {
    this.history.push(to);
  }
  beforeEach(fn) {
    registerHook(this.beforeHooks, fn);
  }
  beforeResolve(fn) {
    registerHook(this.resolveHooks, fn);
  }
  afterEach(fn) {
    registerHook(this.afterHooks, fn);
  }
}

Router.install = () => {
  Vue.mixin({
    beforeCreate() {
      if (this.$options.router !== undefined) {
        this._routerRoot = this;
        this._router = this.$options.router;
        this._router.init(this);
        Vue.util.defineReactive(this, "_route", this._router.history.current);
      } else {
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this;
      }
    },
  });
};

rouerLink 和routerView

<template>
  <div>
    <a style="cursor: pointer" @click="jump">
      <slot></slot>
    </a>
  </div>
</template>

<script>
export default {
  name: "RouterLink",
  props: {
    to: {
      type: String,
      required: true,
    },
  },
  data() {
    return {
      test: "123",
    };
  },
  methods: {
    jump() {
      const router = this._routerRoot._router;
      router.push(this.to);
    },
  },
};
</script>
<script>
export default {
  name: "RouterView",
  render() {
    const route = this._routerRoot._route;
    if (!route) return;
    const { component } = route;
    return <component />;
  },
};
</script>

效果

结言

低配版的路由源码到此结束,部分是我自己的理解,可能有些许错误,希望看到的大佬不吝赐教