手写Vue系列(二)—VueRouter

188 阅读4分钟

前言

Vue RouterVue.js官方路由管理器。它和Vue.js的核心深度集成,让构建单页面应用变得易如反掌。包含的功能有:

  • 嵌套的路由/视图表
  • 模块化的、基于组件的路由配置
  • 路由参数、查询、通配符
  • 基于Vue.js过渡系统的视图过渡效果
  • 细粒度的导航控制
  • 带有自动激活的CSS class的链接
  • HTML5历史模式或hash模式,在IE9中自动降级
  • 自定义的滚动条模式

vue-router源码目录结构

├── components  // 组件
│   ├── link.js   // route-link的实现
│   └── view.js   // route-view的实现
├── create-matcher.js  // 创建匹配
├── create-route-map.js  // 创建路由的映射
├── history  // 操作浏览器记录的一系列内容
│   ├── abstract.js  // 非浏览器的history
│   ├── base.js    // 基本的history
│   ├── hash.js    // hash模式的history
│   └── html5.js   // html5模式的history
├── index.js   // 入口文件
├── install.js  // 插件安装的方法
└── util   // 工具类库
    ├── async.js    // 异步操作的工具库
    ├── dom.js    // dom相关的函数
    ├── location.js     // 对location的处理
    ├── misc.js     // 一个工具方法
    ├── params.js   // 处理参数
    ├── path.js     // 处理路径
    ├── push-state.js  // 处理html模式的 pushState
    ├── query.js  //对query的处理
    ├── resolve-components.js  //异步加载组件
    ├── route.js  // 路由
    ├── scroll.js  //处理滚动
    └── warn.js  // 打印一些警告

基本使用

import Vue from 'vue'
import VueRouter from 'vue-router'
// 1. 定义 (路由) 组件。
import VueRouter from './quick'
import Home from '../views/Home.vue'
import About from '../views/About.vue'
import Detail from '../views/Detail.vue'

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

// 2. 定义路由(配置)
const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    component: About,
    children: [
      {
        path: 'detail/:id',
        component: Detail
      }
    ]
  }
]

// 3. 创建 router 实例,然后传 `routes` 配置
const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

// 4. 创建和挂载根实例。
const app = new Vue({
  router
}).$mount('#app')
<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link>
    </div>
    <router-view/>
  </div>
</template>

实现目标

  • 导航功能
  • 视图渲染功能
  • 支持router-view的嵌套功能

分析实现

从上诉来分析vue-router的基本实现,主要可以概括为3部分:

  • VueRouter
  • router-view组件
  • router-link组件

在实现上诉3大件之前,还需要分析一件事:
在我们使用Vue-Router时,会发现页面地址变化,但是并没有刷新界面,对于这种的实现有2种模式:

  • hash模式
    1. 通过地址栏URL中的#后地址的改变
    2. 监听windows上的onhashchange事件
  • history模板
    1. 通过HTML5 Historyapi的操作
    2. 监听windows上的onpopstate事件

实现VueRouter

VueRouter类主要做几件事:

  1. 实现静态install方法(因为是插件)
  2. 加载router配置
  3. 监听url变化事件
    1. 通过path-to-regexpurl解析成格式化的route对象
    2. 收集成功匹配当前pathroutes配置数组matched

实现静态install方法

let _Vue
class QuickRouter {

    constructor(options) {
        this.options = options
        this.route = Object.create(null)
    }
}

QuickRouter.install = (Vue) => {
    // 防止多次install
    if (QuickRouter.installed) return

    QuickRouter.installed = true

    // 保存外部传入的Vue
    _Vue = Vue

    /**
     * 全局混入beforeCreate方法
     * 延迟挂载$router对象
     * 因为先执行了use(Router),而这时router对象还未产生
     */
    Vue.mixin({
        beforeCreate() {
            if (this.$options.router) {
                Vue.prototype.$router = this.$options.router
                Vue.prototype.$routerRoot = this.$root
                Vue.prototype.$route = this.$options.router.route

                // 响应式route
                Vue.util.defineReactive(this, 'route', this.$options.router.route)
            }
        }
    })

    // 注册全局组件
    Vue.component('router-link', {
        render(h) {
            return <a>link</a>
        }
    })
    Vue.component('router-view', {
        render(h) {
            return <div>view</div>
        }
    })
}

由于Vue.use注册在前,所以需要延迟待router对象产生后,再借用mixinbeforeCreate进行挂载。

加载router配置

按层级的方式,将routes转换为map方便后面获取对应path的组件

let _Vue

class QuickRouter {

    constructor(options) {
        this.options = options
        this.routesMap = Object.create(null)
        this.loadRoutes()
    }

    // 加载routes,并将转换为map
    loadRoutes() {
        const routes = this.options.routes
        routes.forEach(route => {
            const fullPath = route.path
            this.routesMap[fullPath] = route
            this._recRoutes(route, fullPath)
        })
    }

    // 递归加载route
    _recRoutes(route, path = '') {
        if (!route.children || !route.children.length) {
            return
        }

        const routes = route.children
        routes.forEach(item => {
            item.parent = route // 记录父级,为以后view做层级渲染
            const fullPath = path + '/' + item.path
            this.routesMap[fullPath] = item
            this._recRoutes(item, fullPath)
        })
    }

}

获取path

