vue-router的实现

216 阅读6分钟

今天我们来写一个简单版的vue-router!

vue-router在vue的使用中是作为一个插件来使用,首先我们先来写一下install方法供vue.use()去调用。

1.install的实现

那么intall做了什么呢?

  • 插件一般用于定义全局组件 全局指令 过滤器 原型方法....
  • 挂载全局组件 如:router-link、router-view等
  • 原型方法:vue.prototype.routevue.prototype.route、vue.prototype.router
  • 接着我们需要router在所有组件中使用,当每个组件创建之前,那我们使用mixin和beforeCreate将根父亲传入的router实例共享给所有的子组件
  • 实例共享后 启动入口 this._router.init(this);
export default function install(Vue,options){
    // 插件安装的入口
    _Vue = Vue; 
   
    Vue.mixin({
        beforeCreate(){ // this指向的是当前组件的实例
            // 将父亲传入的router实例共享给所有的子组件
            if(this.$options.router){//父组件
                this._routerRoot = this;// 我给当前根组件增加一个属性_routerRoot 代表的是他自己
                this._router = this.$options.router
				
				//install完成后 启动入口
                this._router.init(this); // 这里的this就是根实例

                // 如何获取到current属性 将current属性定义在_route上
                Vue.util.defineReactive(this,'_route',this._router.history.current);

                
                
            }else{//孩子组件
                
                this._routerRoot = this.$parent && this.$parent._routerRoot;
            }
            // 无论是父组件还是子组件 都可以通过 this._routerRoot._router 获取共同的实例
        }
    });

    // 插件一般用于定义全局组件 全局指令 过滤器 原型方法....
    Vue.component('router-link',Link);
    Vue.component('router-view',View);

    // 代表路由中所有的属性
    Object.defineProperty(Vue.prototype,'$route',{
        get(){
            return this._routerRoot._route; // path  matched
        }
    })
    Object.defineProperty(Vue.prototype,'$router',{
        get(){
            return this._routerRoot._router; // 方法 push go repace..
        }
    });
}

接着,我们需要根据用户的配置,创建匹配器(扁平化用户routers,返回match和addRoutes)

路由映射表的创建

创建匹配器

import createRouteMap from "./create-route-map"
import {createRoute} from './history/base'
export default function createMatcher(routes) {

    // pathMap = {'/':Home,'/about':About,'/about/a':'aboutA','/about/b':'aboutB'}
    let { pathMap } = createRouteMap(routes); //  扁平化配置
    function addRoutes(routes) {
        createRouteMap(routes,pathMap);
    }

    function match(location) {
        let record = pathMap[location]; // 可能一个路径有多个记录 
        if(record){
            return createRoute(record,{
                path:location
            })
        }
        //  这个记录可能没有
        return createRoute(null,{
            path:location
        })
    }
    return {
        addRoutes, // 添加路由 
        match // 用于匹配路径
    }

}

1.做路由的扁平化操作

//当有动态加载时需两个参数
export default function createRouteMap(routes,oldPathMap){
    let pathMap = oldPathMap || Object.create(null); // 默认没有传递就是直接创建映射关系
    routes.forEach(route => {
        addRouteRecord(route,pathMap);
    });
    return {
        pathMap
    }
}
// 先序深度
function addRouteRecord(route,pathMap,parent){ // parent就是父组件的路由
   
    let path =parent? (parent.path + '/' + route.path) :route.path
    let record = {
        path,
        component:route.component,
        parent 
    }
    if(!pathMap[path]){ // 不能定义重复的路由
        pathMap[path] = record;
    }
    if(route.children){
        route.children.forEach(childRoute=>{
            // 在遍历儿子时 将父亲的记录传入进去
            addRouteRecord(childRoute,pathMap,record);
        })
    }
}

我们拿到扁平化后的match后,我需要根据不同的路径进行切换

先看用户传入的mode是什么模式在去实例它。

 // 我需要根据不同的 路径进行切换
    options.mode = options.mode || 'hash'; // 默认没有传入就是hash模式
    switch (options.mode) {
        case 'hash':
            this.history = new HashHistory(this);
            break;
        case 'history':
            this.history = new BrowserHistory(this);
            break;
    }
    

路由核心跳转模式

