【学习笔记】手写Vue-Router(二)

145 阅读4分钟

前文提要 跟着来,你也可以手写VueRouter - 掘金 (juejin.cn)

这次是参考这位大佬的文章,尝试跟着写。文章很长,慢慢啃。 我个人是比较喜欢这种循序渐进式的教学文章的,一步步的就像升级打怪,不过如果碰到难题基本上只能自己解决了。

框架版本为vue2,vue-router-3.5.2

往期回顾 【学习笔记】手写Vue-Router(一) - 掘金 (juejin.cn)

一结尾的#号有了解释,是因为在VueRouter类中没有对接收的参数做处理,只需要简单处理就可以访问到类对应的实例

初步构建VueRouter类

首先需要知道VueRouter类的作用,根据router/index.js中传入的参数,主要有三个属性:

  1. mode:路由模式

    • 类型: string
    • 默认值: "hash" (浏览器环境) | "abstract" (Node.js 环境)
    • 可选值: "hash" | "history" | "abstract" 配置路由模式:
    • hash: 使用 URL hash 值来作路由。支持所有浏览器,包括不支持 HTML5 History Api 的浏览器。
    • history: 依赖 HTML5 History API 和服务器配置。
    • abstract: 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式。
  2. base:应用的基路径。例如,如果整个单页应用服务在 /app/ 下,然后 base 就应该设为 "/app/",默认值: "/"

  3. routes: Array类型,包含以下参数

interface RouteConfig = {
  path: string,
  component?: Component,
  name?: string, // 命名路由
  components?: { [name: string]: Component }, // 命名视图组件
  redirect?: string | Location | Function,
  props?: boolean | Object | Function,
  alias?: string | Array<string>,
  children?: Array<RouteConfig>, // 嵌套路由
  beforeEnter?: (to: Route, from: Route, next: Function) => void,
  meta?: any,

  // 2.6.0+
  caseSensitive?: boolean, // 匹配规则是否大小写敏感?(默认值:false)
  pathToRegexpOptions?: Object // 编译正则的选项
}

注: 这里先实现routes和mode属性的解析

VueRouter类constructor具体内容

首先判断是否在浏览器环境中,也就是判断window对象是否存在(若是node环境,则只存在global)。

const inBrowser = typeof window !== 'undefined'

若是非浏览器环境,则强制使用abstract模式

// 支持所有JavaScript 运行环境,非浏览器环境强制使用abstract模式,主要用于SSR
        if (!inBrowser) {
            mode = 'abstract'
        }

若是浏览器环境,则取传入实例的mode

// 路由配置
        this.options = options;
        // 创建路由matcher对象,传入routes路由配置列表以及VueRouter实例,主要负责url匹配
        let mode = options.mode || 'hash'

        // 支持所有JavaScript 运行环境,非浏览器环境强制使用abstract模式,主要用于SSR
        if (!inBrowser) {
            mode = 'abstract'
        }

        this.mode = mode || 'hash'

先根据routes来进行路由与url匹配,这个匹配函数为createMatcher

 this.matcher = createMatcher(options.routes)

然后根据mode创建history实例。

 switch (mode) {
            case 'history':
                this.history = new HTML5History(this)
                break
            case 'hash':
                this.history = new HashHistory(this)
                break
            case 'abstract':
                this.history = new AbstractHistory(this)
                break
            default:
                if (process.env.NODE_ENV !== 'production') {
                    throw new Error(`[vue-router] invalid mode: ${mode}`)
                }
        }

在这里我们又用到了五个导出的函数,所以我们再定义五个文件,导入到入口文件中。

import { createMatcher } from './create-matcher'
import { HashHistory } from './history/hash'
import { HTML5History } from './history/html5'
import { AbstractHistory } from './history/abstract'

接下来我们就来实现这五个函数。

createMatcher

createMatcher函数当中又需要做的事:

  • 解析routes数组成我们需要的格式,我们把解析后的routes叫做pathMap
  • pathMap对象进行增加、修改等操作
  • 通过urlpathMap中对应路由对象找出,我们把这个对象叫做Matcher

我们定义一个函数createRouteMap,用来输出pathMap路由映射对象。

import { createRouteMap } from "./create-route-map";

createRouteMap

createMatcher函数中接收pathMap

export function createMatcher(routes) {
    // 生成路由映射对象pathMap
    const pathMap = createRouteMap(routes)
}

因为我们得到的url#后面的字符串,例如http://localhost/#/home/one这个url,我们得到的是/home/one,我们就需要找到/home/one对应的路由信息,然后找到对应的组件.

我们可以将routes数组转换成{key:value}键值对的形式,其中key/home/one这种格式的pathvalue是具体的路由route信息,这样可以通过匹配key的值来找出对应路由信息。

因为chidren可以嵌套无数层,所以我们这里使用递归来处理。

