vue源码之vue-router基本实现

176 阅读1分钟

vue-router

目标:

  • 实现⼀个插件
    • 实现VueRouter类
      • 处理路由选项
      • 监控url变化,hashchange
      • 响应这个变化
    • 实现install⽅法
      • $router注册
      • 两个全局组件

需求分析:

  • spa ⻚⾯不能刷新
    • hash #/about
    • History api /about
  • 根据url显示对应的内容
    • router-view
    • 数据响应式:current变量持有url地址,⼀旦变化,动态重新执⾏render

实现⼀个插件:创建VueRouter类和install⽅法

创建my-router.js

import LINK from './my-router-link'
import VIEW from './my-router-view'

let Vue
class VueRouter {
    // 保存选项
    constructor(options) {
        this.$options = options
}
// 插件:实现install方法,注册$router
VueRouter.install = function (_Vue) {
    Vue = _Vue
    // 挂载$router
    Vue.mixin({
        beforeCreate() {
            if (this.$options.router) {
                Vue.prototype.$router = this.$options.router
            }
        }
    })
    // 实现两个全局组件router-link和router-view
    Vue.component('router-view', VIEW)
    Vue.component('router-link', LINK)
}

export default VueRouter
//
let Vue

class VueRouter {
    constructor(options) {
        this.$options = options
    }
}

为什么要用混入方式写?主要原因是use代码前,router实例创建在后,而install逻辑又需要用到该实例

创建router-view和router-link

创建my-router-link.js

export default {
    props: {
        to: String,
        required: true
    },
    render(h) {
        return h('a', {
            attrs: {
                href: '#' + this.to
            }
        }, [
            this.$slots.default
        ])
    }
}

创建my-router-view.js

export default {
    render(h) {
        //暂不渲染
        return h(null)
    }
}

监控url变化

import LINK from './my-router-link'
import VIEW from './my-router-view'
let Vue
class VueRouter {
    constructor(options) {
        // 定义响应式的属性current
        const initial = window.location.hash.slice(1) || '/'
        this.current = window.location.hash.slice(1) || '/'
        Vue.util.defineReactive(this, 'matched', [])
        // 监听hashChange事件
        window.addEventListener('hashchange', this.onHashChange.bind(this))
        window.addEventListener('load', this.onHashChange.bind(this))
    }
    onHashChange() {
        this.current = window.location.hash.slice(1)
    }
}

动态获取对应组件 my-router-view.js

export default {
    render(h) {
        let component = null
        const route = this.$router.$options.routes.find(route => route.path == this.$router.current)
        if (route) component = route.component
        return h(component)
    }
}

提前处理路由表,避免每次都循环

class VueRouter() {
    constructor(options) {
        // 缓存path和route映射关系
        this.routeMap = {}
        this.$options.routes.forEach(route => {
            this.routeMap[route.path] = route
        })
        this.current = window.location.hash.slice(1) || '/'
    }
}

my-router-view.js

export default {
    render(h) {
        let component = null
        // const route = this.$router.matched[depth]
        const { routeMap, current } = this.$router
        const route = routeMap[current]
        return h(component)
    }
}

子路由多层嵌套

标记当前router-view的depth(my-router-view.js)

export default {
    render(h) {
        this.$vnode.data.routerView = true
        let depth = 0
        let parent = this.$parent
        while(parent) {
            const vnodeData = parent.$vnode && parent.$vnode.data
            if (vnodeData) {
                if (vnodeData.routerView === true) {
                    depth++
                }
            }
            parent = parent.$parent
        }
        const component = this.$route.matched[depth] ? this.$rout.matched[depth].component : null
        return h(component)
    }
}

my-router.js

class VueRouter {
    constructor(options) {
        // 缓存path和route映射关系
        // this.routeMap = {}
        // this.$options.routes.forEach(route => {
        //     this.routeMap[route.path] = route
        // })
        this.current = window.location.hash.slice(1) || '/'
        Vue.util.defineReactive(this, 'matched', [])
        this.match()
    }
    onHashChange() {
        this.current = window.location.hash.slice(1)
        this.matched = []
        this.match()
    }
    match(routes) {
        routes = routes || this.$options.routes
        for (const route of routes) {
            // 首页不会有子路由配置项
            if (route.path === '/' && this.current === '/') {
                this.matched.push(route)
                return
            }
            if (route.path !== '/' && this.current.indexOf(route.path) != -1) {
                this.matched.push(route)
                if (route.children) {
                    this.match(route.children)
                }
                return
            }
        }
    }
}

源码github地址