【VueRouter 源码学习】第四篇 - 创建路由映射表

452 阅读7分钟

这是我参与8月更文挑战的第20天,活动详情查看:8月更文挑战


一,前言

上篇,介绍路由插件 install 的实现,主要包含以下内容:

  • 创建 vue-router 目录结构;
  • 插件引入入口 index.js 逻辑实现;
  • 插件安装入口 install.js 逻辑实现;
  • 为所有组件混入 router 实例;

本篇,创建路由映射表;


二,承上启下

1,前文回顾

  • 完成了路由插件的配置、注册,并导出 router 路由实例;
  • 在 Vue 初始化时,将 router 路由实例注册到了 Vue 实例上(即根组件Vue.app);
  • 自建 vue-router 插件目录结构:导入入口 index.js,安装逻辑 install.js;
  • 执行 Vue.use 安装插件时,通过 Vue.mixin 向所有组件混入导出的 router 实例;
  • 备注:在组件创建前,通过全局混入 beforeCreate 生命周期函数,将 router 实例从根组件逐层地共享给其他子组件;

至此,完成了插件的创建:创建了插件导入入口和 install 安装方法;

2,本篇介绍

但是,目前还只是一个插件的“空壳”,引入插件后并没有进行插件功能的初始化;

所以,需要在插件安装时执行的 install 方法中,完成插件的初始化操作;

路由插件初始化的主要逻辑如下:

  • 在 VueRouter 实例化时,接收外部传入的路由配置 options:new Router(options),构造函数内部通过 createMatcher 路由匹配器对其进行处理;
  • 在路由匹配器 createMatcher 中,将路由配置处理为便于匹配的扁平化结构对象 matcher;
  • 在路由匹配器中创建路由插件的两个核心方法:match 和 addRoutes;
  • 在 VueRouter 类中,创建路由初始化方法 init,当执行Vue.use安装路由插件在 install 方法中处理根组件时,执行路由的 init 初始化操作,将根实例 Vue.app 传入 init 方法中;

三,路由插件的初始化

1,路由实例化时的初始化逻辑

new VueRouter进行路由的初始化时,在构造函数中需要完成以下几件事:

  • 创建路由匹配器 createMatcher 处理路由配置:将嵌套数组处理为扁平对象,便于路由匹配操作;
  • 创建 match 方法,用于通过路由规则匹配到对应的组件;
  • 创建 addRoutes 方法,用于动态的添加路由匹配规则;

2,路由安装时的初始化逻辑 init

// index.js

import install from './install'

class VueRouter {
    constructor(options) {  // 传入配置对象 options

    }
    // 路由初始化方法,供 install 安装时调用
    init(app) {
            
    }
}
VueRouter.install = install;

export default VueRouter;

在根组件中,调用路由实例上的 init 方法,完成插件的初始化:

// install.js

export let _Vue;

/**
 * 插件安装入口 install 逻辑
 * @param {*} Vue     Vue 的构造函数
 * @param {*} options 插件的选项
 */
export default function install(Vue, options) {
  _Vue = Vue;// 抛出 Vue 供其他文件使用
  
  // 混入:所有组件都能够通过 this._routerRoot._router 获取到同一个 router 实例;
  Vue.mixin({
    beforeCreate() {
      if (this.$options.router) {// 根组件
        this._routerRoot = this;
        this._router = this.$options.router; 
        // 在根组件中,调用路由实例上的 init 方法,完成插件的初始化
        this._router.init(this); // this 为根实例
      } else { // 子组件
        this._routerRoot = this.$parent && this.$parent._routerRoot;
      }
    }
  });
}

四,路由匹配器函数 createMatcher

1,创建路由匹配器函数

新建 create-match.js 文件,创建路由匹配器函数 createMatcher:

// create-match.js

/**
 * 路由匹配器函数
 *  对路由配置进行扁平化处理
 *  addRoutes:动态添加路由匹配规则
 *  match:根据路径进行路由匹配
 * @param {*} routes 
 * @returns 返回路由匹配器的两个核心方法 addRoutes、match
 */
export default function createMatcher(routes) {
    // 将嵌套数组的路由配置,处理为便于匹配的扁平结构
    // 创建 match 方法:根据路径进行路由匹配
    // 创建 addRoutes 方法:动态添加路由匹配规则
}

VueRouter构造函数中,使用路由匹配器函数 createMatcher,将嵌套数组类型的路由配置进行扁平化处理:

// index.js

import install from './install'
import createMatcher from './create-matcher';  // 导入匹配器

class VueRouter {
    constructor(options) {
        // 路由匹配器-处理路由配置:将树形结构的嵌套数组转化为扁平化结构的对象,便于后续的路由匹配
        // 路由匹配器返回两个核心方法:match、addRoutes
        this.matcher = createMatcher(options.routes || []);// options.routes 默认[]
    }
    
