乐趣区

vue轮子系列手写一个mini-vuerouter

mini vue router 要干啥

  1. 实现一个插件:包含有 VueRouter class & 必须的 install 方法;
  2. 实现 2 个全局组件:router-link & router-view;
  3. 监听 url 变化,实现 hash 模式进行跳转;
  4. 实现嵌套子路由跳转显示;

开始实现

插件实现

使用 vue-router 时,我们使用 Vue.use(VueRouter)来注册路由,并且通过 new VueRouter(routes)接收路由配置,构造路由器实例 router 并挂载到根实例选项中,使得所有组件内都可以通过:this.$router访问路由器

let Vue
// 保存路由配置选项
class VueRouter {// new VueRouter(routes)
    constructor(options) {
        this.$options = options
        // todo 
    }
}

VueRouter.install = function (_Vue) { //
    Vue = _Vue // 接受宿主环境的 Vue
    Vue.mixin({beforeCreate() {
            // 将根组件上的 router 实例挂载到 Vue 原型,那么所有的组件实例上都会有 $router
            if (this.$options.router) {Vue.prototype.$router = this.$options.router}
        },
    })
    // todo
}

export default VueRouter

实现 router-link

本轮子使用 hash 模式,因此 url 中会带有 # 号。
router-link 组件目的是点击进行跳转,可以将思路简化为:
<router-link to="/about">About</router-link> => <a href={'#'+this.to}>{this.$slots.default}</a>
上代码:

    const Link = Vue.extend({
        props: {
            to: {
                type: String,
                required: true
            }
        },
        render(h) {
            return h('a', {
                attrs: {href: '#' + this.to}
            }, [this.$slots.default]
            )
        }
    })
    // 注册 router-link
    Vue.component('router-link', Link)

监听 url 变化并实现 router-view 响应视图

  1. 通过监听 hashchange 事件, 获取到更改后的 URL 标识符,并在路由器中创建响应式的 matched 数组记录 URL 变化匹配到的路由;
  2. 深度标记每一个 router-view 组件,然后从路由器的 matched 数组中根据当前访问的深度(depth)取出要显示的 component,在当前 router-view 中渲染。思路如下图:


代码实现如下:

 constructor(options) {
        // 响应式的 matched 按深度存放路由配置
        Vue.util.defineReactive(this, 'matched', [])

        // this.current 记录的当前的 URL 标识符
        const initPath = window.location.hash.slice(1) || '/'
        Vue.util.defineReactive(this, 'current', initPath)

        // 监听 URL 变化
        window.addEventListener('hashchange', this.onHashChange.bind(this))
        // page 加载时也要匹配当前 URL 需要 render 的组件
        window.addEventListener('load', this.onHashChange.bind(this))
        this.match()}
    onHashChange() {this.current = window.location.hash.slice(1) || '/'
        this.matched = []
        this.match()}
    match(routes) {
        routes = routes || this.$options.routes
        // 递归遍历记录当前 URL 下所有命中的 route
        for (const route of routes) {if (route.path === '/' && this.current === '/') { // 首页
                this.matched.push(route)
                return
            }
            // about/info
            if (route.path !== '/' && ~this.current.indexOf(route.path)) {this.matched.push(route)
                if (route.children) {this.match(route.children)
                }
                return
            }
        }
    }
    // 1. 标记深度
    const View = Vue.extend({render(h) {
            this.$vnode.data.routerView = true // 标记当前组件是 router-view
                let depth = 0
                // 递归确认 当前 router-view 在组件树中的深度
                let parent = this.$parent
                while (parent) {
                    const vnodeData = parent.$vnode && parent.$vnode.data
                    if (vnodeData) {if (vnodeData.routerView) { // 说明是一个 router-view
                            ++depth
                        }
                    }
                    parent = parent.$parent
                }
                let component = null
                const {matched} = this.$router
                if(matched[depth]){component = matched[depth].component
                }
                console.log('当前深度:',depth);
                console.log('当前 matched:',this.$router.matched);
                return h(component)
           
        }
    })

    Vue.component('router-view', View)