在获取path时,首先要获取浏览器对应的url地址,而hashhistory模式对应不同的获取方式,这里将其统一封装到getPath函数中

    /**
     * 获取path
     * todo 这里的获取path还不全面,query后面的参数未获取
     */
    getPath() {
        const mode = this.options.mode
        if (mode === 'hash') {
            // 这里通过window.localtion.hash来获取并不准确,会有浏览器兼容性问题
            // 更好的做法是通过window.localtion.href来手动截取
            return '/' + window.localtion.hash
        }

        return window.location.pathname
    }

官方在处理浏览器url相关时,通过抽象出HashHistoryHtml5History类继承了顶级History类来实现,会更加合理。这里暂时先不做处理

监听url事件

    constructor(options) {
        // ...

        this.route = Object.create(null) // 当前route
        this.depth = 0 // 记录router-view层级深度
        this.listen() // 监听路由改变事件
        this.path = this.getPath() || '/' // 浏览器的有效path

        this.createRoute()// 需主动调用一次,获取route,因为此时的listen事件已过时失效,
    }

    // 监听路由改变事件
    listen() {
        const eventType = this.options.mode === 'history' ? 'popstate' : 'hashchange'
        window.addEventListener(eventType, this.handleRoutingEvent.bind(this))
        window.addEventListener('load', this.handleRoutingEvent.bind(this))
    }

    // 事件处理句柄
    handleRoutingEvent() {
        this.depth = 0 // 重置depth
        this.createRoute()// 解析path
    }

    /**
     * 解析path为route对象
     */
    createRoute() {
        Object.keys(this.routesMap).forEach(path => {
            if (path) {
                const matched = match(path)(this.path);
                if (matched) {
                    this.route = matched
                    this.route.realPath = path
                    this.route.matched = this._collectMatcted(this.routesMap[path])
                }
            }
        })
    }

最终VueRouter类

import { match } from "path-to-regexp"

let _Vue

class QuickRouter {

    constructor(options) {
        this.options = options
        this.routesMap = Object.create(null) // 路由配置map
        this.loadRoutes() // 加载路由配置

        this.route = Object.create(null) // 当前route
        this.depth = 0 // 记录router-view层级深度
        this.listen() // 监听路由改变事件
        this.path = this.getPath() || '/' // 浏览器的有效path

        this.createRoute()// 需主动调用一次,因为此时的listen已失效,获取route
    }


    // 加载routes,并将转换为map
    loadRoutes() {
        const routes = this.options.routes
        routes.forEach(route => {
            const path = route.path
            this.routesMap[path] = route
            this._recRoutes(route, path, 0)
        })
    }

    // 递归加载route
    _recRoutes(route, path = '') {
        if (!route.children || !route.children.length) {
            return
        }

        const routes = route.children
        routes.forEach(item => {
            item.parent = route // 记录父级,为以后view做层级渲染
            const fullPath = path + '/' + item.path
            this.routesMap[fullPath] = item
            this._recRoutes(item, fullPath)
        })
    }

    // 监听路由改变事件
    listen() {
        const eventType = this.options.mode === 'history' ? 'popstate' : 'hashchange'
        window.addEventListener(eventType, this.handleRoutingEvent.bind(this))
        window.addEventListener('load', this.handleRoutingEvent.bind(this))
    }

    // 事件处理句柄
    handleRoutingEvent() {
        this.depth = 0 // 重置depth
        this.createRoute()// 解析path
    }

    /**
     * 获取path
     * todo 这里的获取path还不全面,query后面的参数未获取
     */
    getPath() {
        const mode = this.options.mode
        if (mode === 'hash') {
            // 这里通过window.localtion.hash来获取并不准确,会有浏览器兼容性问题
            // 更好的做法是通过window.localtion.href来手动截取
            return '/' + window.localtion.hash
        }

        return window.location.pathname
    }

    /**
     * 解析path为route对象
     */
    createRoute() {
        Object.keys(this.routesMap).forEach(path => {
            if (path) {
                const matched = match(path)(this.path);
                if (matched) {
                    this.route = matched
                    this.route.realPath = path
                    this.route.matched = this._collectMatcted(this.routesMap[path])
                }
            }
        })
    }

    /**
     * 收集matched记录,包括父级
     */
    _collectMatcted(record) {
        const ret = []

        while (record) {
            // 从父->子的顺序放置
            ret.unshift(record)
            record = record.parent
        }
        return ret

    }
}

实现link view组件

其中link view组件都用函数式组件来实现,以提高性能。

    // link组件
    Vue.component('router-link', {
        functional: true,

        render(h, { props, children }) {
            return h('a',
                {
                    attrs: {
                        href: props.to
                    }
                }, children)
        }
    })

    // view组件
    Vue.component('router-view', {
        functional: true,

        render(h, { parent }) {

            // 函数式组件,通过parent获取$router实例
            const route = parent.$router.route

            /**
             * 通过depth获取当前层级对应的comp。
             */
            const comp = route.matched[parent.$router.depth]
            if (comp && comp.component) {
                // 每渲染一个routerview,depth++
                parent.$router.depth++
                // 渲染组件
                return h(comp.component)
            }

            return h()
        }
    })

对于如何找到正确的depth位置,官方是通过赋值data.routerView,然后每次遍历其parent上的data是否有routerView属性来判断其是否是routerView组件。

最终效果

代码