Vue-Router原理剖析之hash模式(一)

一、起步需求分析

从添加router插件开始,在执行了vue add router命令后,项目目录中会增加一个router目录并在main.js中导入router选项。

先从router目录下的index.js开始:

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

// 注册VueRouter插件
Vue.use(VueRouter)

const routes = [
  {
     
    path: '/',
    name: 'Home',
    component: Home
  },
  {
     
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
]

const router = new VueRouter({
     
  routes
})

// 导出router实例
export default router

从router的index.js文件可以看出,这里做了三件事:

  1. 注册了VueRouter插件
  2. 实例化了一个VueRouter类并将route配置传入
  3. 导出VueRouter实例

在main.js文件中,vue-rotuer的修改:

  1. 导入router目录下的index.js
  2. 在Vue实例化时将VueRouter实例当成选项传入

表面上Vue-Router就做了这些事情,但在我们平常开发时,我们可以在每个组件中访问$router,而在表面上我们并没有发现Vue-Router在main.js中将$router挂载到Vue.prototype上,那说明**$router是在Vue-Router内部挂载**的。

还有一个就是我们平时在任意组件中都可以使用组件,而这并不是Vue自身的组件,说明Vue-Router还声明了两个全局组件

所以这里可以转换成Vue-Router实现的起步需求就是实现一个Vue插件,该插件起步的功能有:

  • 挂载$rotuer
  • 声明两个全局组件router-linkrouter-view
  • 实现一个VueRouter类,实现hash模式下路由跳转

二、需求实现

先创建一个vue-router.js文件,在文件中创建一个VueRouter类

2.1 实现挂载$router

挂载$router我们很容易想到在install方法去挂载,但这里有个问题,就是在执行install方法时,Vue还未实例化,这里并不能拿到router实例,Vue-Router的解析方法是在执行install方法时全局混入beforeCreate生命周期方法,等这个生命周期方法执行时,Vue是已经实例化了,就可以拿到router实例了。

新建vue-router.js

class VueRouter {
     
    
}

// Vue插件需要声明一个install方法
VueRouter.install = function (Vue) {
     
    // 全局混入beforeCreate生命周期方法
    Vue.mixin({
     
        beforeCreate() {
     
            // 只要在Vue根实例中挂载,利用只有根实例选项上有router属性找到根实例
            if (this.$options.router) {
     
                Vue.prototype.$router = this.$options.router
            }
        }
    })
}

2.2 声明两个全局组件

首先,我们要清楚在hash模式下,组件到底有什么用。

<router-link to="/about">Aboutrouter-link>

<router-view>router-view>

组件接收一个to属性,并将标签中内容渲染,从浏览器中可以看出其实就是一个a标签,to属性改变的是a标签的href属性。

组件的功能是在路由发生改变时,将对应路由的组件渲染到其中。

2.2.1 组件

新建link.js

// 导出router-link组件配置对象
export default {
     
    name: 'router-link',
    props: {
     
        to: {
     
            type: String,
            default: ''
        }
    },
    render (h) {
     
        // 利用渲染函数,生成一个a标签,href属性值如 /#/about,将默认插槽作为子元素
        return h('a', {
     attrs: {
     href: '#' + this.to}}, this.$slots.default)
    }
}

2.2.2 组件

新建view.js

// 导出router-view组件配置对象
export default {
     
    name: 'router-view',
    render (h) {
     
        // component存放当前路由对应的组件
        let component = null
        
        return h(component)
    }
}

2.3 完善VueRouter类,实现hash模式下路由跳转

因为这是是用hash值的改变来实现地址修改,但其实页面并不会刷新,这里就需要手动去触发渲染

vue-router.js完善

import Link from './link'
import View from './view'

let Vue = null

class VueRouter {
     
    constructor (options) {
     
        // 保存路由选项
        this.$options = options
        
        // 声明一个响应式属性,存放当前路由路径
        Vue.util.defineReactive(this, 'currentPath', '/')
        
        // 监听初始化和hash值变化时,改变当前路由路径
        window.addEventListener('hashchange', this.onHashChange.bind(this))
        window.addEventListener('load', this.onHashChange.bind(this))
        
        // 创建路由map,保存路由表
        this.routeMap = {
     }
        this.$options.routes.forEach(route => {
     
            this.routeMap[route.path] = route
        })
    }
    
    onHashChange () {
     
        // 这里只是先简单将 # 后的路径赋值给currentPath
        this.currentPath = window.location.hash.slice(1)
    }
}

// Vue插件需要声明一个install方法
VueRouter.install = function (_Vue) {
     
    // 保存Vue引用
    Vue = _Vue
    // 全局混入beforeCreate生命周期方法
    Vue.mixin({
     
        beforeCreate() {
     
            // 只要在Vue根实例中挂载,利用只有根实例选项上有router属性找到根实例
            if (this.$options.router) {
     
                Vue.prototype.$router = this.$options.router
            }
        }
    })
    
    // 注册全局组件
    Vue.component('router-link', Link)
    Vue.component('router-view', View)
}

完善view.js

// 导出router-view组件配置对象
export default {
     
    name: 'router-view',
    render (h) {
     
        // component存放当前路由对应的组件
        let component = null
        
        // 因为currentPath是响应式数据,在当前组件的render方法中使用该属性后,当currentPath改变,render会重新执行
        // 根据$router上的currentPath的变化,动态匹配组件
        const {
     routeMap, currentPath} = this.$router
        component = routeMap[currentPath] ? routeMap[currentPath].component : null
        
        return h(component)
    }
}

三、实现嵌套路由

嵌套路由在路由配置时多了children数组,在父路由组件中需要添加组件。

实现嵌套路由的原理就是:

  1. 把匹配hash路径的路由及子路由push到matched数组中
  2. 利用当前匹配的组件的深度得到depth,即可在matched中匹配到对应的路由

vue-router.js修改

import Link from './link'
import View from './view'

let Vue = null

class VueRouter {
     
    constructor (options) {
     
        // 保存路由选项
        this.$options = options
        
        // 声明一个响应式数据,存放当前路由路径
        // Vue.util.defineReactive(this, 'currentPath', '/')
        
        // 将matched声明为一个响应式数据,存放当前匹配的路由数组
        Vue.util.defineReactive(this, 'matched', [])
        
        // 监听初始化和hash值变化时,改变当前路由路径
        window.addEventListener('hashchange', this.onHashChange.bind(this))
        window.addEventListener('load', this.onHashChange.bind(this))
        
        // 创建路由map,保存路由表
        // this.routeMap = {}
        // this.$options.routes.forEach(route => {
     
        //    this.routeMap[route.path] = route
        // })
    }
    
    onHashChange () {
     
        // 这里只是先简单将 # 后的路径赋值给currentPath
        // this.currentPath = window.location.hash.slice(1)
        
        // 获取当前hash路径
        this.current = window.location.hash.slice(1)
        
        this.matched = []
        // 匹配hash路径对应的路由
        this.match()
    }
    
    match (routes) {
     
        routes = routes || this.$options.routes
        
        // 遍历路由选项
        for (let 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
            }
        }
    }
}