至此,一个简单的 mini vue-router 就实现了,我们可以像使用官方的 vue-router 一样,引入我们自己的 mini-vue-router 在项目中使用。
当然,官方库要远远复杂于本轮子,本轮子旨在理解 vue-router 核心思想,更深层次研究请阅读官方源码【》》传送门】
本轮子实现效果如下:

扩展与思考

  1. 如何实现路由守卫
  2. 如何实现路由缓存
  3. 如何实现 history 模式
  4. 如何实现路由懒加载

附注

本轮子的完整代码:

// 1. 是一个插件 有 VueRouter class 以及 install 方法
// 2. new VueRouter(options) 一个实例 挂载到 根实例上 并且所有组件能通过 this.$router  访问 router 实例
// 3. router-link router-view 两个全局组件 router-link 跳转,router-view 显示内容
// 4. 监听 url 变化 监听 haschange || popstate 事件
// 5. 响应最新的 url:创建一个响应式的属性 current 当它改变的时候获取对应的组件并显示
// 6. 子组件 深度标记 与 macth()
let Vue
// 保存路由配置选项
class VueRouter {// new VueRouter(routes)
    constructor(options) {
        this.$options = options
        // todo 缓存一个路由映射表

        // 响应式的 matched 按深度存放路由配置
        Vue.util.defineReactive(this, 'matched', [])

        // this.current 记录的当前的 URL 标识符
        const initPath = window.location.hash.slice(1) || '/'
        Vue.util.defineReactive(this, 'current', initPath)

        // 监听 URL 变化
        window.addEventListener('hashchange', this.onHashChange.bind(this))
        // page 加载时也要匹配当前 URL 需要 render 的组件
        window.addEventListener('load', this.onHashChange.bind(this))
        this.match()}
    onHashChange() {this.current = window.location.hash.slice(1) || '/'
        this.matched = []
        this.match()}
    match(routes) {
        routes = routes || this.$options.routes
        // 递归遍历记录当前 URL 下所有命中的 route
        for (const route of routes) {if (route.path === '/' && this.current === '/') { // 首页
                this.matched.push(route)
                return
            }
            // about/info
            if (route.path !== '/' && ~this.current.indexOf(route.path)) {this.matched.push(route)
                if (route.children) {this.match(route.children)
                }
                return
            }
        }
    }
}

VueRouter.install = function (_Vue) { //
    Vue = _Vue // 接受宿主环境的 Vue
    Vue.mixin({beforeCreate() {
            // 将根组件上的 router 实例挂载到 Vue 实例原型,那么所有的 组件实例上都会有 $router
            if (this.$options.router) {Vue.prototype.$router = this.$options.router}
        },
    })

    const Link = Vue.extend({
        props: {
            to: {
                type: String,
                required: true
            }
        },
        render(h) {
            return h('a', {
                attrs: {href: '#' + this.to}
            }, [this.$slots.default]
            )
        }
    })

    // 1. 标记深度
    const View = Vue.extend({render(h) {
            this.$vnode.data.routerView = true // 标记当前组件是 router-view
            let depth = 0
            // 递归确认 当前 router-view 在组件树中的深度
            let parent = this.$parent
            while (parent) {
                const vnodeData = parent.$vnode && parent.$vnode.data
                if (vnodeData) {if (vnodeData.routerView) { // 说明是一个 router-view
                        ++depth
                    }
                }
                parent = parent.$parent
            }
            let component = null
            const {matched} = this.$router
            if (matched[depth]) {component = matched[depth].component
            }
            console.log('当前深度:', depth);
            console.log('当前 matched:', this.$router.matched);
            return h(component)

        }
    })

    Vue.component('router-link', Link)
    Vue.component('router-view', View)
}

export default VueRouter
退出移动版