    /**
     * 路由初始化方法,供 install 安装时调用
     * @param {*} app 根组件实例(在处理根组件时被调用)
     */
    init(app) { 
            
    }
}

VueRouter.install = install;

export default VueRouter;

处理完成后,路由实例上的 matcher 属性,将存储扁平化结构的全部路由匹配规则,用于后续路由匹配操作;

2,路由配置的扁平化处理

在 create-route-map.js 文件中,处理路由配置:

// create-route-map.js

/**
 * 路由配置扁平化处理
 *  支持初始化和追加两种情况
 * @param {*} routes     路由实例中的路由配置
 * @param {*} oldPathMap 路由规则映射表(扁平化结构)
 * @returns 新的路由规则映射表(扁平化结构)
 */
export default function createRouteMap(routes, oldPathMap){

  // 拿到当前已有的映射关系
  let pathMap = oldPathMap || Object.create(null);

  // 将路由配置 routes 依次加入到 pathMap 路由规则的扁平化映射表
  routes.forEach(route => {
    addRouteRecord(route, pathMap);
  });

  return {
    pathMap
  }
}

/**
 * 添加一个路由记录(递归当前的树形路由配置)
 *  先序深度遍历:先把当前路由放进去,再处理他的子路由
 * @param {*} route   原始路由记录
 * @param {*} pathMap 路由规则的扁平化映射表
 * @param {*} parent  当前路由所属的父路由对象
 */
function addRouteRecord(route, pathMap, parent){

  // 处理子路由时,需要做路径拼接
  let path = parent ? (parent.path + '/' + route.path) : route.path
  
  // 构造路由记录对象(还包含其他属性:path、component、parent、name、props、meta、redirect...)
  let record = {
    path,
    component: route.component,
    parent // 标识当前组件的父路由记录对象
  }

  // 查重:路由定义不能重复,否则仅第一个生效
  if (!pathMap[path]) { 
    pathMap[path] = record; // 将当前路由的映射关系存入pathMap
  }

  // 递归处理当前路由的子路由
  if (route.children) {
    route.children.forEach(childRoute => {
      addRouteRecord(childRoute, pathMap, record);
    })
  }
}

在路由匹配器中,调用 createRouteMap 方法,进行路由配置的扁平化处理:

import createRouteMap from "./create-route-map"

export default function createMatcher(routes) {
  //  路由配置的扁平化处理
  let { pathMap } = createRouteMap(routes);
}

3,添加 match 方法

创建 match 方法,用于通过路由规则匹配到对应的组件;

import createRouteMap from "./create-route-map"

export default function createMatcher(routes) {

    //  路由配置的扁平化处理
    let { pathMap } = createRouteMap(routes);
    
    // 根据路径进行路由匹配
    function match(location) {
        let record = pathMap[location];
    }
    
    return {
        addRoutes, // 添加路由 
        match // 用于匹配路径
    }
}

4,添加 addRoutes 方法

创建 addRoutes 方法,用于动态的添加路由匹配规则;

import createRouteMap from "./create-route-map"

export default function createMatcher(routes) {

    //  路由配置的扁平化处理
    let { pathMap } = createRouteMap(routes);
    
    // 根据路径进行路由匹配
    function match(location) {
        let record = pathMap[location];
    }
    
    /**
     * 动态添加路由匹配规则
     *  将追加的路由规则进行扁平化处理
     */
    function addRoutes(routes) {
        createRouteMap(routes,pathMap);
    }
    
    return {
        addRoutes, // 添加路由 
        match // 用于匹配路径
    }
}

应用场景:在后台管理应用中,部分菜单/路由是根据权限配置来决定的,可以使用路由插件提供的 addRoutes 方法动态添加路由;


五,结尾

本篇,介绍了路由映射表的创建,主要包含以下内容:

  • 路由安装的初始化:init 方法;
  • 路由初始化:constructor 构造函数逻辑;
  • 创建路由匹配器:createMatcher;
  • match方法 和 addRoutes方法的实现;

下一篇,介绍路由跳转的实现;

// todo 本篇大多篇幅和主题介绍了路由匹配器和 VueRouter 构造函数中的处理逻辑,init 方法的相关介绍可以考虑从本篇移除;


维护日志

  • 20210821:

    • 添加了“承上启下”部分,总结前置知识点,并引出本文要解决的问题;
    • 修改了部分二级标题的命名,使主题描述更加准确;
    • 其他:决定要有一个“承上启下”的部分作为开篇:
      • 一来,让读者能够快速了解与本文有关的前置知识点,更好的将前后文串联起来;
      • 二来,强制自己做一次阶段性总结,捋清叙事思路;
      • “承上启下”部分命名仍需思考,与“前言”部分有重复;
  • 20210823:

    • 调整了“路由安装”与“路由实例化”初始化先后顺序;
    • 添加了诸多代码注释,使关键逻辑清晰易懂;
    • 添加 todo,修改“结尾”与文章摘要;