// Vue插件需要声明一个install方法
VueRouter.install = function (_Vue) {
     
    // 保存Vue引用
    Vue = _Vue
    // 全局混入beforeCreate生命周期方法
    Vue.mixin({
     
        beforeCreate() {
     
            // 只要在Vue根实例中挂载,利用只有根实例选项上有router属性找到根实例
            if (this.$options.router) {
     
                Vue.prototype.$router = this.$options.router
            }
        }
    })
    
    // 注册全局组件
    Vue.component('router-link', Link)
    Vue.component('router-view', View)
}

view.js修改

// 导出router-view组件配置对象
export default {
     
    name: 'router-view',
    render (h) {
     
        // 标记当前组件为routerView
        this.$vnode.data.routerView = true
        
        // component存放当前路由对应的组件
        let component = null
        // 当前router-view组件的深度
        let depth = 0
        let parent = this.$parent
        
        // 向上遍历是否还存在router-view组件,得到当前router-view的深度
        while(parent) {
     
            if (parent.$vnode.data && parent.$vnode.data.routerView) {
     
                depth++
            }  
            parent = parent.$parent
        }
        
        // 取出匹配路由的组件
        const matched = this.$router.matched[depth]
        component = matched && matched.component
        
        
        // 因为currentPath是响应式数据,在当前组件的render方法中使用该属性后,当currentPath改变,render会重新执行
        // 根据$router上的currentPath的变化,动态匹配组件
        // const {routeMap, currentPath} = this.$router
        // component = routeMap[currentPath] ? routeMap[currentPath].component : null
        
        return h(component)
    }
}

这里的实现并没有像官方一样使用函数式组件

你可能感兴趣的:(vue,vue-router,原理,vue,js)