这次是参考这位大佬的文章,尝试跟着写。文章很长,慢慢啃。 我个人是比较喜欢这种循序渐进式的教学文章的,一步步的就像升级打怪,不过如果碰到难题基本上只能自己解决了。
框架版本为vue2,vue-router-3.5.2
一结尾的#号有了解释,是因为在VueRouter类中没有对接收的参数做处理,只需要简单处理就可以访问到类对应的实例
初步构建VueRouter类
首先需要知道VueRouter类的作用,根据router/index.js中传入的参数,主要有三个属性:
-
mode:路由模式
- 类型:
string
- 默认值:
"hash" (浏览器环境) | "abstract" (Node.js 环境)
- 可选值:
"hash" | "history" | "abstract"
配置路由模式: hash
: 使用 URL hash 值来作路由。支持所有浏览器,包括不支持 HTML5 History Api 的浏览器。history
: 依赖 HTML5 History API 和服务器配置。abstract
: 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式。
- 类型:
-
base:应用的基路径。例如,如果整个单页应用服务在
/app/
下,然后base
就应该设为"/app/"
,默认值:"/"
-
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
对象进行增加、修改等操作 - 通过
url
将pathMap
中对应路由对象找出,我们把这个对象叫做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
这种格式的path
,value
是具体的路由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)
如果当前route
有children
属性,遍历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
}
}
addRoute
和addRoutes
是通过改写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
实例化。