history
  -base.js(共有的方法写在这)
  -hash.js
  -history.js
init(app) { // 初始化
        // 监听hash值变化 默认跳转到对应的路径中
        const history = this.history;
        const setUpHashListener = () =>{
            history.setupListener(); // 监听路由变化 hashchange
        }
        // 初始化 会先获得当前hash值 进行跳转, 并且监听hash变化
        history.transitionTo(
            history.getCurrentLocation(), // 获取当前的位置
            setUpHashListener
        )
        history.listen((route)=>{ // 每次路径变化 都会调用此方法  订阅
            app._route = route;
        });
        // setupListener  放到hash里取
        // transitionTo  放到base中 做成公共的方法
        // getCurrentLocation // 放到自己家里  window.location.hash / window.location.path
}

hash.js里的setupListener(监听hash变化)及getCurrentLocation(获取当前路径)

import { History } from "./base";


function ensureSlash() {//确保路径有/
    if(window.location.hash){ // location.hash 是有兼容性问题的 
        return;
    }
    window.location.hash = '/'; // 默认就是 / 路径即可
}
function getHash(){
    return window.location.hash.slice(1);
}
class HashHistory extends History{
    constructor(router){
        super(router);
        this.router = router;
        // 确保hash模式下 有一个/路径
        ensureSlash();
    }
    getCurrentLocation(){
        // 这里也是要拿到hash值
        return getHash();
    }
    push(location){
        this.transitionTo(location,()=>{ // 去更新hash值,hash值变化后虽然会再次跳转但是 不会重新更新current属性
            window.location.hash = location
        })
    }
    setupListener(){
        window.addEventListener('hashchange',()=>{
            // 当hash值变化了 在次拿到hash值进行跳转
            this.transitionTo(getHash()); // hash变化在次进行跳转
        })
    }
}

export default HashHistory

base中的transitionTo

class History {
    constructor(router) {
        this.router = router;

        // 当我们创建完路由号 ,先有一个默认值 路径 和 匹配到的记录做成一个映射表

        // 默认当创建history时 路径应该是/ 并且匹配到的记录是[]
        this.current = createRoute(null, { // 存放路由状态的
            path: '/'
        });
        console.log(this.current)
        // this.current = {path:'/',matched:[]}

    }
    listen(cb) {
        this.cb = cb;
    }
    transitionTo(location, onComplete) {
        // 跳转时都会调用此方法 from  to..   
        // 路径变化了 视图还要刷新 ,  响应式的数据原理
        let route = this.router.match(location); // {'/'.matched:[]}
        // 这个route 就是当前最新的匹配到的结果
        if (location == this.current.path && route.matched.length == this.current.matched.length) { // 防止重复跳转
            return
        }
        let queue = [].concat(this.router.beforeHooks); // 拿到了注册方法 

        const iterator = (hook,next) =>{
            hook(this.current,route,()=>{
                next();
            })
        }
        runQueue(queue, iterator, () => {
            // 在更新之前先调用注册好的导航守卫

            this.updateRoute(route);
            // 根据路径加载不同的组件  this.router.matcher.match(location)  组件 
            // 渲染组件
            onComplete && onComplete();
        })

    }
    updateRoute(route) {
        // 每次你更新的是current
        this.current = route; // 每次路由切换都会更改current属性
        this.cb && this.cb(route); // 发布
        // 视图重新渲染有几个要求? 1.模板中要用  2.current得是响应式的
    }
}
  • transitionTo方法的注意
  • 在我们创建路由时刷新this.current = {path:'/',matched:[]}
  • 在嵌套路由跳转时需要需要,需把父亲放到matched数组位置的前面
export function createRoute(record, location) {
    let res = []; //[/about /about/a] 
    if (record) {
        while (record) {
            res.unshift(record);
            record = record.parent;
        }
    }
    return {
        ...location,
        matched: res
    }
}
  • this.router.match这个发放做了转接,实际调用的是create-matcher.js里的createMatcher
  • 当路由变化时用updateRoute()更改this.current
  • 视图重新渲染有几个要求? 1.模板中要用 2.current得是响应式的

这是我们需要在install.js中把current变成响应式的,需要使用Vue.util.defineReactive变成响应式的方法,这是可以看看install.js如何写的

