手写简版vue-router

125 阅读2分钟

vue-router简版的实现

首先,新建一个lrouter文件夹,新建一个index.js,里面的内容和我们平常代码中写的router配置一致, 唯一区别在于引入的router插件不同,使其正常显示即为实现完成.

// index.js
import Vue from 'vue'
import VueRouter from './lvue-router'
import Home from '../views/Home.vue'

// use方法内部会调用install(Vue)
Vue.use(VueRouter)

const routes = [
    {
        path: '/',
        name: 'Home',
        component: Home
    },
    {
        path: '/about',
        name: 'About',
        component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'),
        children: [
            { path: 'a', name: 'AboutA', component: { name: 'AboutA', render: (h) => h('div', 'This is about/a') } },
            { path: 'b', name: 'AboutB', component: { name: 'AboutB', render: (h) => h('div', 'This is about/b') } }
        ]
    },
    {
        path: '/info',
        name: 'About',
        component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'),
        children: [
            { path: 'a', name: 'InfoA', component: { name: 'InfoA', render: (h) => h('div', 'This is info/a') } },
        ]
    }
]

const router = new VueRouter({
  routes
})

export default router

前置工作完成,接下来进行vue-router的代码编写,实现一个VueRouter类以及一个install方法

// vue-router.js
let Vue
Class VueRouter {
    constructor(options) {
        this.$options = options
    }
}
VueRouter.install = function(_Vue) {
    // Vue.use(router)时第一个参数默认传入Vue,在VueRouter中需要使用,提前进行存储
    Vue = _Vue
}
export default VueRouter

在vue-router中,我们需要根据对应路由进行组件切换,因此,我们需要在VueRouter中添加一个响应式的current属性作为当前路由信息,同时,我们需要在全局中注册$router

// vue-router.js
let Vue
Class VueRouter {
    constructor(options) {
        this.options = options
        // current为响应式,因为VueRouter不为响应式,不能通过Vue.set设置,我们采用以下方式
        Vue.util.defineReactive(this, 'current', getHash())
        // 添加hashchange事件
        window.addEventListener('hashchange', () => {
            this.current = getHash()
        })
    }
    getHash(){
        return window.location.hash.slice(1) || '/'
    }
}

VueRouter.install = function(_Vue) {
    // Vue.use(router)时第一个参数默认传入Vue,在VueRouter中需要使用,提前进行存储
    Vue = _Vue
    Vue.mixin({
        // 我们需要注册$router至根组件(router-view中需要获取$router信息以进行路由跳转),但是install(Vue.use(router))时,router还未完成挂载,因此采用混入方式,延迟执行注册操作
        beforeCreate(){
            if (this.$options.router){
                Vue.prototype.$router = this.$options.router
            }
        }
    })
    Vue.component('router-link', link)
    Vue.component('router-view', view)
}
export default VueRouter

接下来我们需要在install时实现router-link以及router-view组件

// link.js
// 分析: 我们的日常使用为<router-link to='/'>首页</router-link> 因此我们需要传入一个to属性,并且点击后跳转至相应链接(本文使用hash模式)
export defalut {
    props: {
        to: {
            type: String,
            required: true
        }
    },
    render(h){
        // h函数即为createElement,依次传入type, attrs, children
        return h('a', {
            attrs: {
                href: '#' + this.to
            }
        // 获取插槽值并传入children, 此时其值为'首页'
        }, this.$slots.default)
    }
}

// view.js
export default {
    render(h) {
        let component = null
        // 寻找到具有相同路径的route
        const route = this.$router.$options.routes.find(route => route.path === this.$router.current)
        if (route) component = route.component
        // h函数也可以直接接受组件并且渲染
        return h(component)
    }
}

完成以上功能,单层路由的简单跳转就可以实现了,接下来进行嵌套路由跳转的实现


首先,我们需要实现一个新的方法 createMatcher

// create-matcher.js
// 入参: 路由表 返回值: 返回当前路由的所有层级的match函数
const createMatcher = (routes) => {
    // pathMap: 一个含有所有路由的信息表,存储方式 {'/': {...路由信息}, '/about': {...}}
    const pathMap = createRouteMap(routes)
    const match = (path) => {
        return createRoute(pathMap[path])
    }
    return { match }
}

// 返回当前路由的自身与祖先路由信息 如: /about/detail matched: [{/about 的路由信息}, {/about/detail的路由信息}]
function createRoute (route) {
    const matched = []
    while (route) {
        matched.unshift(route)
        route = route.parent
    }
    return matched
}

// 生成路由信息表
function createRouteMap (routes, pathMap = {}) {
    addRouteRecord(routes, pathMap)
    return pathMap
}

// 路由信息表的具体生成方式
function addRouteRecord (routes, pathMap, parent) {
    routes.forEach(route => {
        const { path, children, ...rest } = route
        const normalizedPath = parent ? parent.path + '/' + path : path
        pathMap[normalizedPath] = { ...rest, path: normalizedPath, parent }
        if (children) addRouteRecord(children, pathMap, route)
    })
}

export default createMatcher

由上,我们可得到一个路由信息表,以此重构我们的vue-router

// vue-router.js
class VueRouter {
    constructor(options) {
        // 生成路由信息表,并且获得match函数
        this.matcher = createMatcher(options.routes)
        // 原先的单个路由现在改为自身加祖先的所有路由信息,因此换为数组形式
        Vue.util.defineReactive(this, 'matched', [])
        // 获取当前的路由信息
        this.matched = this.match(this.getHash())
        // 监听路由变化
        window.addEventListener('hashchange', () => {
            this.matched = this.match(this.getHash())
        })
    }
    getHash () {
        return window.location.hash.slice(1) || '/'
    }
    // 获取当前路由的所有路由信息
    match (path) {
        return this.matcher.match(path)
    }
}

最后再重构一下router-view

// view.js
export default {
    render (h) {
    // 新增routerView属性,若为true,即为已渲染
    this.$vnode.data.routerView = true
    let component = null
    // 新增depth,判断当前路由为第几层级
    let depth = 0
    let parent = this.$parent
    // 循环获取层级
    while (parent) {
        if (parent.$vnode && parent.$vnode.data && parent.$vnode.data.routerView) depth++
        parent = parent.$parent
    }
    // 得到当前的所有路由信息
    const { matched } = this.$router
    // 获取当前层级的路由信息并渲染
    const route = matched[depth]
    if (route) component = route.component
    return h(component)
    }
}
以上就是手写简版vue-router的所有内容啦.实现了以下内容:
1. 实现了插件的安装,包括VueRouter类以及install方法
2. 其中VueRouter负责响应式的生成当前路由信息
3. install方法中,进行了$router的注册以及router-link, router-view两个全局组件的添加
4. link负责控制路由的变化,view根据路由信息找到相应的组件并渲染