手写Vue-Router

1,204 阅读5分钟

思维导图

Vue-Router提供了俩个组件 router-link router-view, 提供了俩个原型上的属性$route $router ,我现在跟着源码来把它实现一下

开始

先看平时使用的 Vue-Router ,引入Router , Vue.use 注册插件。直接从这里开始入手

使用场景

import Vue from 'vue'
import Router from "../vue-router"
import routes from './routes'
Vue.use(Router)
let router =  new Router({
  routes
})

index

先看vue-router.js文件,先生成一个VueRouter类,然后导入install方法,因为Vue-Routerinstall方法比Vuex复杂一些,所以将install单独作为一个文件。

import install from './install';

class VueRouter {
    constructor(options) {
      
    }

}
VueRouter.install = install;

export default VueRouter

Vue.use()

来先看 Vue.use()的源码中的一部分,这里面判断注册的插件里的install是不是一个函数,有就执行插件里的install函数。或者判断插件本身是不是一个函数,有就执行插件本身。这里本质上是没有区别,有没有install都可以。而VueRouter使用了install,目的是为了将install作为入口函数,方便封装,同时也将install和其他代码分开。

if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args)
 } else if (typeof plugin === 'function') {
      plugin.apply(null, args)
 }

install

上述已经将vue-router的类构建好,现在VueRouter实例已经有了,然后执行vue.use(),然后会执行VueRouter类里的install函数,那来看install.js
install里使用了Vue.mixin,混入代码,下面代码是在当前组件生命周期beforeCreate里混入代码,代码逻辑是判断当前组件是否为根组件,如果是则将_routerRoot作为键放入当前组件中,值为Vue实例。再将_router作为键放入当前组件中,值为VueRouter实例。然后执行初始化操作。如果不为当前组件不是根组件,则该组件为根组件的子组件。将_routerRoot作为键放入当前组件中,值为父组件的_routerRoot,从父亲身上获取.
$route$router是利用Object.definePropert代理_routerRoot里的_router_route,访问到的。
接着注册全局组件router-viewrouter-link

import RouterView from '../components/router-view'
import RouterLink from '../components/router-link'

const install = (_Vue, s) => {
    _Vue.mixin({
        beforeCreate() {
            if (this.$options.router) {      // 判断是不是根组件            
                this._routerRoot = this;              // 把vue实例放在当前组件的——routerRoot属性上
                this._router = this.$options.router   // 这里直接获取根实例
                this._router.init(this);      // 初始化     
                _Vue.util.defineReactive(this, '_route', this._router.history.current)  // 将属性_route成为响应式属性
            } else {
                this._routerRoot = this.$parent && this.$parent._routerRoot   // 这里的是从父亲最顶层获取Router实例
            }
        }
    })
    Object.defineProperty(_Vue.prototype, '$route', {    // 代理$route
        get() {
            return this._routerRoot._route
        }
    })
    Object.defineProperty(_Vue.prototype, '$router', {  // 代理$router
        get() {
            return this._routerRoot._router
        }
    })
    _Vue.component('router-view', RouterView)   // 注册组件router-view
    _Vue.component('router-link', RouterLink)   // 注册组件router-view
}
export default install

init

vue-router 默认 hash 模式 —— 使用 URL 的 hash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载。

然后回到VueRouter类中,此时多了个init函数。目前做的路由方式是hash方式,还有另外俩种方式,historyabstract。因为有三种方式,所以Vue-Router做了一个父类base执行同样的逻辑,子类三种方式继承父类base,再独自执行自己方式的代码。
通过new HashHistory 获取 history实例,初始化init执行history实例对应函数。将目光放到history实例上,这些函数来自于base.jshash.js

import install from './install';

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

	

    init(app) {
        const history = this.history;

        const setupHashLister = () => {
            history.setupLister(); // hash的跳转
        }

        history.transitionTo(
            history.getCurrentLocation(),
            setupHashLister
        )

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

    }

}
VueRouter.install = install;

export default VueRouter

hash.js

hash方式的函数就简单介绍一下,看构造函数constructor,跟父类一样赋值router。执行ensureSlash函数,因为hash相比其他函数,一进入页面就会多个#。所以就初始化的时候处理一下。getCurrentLocation函数是获取当前路径的,pushhash方式的跳转,setupLister函数是刚刚所述的监听函数hashchange

import Histroy from './base';

function ensureSlash() {
    if (window.location.hash) {
        return
    }
    window.location.hash = '/';
}


class HashHistory extends Histroy {
    constructor(router) {
        super();
        this.router = router;
        ensureSlash();
    }
    getCurrentLocation() {
        return window.location.hash.slice(1);
    }
    push(location){
        this.transitionTo(location,()=>{
            window.location.hash = location
        })
    }
    setupLister() {
        window.addEventListener('hashchange', () => {
            this.transitionTo(window.location.hash.slice(1));
        })

    }

}
export default HashHistory

base.js