Vue.util.defineReactive(this,'_route',this._router.history.current);
  • 当路由更改时,我们需要把this._route的值更改,初始化时调用history.listen去更改this._route

这是我们就明白了this.route是当前this.current对象,this.route是当前this.current对象,this.router是router的实例

接下来我们就可以写两个组件了,router-link、router-view

router-link、router-view函数式组件的编写

components
	-link.js
    -view.js

这是就可以在install.js引入组件了

import Link from './components/link';
import View from './components/view';
Vue.component('router-link',Link);
Vue.component('router-view',View);

link.js(router-link)

export default {
    name:'routerLink',
    props:{ // 属性接受
        to:{
            type:String,
            required:true
        },
        tag:{
            type:String,
            default:'a'
        }
    }, // 写组件库 都可以采用jsx 来写
    methods:{
        handler(to){
            this.$router.push(to);
        }
    },  
    render(){
        let {tag,to} = this;
        // jsx 语法  绑定事件
        return <tag onClick={this.handler.bind(this,to)}>{this.$slots.default}</tag>
    }
}
  • 调用handler方法,实际是调用hash的跳转push方法,在调用bash中的transitionTo去更新路径变化

view.js(router-view)

export default {
    name: 'routerView',
    functional: true, // 函数式组件,函数式组件的特点 性能高,不用创建实例 = react函数组件   new Ctor().$mount() 
    render(h, { parent, data }) { // 调用render方法 说明他一定是一个routerView组件
        // 获取 当前对应要渲染的记录 
        let route = parent.$route; // this.current;
        let depth = 0;
        data.routerView = true; // 自定义属性

        // App.vue 中渲染组件时  默认会调用render函数,父亲中没有 data.routerView属性
        // 渲染第一层,并且标识当前routerView为true
        while (parent) { // router-view的父标签
            //  $vnode 代表的是占位符vnode 组件的标签名的虚拟节点
            //  _vnode 组件内部渲染的虚拟节点 
            if (parent.$vnode && parent.$vnode.data.routerView) {
                depth++;
            }
            parent = parent.$parent; // 不停的找父组件
        }
        // 第一层router-view 渲染第一个record 第二个router-view渲染第二个
        let record = route.matched[depth]; // 获取对应层级的记录
        if (!record) {
            return h(); // 空的虚拟节点 empty-vnode  注释节点
        }
        // components
        return h(record.component, data)
    }
}

那路由就大致完成了,加下来写路由的钩子函数了

例如:

router.beforeEach((from,to,next)=>{ 
    console.log(1);
    setTimeout(() => {
        next();
    }, 1000);
})
router.beforeEach((from,to,next)=>{ 
    console.log(2);
    setTimeout(() => {
        next();
    }, 1000);
})

实现:

  • 在router类上定义this.beforeHooks = [];
  • 在实例上加一个方法,做订阅
beforeEach(fn){
        this.beforeHooks.push(fn);
}
  • 在base中的transitionTo的方法中,在更新之前先调用注册好的导航守卫
function runQueue(queue,iterator,cb){
    // 异步迭代
    function step(index){ // 可以实现中间件逻辑
        if(index >= queue.length) return cb();
        let hook = queue[index]; // 先执行第一个 将第二个hook执行的逻辑当做参数传入
        iterator(hook,()=>step(index+1));
    }
    step(0);
}
class History {
	transitionTo(location, onComplete) {
      // 跳转时都会调用此方法 from  to..   
      // 路径变化了 视图还要刷新 ,  响应式的数据原理
      let route = this.router.match(location); // {'/'.matched:[]}
      // 这个route 就是当前最新的匹配到的结果
      if (location == this.current.path && route.matched.length == this.current.matched.length) { // 防止重复跳转
          return
      }
      let queue = [].concat(this.router.beforeHooks); // 拿到了注册方法 

      const iterator = (hook,next) =>{
          hook(this.current,route,()=>{
              next();
          })
      }
      runQueue(queue, iterator, () => {
          // 在更新之前先调用注册好的导航守卫

          this.updateRoute(route);
          // 根据路径加载不同的组件  this.router.matcher.match(location)  组件 
          // 渲染组件
          onComplete && onComplete();
      })

  }
}

好了,差不多写完了,看完给个赞!