export function createRouteMap(routes, oldPathMap, parentRoute) {
    const pathMap = Object.create(null) // old
    // 递归处理路由记录,最终生成路由映射
    routes.forEach(route => {
        // 生成一个RouteRecord并更新pathMap
        addRouteRecord(pathMap, route, null)
    })
    return pathMap
}
// 添加路由记录
function addRouteRecord(pathMap, route, parent) {
    const { path, name } = route

    // 生成格式化后的path(子路由会拼接上父路由的path)
    const normalizedPath = normalizePath(path, parent)

    // 生成一条路由记录
    const record = {
        path: normalizedPath, // 规范化后的路径
        regex: '', // 利用path-to-regexp包生成用来匹配path的增强正则对象,用来匹配动态路由(/a/:b)
        components: route.components, // 保存路由组件,省略了命名视图解析
        name,
        parent, // 父路由记录
        redirect: route.redirect, // 重定向的路由配置对象
        beforeEnter: route.beforeEnter, // 路由独享的守卫
        meta: route.meta || {}, // 元信息
        props: route.props == null ? {} : route.props // 动态路由传参
    }

    // 处理有子路由情况,递归
    if (route.children) {
        // 遍历生成子路由记录
        route.children.forEach(child => {
            addRouteRecord(pathMap, child, record)
        })
    }

    // 若pathMap中不存在当前路径,则添加pathList和pathMap
    if (!pathMap[record.path]) {
        pathMap[record.path] = record
    }
}

大体原理就是遍历routes数组,调用递归函数addRouteRecord

addRouteRecord有三个参数,第一个是pathMap,是最后要输出的对象,第二个是route,是routes数组的单个对象,最后一个是parent,也就是当前route对象的父路由对象。

首先会进行格式化路径,调用函数normalizePath

// 规格化路径
function normalizePath(
    path,
    parent
) {
    // 下标0为/,则是最外层path
    if (path[0] === '/') { return path }
    // 无父级,则是最外层path
    if (!parent) { return path }
    // 清除path中双斜杠中的一个
    return `${parent.path}/${path}`.replace(/\/\//g, '/')
}

得到格式化后的路径const normalizedPath = normalizePath(path, parent)

如果当前routechildren属性,遍历route.children并递归调用addRouteRecord

最后根据normalizedPath判断pathMap中是否已存入此record,若没有则赋值存入。

这样就成功输出了我们需要的格式{path:record, ...}

接下来我们来看对pathMap进行操作的函数。

addRoute、addRoutes、getRoutes、match

export function createMatcher(routes) {
    // 生成路由映射对象pathMap
    const pathMap = createRouteMap(routes)

    // 动态添加路由(添加一条新路由规则)
    /* 
    - 有两个参数,2种用法
    - 添加一条新路由规则。如果该路由规则有name,并且已经存在一个与之相同的名字,则会覆盖它
    - 添加一条新路由规则记录作为现有路由的子路由。如果该路由规则有name,并且已经存在一个与之相同的名字,则会覆盖它
    */
    function addRoute(parentOrRoute, route) {
        // 这里用path路径取代了name字符串
        const parent = (typeof parentOrRoute !== 'object') ? pathMap[parentOrRoute] : undefined
        createRouteMap([route || parentOrRoute], pathMap, parent)
    }

    // 动态添加路由(参数必须是一个符合routes选项要求的数组)
    function addRoutes(routes) {
        createRouteMap(routes, pathMap)
    }

    // 获取所有活跃的路由记录列表
    function getRoutes() {
        return pathMap
    }

    // 路由匹配
    function match(location) {
        location = typeof location === 'string' ? { path: location } : location
        // return pathMap[location.path]
        return createRoute(pathMap[location.path], location)// 修改
    }

    return {
        match,
        addRoute,
        getRoutes,
        addRoutes
    }
}

addRouteaddRoutes是通过改写createRouteMap实现的,addRoutes将之前的pathMap也传入了createRouteMap中,我们就需要判断第二个参数是否有初始值

export function createRouteMap(routes, oldPathMap, parentRoute) {
    // const pathMap = Object.create(null) // old
    const pathMap = oldPathMap || Object.create(null) // new
    // 递归处理路由记录,最终生成路由映射
    routes.forEach(route => {
        // 生成一个RouteRecord并更新pathMap
        // addRouteRecord(pathMap, route, null)
        addRouteRecord(pathMap, route, parentRoute)
    })
    return pathMap
}

addRoute除了pathMap之外还传入了route的父路由对象,所以在遍历的时候就不能初始值为null了,将parent替代初始值传入。

接下来看match方法,它需要将匹配的路由对象输出

 // 路由匹配
    function match(location) {
        location = typeof location === 'string' ? { path: location } : location
        return pathMap[location.path]
    }

第二篇笔记写到这里,接下来实现history实例化。