先看构造函数construcror,将router作为键放在自身实例上,值为VueRouter实例,curent为当前导航正要离开的路由,也就是路由守卫的参数里的from
1.transitionTo()为跳转后立即执行的函数,传入当前路径和回调函数,r$route,是扁平化后的配置,也就是即将要进入的目标 路由对象
2.cbHistorylisten()函数,将$route放入当前组件上供用户使用。
3.callback是执行HashHistorysetupHashLister()函数,是给当前window添加监听事件onhashChangeonhashChange后续通过hash变化执行transitionTo进行更新。
4.最后将r赋值给current,更新路由信息。

class History {
    constructor(router) {
        this.router = router;
        this.current = createRoute(null, {
            path: '/'
        })
    }

    transitionTo(location, callback) {

        let r = this.router.match(location)
        if (location == this.current.path && r.matched.length == this.current.matched.length) { // 防止重复跳转
            return
        }


     
        this.cb && this.cb(r);
        callback && callback();
        this.current = r;
    }



    listen(cb) {

        this.cb = cb;
    }

}

扁平化

刚刚base.js里执行的this.router.match(location)以及createRoute(),都是需要建立在扁平化配置基础之上的。 平时配置的路由是这样的,需要将配置进行扁平化,才能用得上。

 [
    {
        path: '/',
        name: 'home',
        component: Home
    },
    {
        path: '/about',
        name: 'about',
        component: About,
        children: [
            {
                path: 'add',
                name: 'add',
                component: Add
            },
            {
                path: 'bull',
                name: 'bull',
                component: Bull
            }
        ]
    }
]

扁平化后是这样的

/: {path: "/", component: {…}, parnent: undefined}
/about: {path: "about", component: {…}, parnent: {…}}
/about/add: {path: "add", component: {…}, parnent: {…}}
/about/bull: {path: "bull", component: {…}, parnent: {…}}

接着看扁平化函数createMatcher以及createRouteMap

createMatcher

createMatcher返回一个match函数,match方法是匹配路径,根据路径拿扁平化对象里的配置,然后执行createRoute方法,将其转化为route,返回。pathMapcreateRouteMap生成

import createRouteMap from './create-route-map'
import { createRoute } from './history/base';

export default function createMatcher(routes) {
    let { pathList, pathMap } = createRouteMap(routes);


    function match(location) {
        console.log(pathMap)
        let record = pathMap[location];
        
        return createRoute(record,{
            path: location
        })
    }

    return {
        match
    }
}

createRouteMap(扁平化处理)

routes配置传入createRouteMap中,遍历routes,进行扁平化操作 pathMap以路径为键名,值为一个对象包裹着路径,组件,父组件。 将路径匹配上父组件的路径和自身的路径 如果有子组件就进行递归,全部转为扁平化返回。

export default function createRouteMap(routes, oldPathList, oldpathMap) {

    let pathList = oldPathList || [];
    let pathMap = oldpathMap || Object.create(null);
    routes.forEach(route => {
        addRouteRecord(route, pathList, pathMap);
    })

    return {
        pathList,
        pathMap
    }
}

function addRouteRecord(route, pathList, pathMap,parnent) {
    let path = parnent ? `${parnent.path}/${route.path}`: route.path;
    let record = {
        path: route.path,
        component: route.component,
        parnent
    }
    if (!pathMap[path]) {
        pathList.push(path);
        pathMap[path] = record;
    }
    if(route.children){
        route.children.forEach(route=>{
            addRouteRecord(route, pathList, pathMap,record)
        })
    }
}

createRoute

createRoute是生成$route的函数,传入参数为扁平化配置,路径。将res作为空数组,如果传进来的扁平化配置有值,则进行while循环,将自己从数组头部插入,取出父组件再从头部插入,如此反复,得到一个含着层次关系的数组。将loaction和数组包裹为对象返回。

export function createRoute(record, location) {
    let res = [];

    if (record) {
        while (record) {
            res.unshift(record)
            record = record.parnent
        }
    }

    return {
        ...location,
        matched: res
    }
}

router-view

然后在来看看routerview 是一个函数式组件, 返回render方法 进行while循环,遍历出嵌套的routerviewdepth作为深度,也是matchedindex. 每遍历一次,就在$vnode.data.rouView 改为true,将深度加1 返回对应的组件即可

export default {
    name:'routerView',
    functional: true,
    render(h,{parent,data}) {
        let route = parent.$route

        let depth = 0;
        while (parent) {  
            if (parent.$vnode && parent.$vnode.data.routeView) {
                depth++;
            }
            parent = parent.$parent;
        }
        data.routeView = true;
        let record = route.matched[depth];
        if (!record) {
            return h();
        }
        return h(record.component, data);
    }
}

router-link

再来看看routerlink 没什么东西就返回一个a标签,用插槽把对应的文本显示出来,在添加的跳转事件 调用$routerpush方法,也就是Router类上的push

export default {
    name: 'routerLink',
    props: {
        to: {
            type: String,
            required: true
        },
        tag: {
            type: String,
            default: 'a'
        }
    },
    methods: {
        handler(to) {
            this.$router.push(to)   // 路由跳转
        }
    },
    render() {
    
        return <a onClick={this.handler.bind(this,this.to)}>{this.$slots.default[0].text}</a>
    }
}

Vue-Router草稿 :shimo.im/docs/TqKywd…