router前置知识 以及 vue-router源码部分解析
URI和URL的简介
URI
统一资源标识符,允许用户对网络中的资源通过特定的协议进行交互操作
- uniform:规定的统一的语法格式,以方便处理多种不同类型的资源,而无需根据上下文环境来识别资源的类型
- resource:表示可标识的任何资源,资源不仅可以作为单一对象,也可以作为多个对象的集合体
- Identifier:表示可标识对象,也被称作标识符
URI是由某个协议方案表示的资源的定位标识符,协议方案是指访问资源时使用的协议类型名称,HTTP就是一种协议类型名称,除此之外,还有FTP,file,TELNET等的协议方案
通用语法组成
URI = schme:[//authority]path[?query][fragment]
authority组成
authority = [userInfo]host[sport]
在authority当中,
- userinfo是登录信息,通常是指定的用户名和密码,当作身份认证凭证使用,可选项
- 服务器地址host使用需要指定访问的服务器地址,地址可以DNS解析
- port:服务器连接的端口号,可选项,不指定就是默认的端口号
在URI当中,scheme是协议方案名,在使用HTTPS或者HTTP等协议方案名是不区分大小写,data:指定的脚本程序或者数据
path是带层次的文件路径,访问特定的资源
query是查询字符串,针对指定路径的文件资源
fragment片段标识符,通常标记已经获取资源的子资源
URL
统一资源定位器,是URI的一种,标识网络资源
语法定义和URI是一样的
浏览器URI编码
百分号编码
使用百分号进行编码。对于需要编码的字符串,将其表示成两个16进制数字,在前面放置%,替换字符相应的位置进行编码
特殊意思的字符,?表示查询,#表示片段标识,没有特殊意义的就进行编码操作
encodeURI
对于ASCII字符,对数字和字母进行编码,但是不对标点符号进行编码
对于非ASCII字符,在每个字节前面放置转义字符,放置对应的位置
encodeURIComponent
这个的假定参数是URI的一部分,(比如:协议名,主机名,路径或者查询字符串)将转移encodeURI转移不到的符号,好处是:如果字符串被发送到服务端进行解析,那么服务端会把紧跟在%后面的字节当成普通的字节,而不会将他当成各个参数或者键值对的分隔符
相比这个,encodeURI被用作对完整的URI进行编码,encodeURIComponent用作对URI的一个组件中的一个片段进行编码
两者的区别:
encodeURL:
- 用来编码整个URL,只处理“非法字符”,保留URL的结构(:,/,之类的)
encodeURLComponent:
- 用来编码URL的某一个片段,会把所有的特殊字符都转移,包括encodeURL不包括的
简单的对比
字符 encodeURL encodeURLComponent 说明
| 空格 | %20 | %20 | 都编码为空格 |
|---|---|---|---|
: | 不编码 | 不编码 | 保留协议部分 |
/ | 不编码 | %2F | encodeURI保留路径结构 |
? | 不编码 | %3F | encodeURI保留查询开始符 |
= | 不编码 | %3D | encodeURIComponent编码参数值中的= |
& | 不编码 | %26 | 防止被误认为参数分隔符 |
# | 不编码 | %23 | 防止被误认为锚点 |
| 中文 | %E... | %E... | 都会编码 |
使用场景:
- 编码整个URL,使用encodeURL
- 编码参数值(部分场景),使用encodeURLcomponent
浏览器记录
是指浏览器中各页面的导航记录,在现代浏览器当中,浏览器当中没有直接API可以获取,浏览器记录由浏览器统一管理,并不属于某个具体的页面,和页面形式及其内存无关
window.history中包含很多属性,这一特性包括pushState,replaceState|popState事件 分别用来添加和修改历史记录条目
history.pushState
基本用法
用来无刷新增增加历史栈记录,调用pushState方法可以修改浏览器路径。
使用需要三个参数,状态对象,标题(忽略),可选择的URL
当设置第三个参数URL的时候,可以改变浏览器的URL且不会刷新浏览器,
第一个参数是需要传进来的状态,状态的类型可以是任意类型,设置第一个参数之后我们就可以使用history.state进行读取
历史栈的变化
pushState调用会引起历史栈的变化,浏览器通常会维护一个用户访问过的历史栈,使用前进或者后退进行访问(window.history.go),调用pushState方法,历史栈的内容会被修改,会添加历史栈的栈记录,同时也会改变指针的指向
是向栈顶放置新的历史记录,并使之成为新的栈顶
第三个参数不传入,也会增加长度
history.replaceState
基本用法
和pushState使用方法类似,但是不是新建而是修改当前的历史记录,length不会发生变化
历史栈变化
replaceState不会改变历史栈的数量,改变的当前栈指针指向的内容
通过相对路径添加和修改浏览器的记录
上面的两个参数除了支持绝对路径导航之外,还支持相对路径导航,会自动的进行路径的补全,但是注意补全路径的正确性
在base元素存在的情况下添加和修改浏览器的记录
当HTML元素当中base元素存在使用base的href的值作为基准路径
浏览器跳转
内置的window存在history对象和location对象,可以进行导航的跳转
window.history.go
用来完成用户历史记录中向后向前的跳转,传入的参数可以是正数和负数,0代表刷新,仅仅移动指针
window.history.forward
作为跳转当前栈指针所指钱一个记录的方法,栈指针向前移动一位,等同于go(1)
注意:前进是否刷新取决于历史栈中的栈记录,push操作不会进行书信
window.history.back
go(-1)
window.location.href
将window.location.href导航会产生一个新的历史记录,将字符串设置到window.location和设置到window.location.href行为一致,这个设置操作会刷新页面并重新加载URL所指定的内容
是相当于增加了历史栈,对原来的历史栈没影响
操作流程:中断当前页面的执行,发送新的HTTP请求,完全重新加载页面,创建新的历史记录
window.location.hash
同样会设产生新的历史栈记录,如果设置location.hash和浏览器的URL的地址相同,不会触发任何事件,也不会添加历史记录
想要不产生新的历史栈记录,使用replace实现
window.llocation.replace
替换当前栈的记录,并刷新页面,重新加载导入的URL,和设置href不同,旧页面的不会保存,不能进行后退操作
浏览器相关的事件
popState
在pushState或者replaceState产生历史栈记录当中,移动指针或者前进后退将会触发popState事件,可以通过window.addListener进行监听
监听函数的参数是对应的popState事件的事件对象,
使用pushHistory或者replaceState都不会触发popState事件,前进后退或者go,back之类的会触发事件
除了可以从popState事件对象获取当前的state对象,还可以通过history获取当前的对象,就是读取history.state的值即可
注意:有的浏览器对popState实现不一致,当网页加载完成之后,可能不会触发,我们可以使用history获取状态对象,而不是通过popState获取,在事件当中更改event.state,history.state不会改变,但是直接更改就会改变
当通过href设置的hash值,不管设置前后是否相同,都会触发事件
hashChange
用来监听浏览器hash值的变化,使用addListener即可进行监听
hashChange事件可以通过设置location.hash,在地址栏中手动的修改hash,调用window.history.go,在浏览器前进或者后退触发
可以获取事件对象HashChangeEvent,获得相应的继承属性
注意:history.pushState不会触发hashChange事件,即使前后导航的URL仅仅hash不同
手动触发事件
对于popState事件,如果调用pushState方法,则replaceState方法不会触发,仅仅移动指针才会触发
但是我们通过dispatchEvent方法,也能实现不移动指针便可控制popState方法的触发,调用这个dispatchEvent方法,方法的返回值是事件的取消状态
详细解释一下dispatch和addListener的关系:
- dispatch相当于触发操作,addlistener相当于接收这个操作并执行相应的回调,所有的listen都会收到对应的消息
- 使用dispatch之前还是需要手动的创建事件,dispatch只能触发事件,没有创建的过程
使用场景:
- 自动化测试
- 程序化表单提交
- 自定义组件通信
- 模拟用户交互
- 集成第三方库
history
主要了解三种模式定义的区别
在vue-router的的使用下,3版本之前使用history实现,但是最新版本下已经实现了自己的history状态管理
相对应的关系
| Vue Router 功能 | history 库对应 | 说明 |
|---|---|---|
createWebHistory() | createBrowserHistory() | HTML5 History API |
createWebHashHistory() | createHashHistory() | Hash 模式 |
createMemoryHistory() | createMemoryHistory() | 服务端渲染 |
这里我们可以直接学习vue-router中自己实现的history状态管理
核心的接口设计 实现主要的接口和方法
export interface RouterHistory{
readonly base: string
readonly location: HistoryLocation
readonly state: HistoryState
push(to: HistoryLocation,data?:HistoryState): void
replace(to: HistoryLocation,data?:HistoryState): void
go(delta: number,triggerListeners?:boolean): void
back(): void
forward(): void
listen(callback: NavigationCallback): () => void
destory(): void
}
三种History的实现
webHistory
HTML5 History API
接收一个参数,函数可以接收一个base字符串的参数,这个参数提供了一个基础的路径,在createWebHistory会首先调用normalizeBase函数,将base标准化
在没有配置base的情况下,在浏览器环境下会尝试获取标签中的href属性作为base,如果没有或者标签的href属性没有值,base最终使用/,然后对base进行操作,去除HTTP的部分,最终目的通过base+fullPath的形式建立一个href,base标准化之后,会使用historyNavigation和historyListener变量
export function createWebHistory(base?: string): RouterHistory {
base = normalizeBase(base);
const historyNavigation = useHistoryStateNavigation(base);
const historyListeners = useHistoryListeners(
base,
historyNavigation.state,
historyNavigation.location,
historyNavigation.replace
);
function go(delta: number, triggerListeners = true) {
if (!triggerListeners) historyListeners.pauseListeners();
window.history.go(delta);
}
return {
base,
location: historyNavigation.location,
state: historyNavigation.state,
push: historyNavigation.push,
replace: historyNavigation.replace,
go,
back: () => go(-1),
forward: () => go(1),
listen: historyListeners.listen,
destroy: historyListeners.teardown,
};
}
HashHistory
hash模式
export function createWebHashHistory(base?: string): RouterHistory {
base = location.host ? base || location.pathname : '';
// 确保 base 包含 #
if (!base.includes('#')) base += '#';
return createWebHistory(base);
}
MemoryHistory
export function createMemoryHistory(base: string = ''): RouterHistory {
let currentLocation: HistoryLocation = normalizeBase(base);
let currentState: HistoryState = {};
let listeners: NavigationCallback[] = [];
let position = 0;
function setLocation(
to: HistoryLocation,
state: HistoryState,
replace: boolean
) {
const from = currentLocation;
currentLocation = to;
currentState = state;
if (!replace) position++;
listeners.forEach(listener => {
listener(currentLocation, from, { delta: replace ? 0 : 1 });
});
}
return {
base,
location: currentLocation,
state: currentState,
push(to: HistoryLocation, data?: HistoryState) {
setLocation(to, data || {}, false);
},
replace(to: HistoryLocation, data?: HistoryState) {
setLocation(to, data || {}, true);
},
go(delta: number) {
const newPosition = position + delta;
if (newPosition >= 0 && newPosition < position) {
position = newPosition;
// 这里会模拟历史记录跳转
}
},
listen(callback: NavigationCallback) {
listeners.push(callback);
return () => {
const index = listeners.indexOf(callback);
if (index > -1) listeners.splice(index, 1);
};
},
destroy() {
listeners = [];
}
};
}
和createWebHistory,createWebHashHistory一样,createMemoryHistory同样返回一个routerHistory类型的对象,和前面两个方法不同的是,这个方法维护一个队列queue和 一个position保证历史存储的正确性,主要是SSR中正确的使用
核心函数的实现
useHistoryStateNavigation
状态导航管理:
主要负责维护和操作浏览器的历史记录状态,主要的包含的部分就是
-
location 当前路径
-
location:包含一个value属性值,value值是
createCurrentLocation()方法的返回值 -
createCurrentLocation()方法作用是通过window.location创建一个规范化的history location。,方法接收两个参数:经过标准化base字符串和一个window.location对象function createCurrentLocation( base: string, location: Location ): HistoryLocation { const { pathname, search, hash } = location // allows hash bases like #, /#, #/, #!, #!/, /#!/, or even /folder#end // 从base中获取#的索引 const hashPos = base.indexOf('#') // 如果base中包含# if (hashPos > -1) { // 如果hash包含base中的#后面部分,slicePos为base中#及后面字符串的的长度,否则为1 let slicePos = hash.includes(base.slice(hashPos)) ? base.slice(hashPos).length : 1 // 从location.hash中获取path,/#add, #add let pathFromHash = hash.slice(slicePos) // 在开头加上/,形成/#的格式 if (pathFromHash[0] !== '/') pathFromHash = '/' + pathFromHash // stripBase(pathname, base):将pathname去除base部分 return stripBase(pathFromHash, '') } // 如果base中不包含#,把pathname中的base部分删除 const path = stripBase(pathname, base) return path + search + hash }将base删除拿到的就是location
-
-
state 当前状态
- 一个包含value的属性对象,value存储的是当前的
history.state
- 一个包含value的属性对象,value存储的是当前的
-
push 添加新的记录
- 向历史记录当中添加一条记录,在
push的过程中你会发现调用了两次changeLiocation,目的是为了记录当前页面在滚动位置,如果使用history.back()或者前进|后退会返回对应的位置,为了不在历史栈中保存新的记录,会进行替换
- 向历史记录当中添加一条记录,在
-
replace 替换当前的记录
状态的数据结构historyState
interface HistoryState{
position: number, // 在历史栈当中的位置
back: string|null, // 上一个路径
forward: string|null, // 下一个路径
timestamp: number, //时间戳
scollPostion?:{ // 滚动的位置
x: number,
y: number,
}
[key:stirng]: any //自定义数据
}
// 这里的base已经是处理之后的路径了
function useHistoryStateNavigation(base: string) {
const { history, location } = window;
// 当前状态
const currentLocation: HistoryLocation = normalizeLocation(
location,
base
);
// 创建初始状态
const currentState: HistoryState = {
position: history.length - 1,
...(history.state || {})
};
// 改变历史记录
function changeLocation(
to: HistoryLocation,
state: HistoryState,
replace: boolean
): void {
const url = createBaseLocation(base, to);
try {
// 使用 History API
if (replace) {
history.replaceState(state, '', url);
} else {
history.pushState(state, '', url);
}
} catch (err) {
// 降级方案:对于某些浏览器限制
if (replace) {
location.replace(url);
} else {
location.assign(url);
}
}
}
// 添加新的历史记录
function push(to: HistoryLocation, data?: HistoryState) {
const state: HistoryState = {
...currentState,
...data,
position: currentState.position + 1,
};
changeLocation(to, state, false);
currentLocation.value = to;
currentState.value = state;
}
// 替换当前的记录
function replace(to: HistoryLocation, data?: HistoryState) {
const state: HistoryState = {
...currentState,
...data,
position: currentState.position,
};
changeLocation(to, state, true);
currentLocation.value = to;
currentState.value = state;
}
return {
location: currentLocation,
state: currentState,
push,
replace,
};
}
useHistoryListeners
主要是用来管理浏览器的历史记录监听的核心函数,主要处理PopState事件和页面卸载之前的状态保存
接收四个参数,base(标准化的base),historyState,currentLocation,replace,(后面的三个参数是useHistoryStateNavigation的返回值),在listeners当中,会对popState,beforeunload进行监听
返回参数的类型对象包含三个属性:
pauseListeners:暂停监听的函数listen:接受一个回调函数,并返回一个删除监听的函数,这个函数会加入到listener数组当中,并向teardowns数组中添加卸载函数destory:销毁函数,清空listeners和teardowns。移除popState``beforeunload监听
function useHistoryListeners(
base: string,
historyState: ValueContainer<StateEntry>,
currentLocation: ValueContainer<HistoryLocation>,
replace: RouterHistory['replace']
) {
let listeners: NavigationCallback[] = []
let teardowns: Array<() => void> = []
let pauseState: HistoryLocation | null = null
const popStateHandler: PopStateListener = ({
state,
}: {
state: StateEntry | null
}) => {
const to = createCurrentLocation(base, location)
const from: HistoryLocation = currentLocation.value
const fromState: StateEntry = historyState.value
let delta = 0
if (state) {
currentLocation.value = to
historyState.value = state
// 如果暂停监听了,则直接return,同时pauseState赋为null
if (pauseState && pauseState === from) {
pauseState = null
return
}
// 计算移动步数
delta = fromState ? state.position - fromState.position : 0
} else {
replace(to)
}
// 执行监听函数列表
listeners.forEach(listener => {
listener(currentLocation.value, from, {
delta,
type: NavigationType.pop,
direction: delta
? delta > 0
? NavigationDirection.forward
: NavigationDirection.back
: NavigationDirection.unknown,
})
})
}
function pauseListeners() {
pauseState = currentLocation.value
}
function listen(callback: NavigationCallback) {
listeners.push(callback)
const teardown = () => {
const index = listeners.indexOf(callback)
if (index > -1) listeners.splice(index, 1)
}
teardowns.push(teardown)
return teardown
}
function beforeUnloadListener() {
const { history } = window
if (!history.state) return
// 当页面关闭时记录页面滚动位置
history.replaceState(
assign({}, history.state, { scroll: computeScrollPosition() }),
''
)
}
function destroy() {
for (const teardown of teardowns) teardown()
teardowns = []
window.removeEventListener('popstate', popStateHandler)
window.removeEventListener('beforeunload', beforeUnloadListener)
}
window.addEventListener('popstate', popStateHandler)
window.addEventListener('beforeunload', beforeUnloadListener)
return {
pauseListeners,
listen,
destroy,
}
}
现在已经实现了两个核心的函数,我们回到createWebHistory当中,创建之后声明go函数,这个函数接受两个的变量,delta历史记录移动步数,triggerListener是是否触发监听的函数
function go(delta: number, triggerListeners = true) {
if (!triggerListeners) historyListeners.pauseListeners()
history.go(delta)
}
最后创建一个routerHistory对象,并将其返回
const routerHistory: RouterHistory = assign(
{
location: '',
base,
go,
createHref: createHref.bind(null, base),
},
historyNavigation,
historyListeners
)
// 拦截routerHistory.location,使routerHistory.location返回当前路由地址
Object.defineProperty(routerHistory, 'location', {
enumerable: true,
get: () => historyNavigation.location.value,
})
// 拦截routerHistory.state,使routerHistory.state返回当前的的history.state
Object.defineProperty(routerHistory, 'state', {
enumerable: true,
get: () => historyNavigation.state.value,
})
return routerHistory
Matcher
什么是matcher?
vue-router当中,每一个我们定义的的路由都会解析成一个对应的matcher(routeRecordMatcher类型),路由的增删改查,都会依赖matcher实现
route和matcher的区别
路由是配置对象,具有映射关系(匹配)
matcher是匹配器,负责解析和匹配路由(处理)和进行路由管理
基本的功能:
- 管理路由规则:维护所有的路由配置信息,包括路径,组件和嵌套路由
-
路由匹配
- 根据当前的路径(URL)进行访问找到应该显示的组件
-
生成URL
- 根据当前的路由名称和参数生成对应的URL路径
-
处理路由动态变化
- 支持添加,删除路由规则(动态路由)
工作原理:
-
初始化阶段:
- 当创建vue router实例的时候,会根据传入的routes配置生成一个matcher实例
- matcher会递归的解析路由配置,构建内部的数据结构
- 处理动态的路由参数,嵌套路由,命名路由等特殊配置
-
路由匹配的过程
- 当URL发生变化的时候,matcher会接收新的路径进行匹配
- 从当前的根路由开始,按照定义的路由规则逐层匹配URL片段
- 优先匹配静态路由,在动态处理路由参数和通配符
- 最终返回匹配到的路由记录数组
-
动态路由处理:
- 通过
matcher.addRoutes()| router.addRoute()可以动态添路由 - matcher会更新拆路由规则数据结构-,使得新的路由立即生效
- 通过
createRouteMatcher
在createRouter当中会通过createRouteMatcher创建一个matcher
这个函数接收两个参数routes,globalOptions其中routes为我们定义的路由表,也就是在createRouter时候传入的options.routes,而globalOpotions就是createRouter中的options
createRouteMatcher中声明了两个变量matchers,matcherMap,用来存储通过路由表解析的matcher,然后遍历routes,对每个元素调用addRoute方法,最后返回一个对象,这个对象有addRoute,resolve,removeRoute,getRoute,getRecordMatcher几个属性,每个属性对应一个函数
export function createRouterMatcher(
routes: RouteRecordRaw[],
globalOptions: PathParserOptions
): RouterMatcher {
const matchers: RouteRecordMatcher[] = []
const matcherMap = new Map<RouteRecordName, RouteRecordMatcher>()
globalOptions = mergeOptions(
{ strict: false, end: true, sensitive: false } as PathParserOptions,
globalOptions
)
function getRecordMatcher(name: RouteRecordName) { // ... }
function addRoute(
record: RouteRecordRaw,
parent?: RouteRecordMatcher,
originalRecord?: RouteRecordMatcher
) {
// ...
}
function removeRoute(matcherRef: RouteRecordName | RouteRecordMatcher) { // ... }
function getRoutes() { // ... }
function insertMatcher(matcher: RouteRecordMatcher) { // ... }
function resolve(
location: Readonly<MatcherLocationRaw>,
currentLocation: Readonly<MatcherLocation>
): MatcherLocation {
// ...
}
routes.forEach(route => addRoute(route))
return { addRoute, resolve, removeRoute, getRoutes, getRecordMatcher }
}
addRoute
接受三个参数,record(新增路由),parent(父matcher),originalRecord(原始matcher)
function addRoute(
record: RouteRecordRaw,
parent?: RouteRecordMatcher,
originalRecord?: RouteRecordMatcher
) {
// used later on to remove by name
const isRootAdd = !originalRecord
// 标准化化路由记录
const mainNormalizedRecord = normalizeRouteRecord(record)
// aliasOf表示此记录是否是另一个记录的别名
mainNormalizedRecord.aliasOf = originalRecord && originalRecord.record
const options: PathParserOptions = mergeOptions(globalOptions, record)
// 声明一个记录的数组用来处理别名
const normalizedRecords: typeof mainNormalizedRecord[] = [
mainNormalizedRecord,
]
// 如果record设置了别名
if ('alias' in record) {
// 别名数组
const aliases =
typeof record.alias === 'string' ? [record.alias] : record.alias!
// 遍历别名数组,并根据别名创建记录存储到normalizedRecords中
for (const alias of aliases) {
normalizedRecords.push(
assign({}, mainNormalizedRecord, {
components: originalRecord
? originalRecord.record.components
: mainNormalizedRecord.components,
path: alias,
// 如果有原始记录,aliasOf为原始记录,如果没有原始记录就是它自己
aliasOf: originalRecord
? originalRecord.record
: mainNormalizedRecord,
}) as typeof mainNormalizedRecord
)
}
}
let matcher: RouteRecordMatcher
let originalMatcher: RouteRecordMatcher | undefined
// 遍历normalizedRecords
for (const normalizedRecord of normalizedRecords) {
// 处理normalizedRecord.path为完整的path
const { path } = normalizedRecord
// 如果path不是以/开头,那么说明它不是根路由,需要拼接为完整的path
// { path: '/a', children: [ { path: 'b' } ] } -> { path: '/a', children: [ { path: '/a/b' } ] }
if (parent && path[0] !== '/') {
const parentPath = parent.record.path
const connectingSlash =
parentPath[parentPath.length - 1] === '/' ? '' : '/'
normalizedRecord.path =
parent.record.path + (path && connectingSlash + path)
}
// 提示*应使用正则表示式形式
if (__DEV__ && normalizedRecord.path === '*') {
throw new Error(
'Catch all routes ("*") must now be defined using a param with a custom regexp.\n' +
'See more at https://next.router.vuejs.org/guide/migration/#removed-star-or-catch-all-routes.'
)
}
// 创建一个路由记录匹配器
matcher = createRouteRecordMatcher(normalizedRecord, parent, options)
// 检查是否有丢失的参数
if (__DEV__ && parent && path[0] === '/')
checkMissingParamsInAbsolutePath(matcher, parent)
// 如果有originalRecord,将matcher放入原始记录的alias中,以便后续能够删除
if (originalRecord) {
originalRecord.alias.push(matcher)
// 检查originalRecord与matcher中动态参数是否相同
if (__DEV__) {
checkSameParams(originalRecord, matcher)
}
} else { // 没有originalRecord
// 因为原始记录索引为0,所以originalMatcher为有原始记录所产生的matcher
originalMatcher = originalMatcher || matcher
// 如果matcher不是原始记录产生的matcher,说明此时matcher是由别名记录产生的,此时将matcher放入originalMatcher.alias中
if (originalMatcher !== matcher) originalMatcher.alias.push(matcher)
// 如果命名并且仅用于顶部记录,则删除路由(避免嵌套调用)
if (isRootAdd && record.name && !isAliasRecord(matcher))
removeRoute(record.name)
}
// 遍历children,递归addRoute
if ('children' in mainNormalizedRecord) {
const children = mainNormalizedRecord.children
for (let i = 0; i < children.length; i++) {
addRoute(
children[i],
matcher,
originalRecord && originalRecord.children[i]
)
}
}
originalRecord = originalRecord || matcher
// 添加matcher
insertMatcher(matcher)
}
// 返回一个删除原始matcher的方法
return originalMatcher
? () => {
removeRoute(originalMatcher!)
}
: noop
}
在addRoute当中,会对record进行标准化的处理,如果原来存在原始的matcher,也就是originRecord,说明此时要添加的路由时另一记录的别名,这时会将originalRecord.record传入mainNormalizeRecord.aliasOf当中(当设置了路由别名的情况下就会这样)
const isRootAdd = !originalRecord
// 标准化化路由记录
const mainNormalizedRecord = normalizeRouteRecord(record)
// aliasOf表示此记录是否是另一个记录的别名
mainNormalizedRecord.aliasOf = originalRecord && originalRecord.record
const options: PathParserOptions = mergeOptions(globalOptions, record)
// 声明一个记录的数组用来处理别名
const normalizedRecords: typeof mainNormalizedRecord[] = [
mainNormalizedRecord,
]
然后会遍历record的别名,向normalizedRecords中添加由别名产生的路由
if ('alias' in record) {
// 别名数组
const aliases =
typeof record.alias === 'string' ? [record.alias] : record.alias!
// 遍历别名数组,并根据别名创建记录存储到normalizedRecords中
for (const alias of aliases) {
normalizedRecords.push(
assign({}, mainNormalizedRecord, {
components: originalRecord
? originalRecord.record.components
: mainNormalizedRecord.components,
path: alias,
// 如果有原始记录,aliasOf为原始记录,如果没有原始记录就是它自己
aliasOf: originalRecord
? originalRecord.record
: mainNormalizedRecord,
}) as typeof mainNormalizedRecord
)
}
}
后面会遍历normalizedRocords,遍历过程中,会首先处理成完整的path,然后通过createRouteMatcher方法创建一个matcher,如果matcher是由别名产生的,那么matcher会被加入由原始记录产生的matcher的alias属性当中,然后会遍历normalizedRecord的children属性,递归调用addRoute方法,最后,调用insertMatcher添加新创建的matcher
for (const normalizedRecord of normalizedRecords) {
// 处理normalizedRecord.path为完整的path
const { path } = normalizedRecord
// 如果path不是以/开头,那么说明它不是根路由,需要拼接为完整的path
// { path: '/a', children: [ { path: 'b' } ] } -> { path: '/a', children: [ { path: '/a/b' } ] }
if (parent && path[0] !== '/') {
const parentPath = parent.record.path
const connectingSlash =
parentPath[parentPath.length - 1] === '/' ? '' : '/'
normalizedRecord.path =
parent.record.path + (path && connectingSlash + path)
}
// 提示*应使用正则表示式形式
if (__DEV__ && normalizedRecord.path === '*') {
throw new Error(
'Catch all routes ("*") must now be defined using a param with a custom regexp.\n' +
'See more at https://next.router.vuejs.org/guide/migration/#removed-star-or-catch-all-routes.'
)
}
// 创建一个路由记录匹配器
matcher = createRouteRecordMatcher(normalizedRecord, parent, options)
// 检查是否有丢失的参数
if (__DEV__ && parent && path[0] === '/')
checkMissingParamsInAbsolutePath(matcher, parent)
// 如果有originalRecord,将matcher放入原始记录的alias中,以便后续能够删除
if (originalRecord) {
originalRecord.alias.push(matcher)
// 检查originalRecord与matcher中动态参数是否相同
if (__DEV__) {
checkSameParams(originalRecord, matcher)
}
} else { // 没有originalRecord
// 因为原始记录索引为0,所以originalMatcher为有原始记录所产生的matcher
originalMatcher = originalMatcher || matcher
// 如果matcher不是原始记录产生的matcher,说明此时matcher是由别名记录产生的,此时将matcher放入originalMatcher.alias中
if (originalMatcher !== matcher) originalMatcher.alias.push(matcher)
// 如果存在record.name并且是顶部记录,则删除路由(避免嵌套调用)
if (isRootAdd && record.name && !isAliasRecord(matcher))
removeRoute(record.name)
}
// 遍历children,递归addRoute
if ('children' in mainNormalizedRecord) {
const children = mainNormalizedRecord.children
for (let i = 0; i < children.length; i++) {
addRoute(
children[i],
matcher,
originalRecord && originalRecord.children[i]
)
}
}
// 如果originalRecord是方法传入的,那么originalRecord继续保持
// 如果originalRecord方法未传入。由于原始的matcher总是在索引为0的位置,所以如果有别名,那么这些别名的原始matcher会始终指向索引为0的位置
originalRecord = originalRecord || matcher
// 添加matcher
insertMatcher(matcher)
}
token
interface TokenStatic {
type: TokenType.Static
value: string
}
interface TokenParam {
type: TokenType.Param
regexp?: string
value: string
optional: boolean
repeatable: boolean
}
interface TokenGroup {
type: TokenType.Group
value: Exclude<Token, TokenGroup>[]
}
export type Token = TokenStatic | TokenParam | TokenGroup
token具有三种类型
TokenStatic:一种静态的token,不可变TokenParam:参数tokenTokenGroup:分组token
-
/one/two/three 对应的token数组
[ [{ type: TokenType.Static, value: 'one' }], [{ type: TokenType.Static, value: 'two' }], [{ type: TokenType.Static, value: 'three' }] ] -
/user/:id 对应的token数组
[ [ { type: TokenType.Static, value: 'user', }, ], [ { type: TokenType.Param, value: 'id', regexp: '', repeatable: false, optional: false, } ] ] -
/:id(\d+)new 对应的token数组
[ [ { type: TokenType.Param, value: 'id', regexp: '\d+', repeatable: false, optional: false, }, { type: TokenType.Static, value: 'new' } ] ]
我们可以看出token数组详细的描述了path的每一级路由的生成,通过token我们能够知道他是一个一级路由,并且他的这次的路由是由两部分组成,其中一部分是参数部分,另一部分是静态的,并且参数部分还说明了参数的正则,以及是否重复,是否可选配置
接下来就是tokanizePath是如何将path转化成token的
tokenizePath
tokenizePath的过程就是利用有限状态机生成token数组,解析path生成对应的token数组
生成对应的数据和对应的节点
- 将路径字符串分解成有意义的令牌
- 模式识别,识别静态端,动态参数,可选参数,通配符
- 结构解析:理解路径的语法结构
export const enum TokenType {
Static,
Param,
Group,
}
const ROOT_TOKEN: Token = {
type: TokenType.Static,
value: '',
}
export function tokenizePath(path: string): Array<Token[]> {
if (!path) return [[]]
if (path === '/') return [[ROOT_TOKEN]]
// 如果path不是以/开头,抛出错误
if (!path.startsWith('/')) {
throw new Error(
__DEV__
? `Route paths should start with a "/": "${path}" should be "/${path}".`
: `Invalid path "${path}"`
)
}
function crash(message: string) {
throw new Error(`ERR (${state})/"${buffer}": ${message}`)
}
// token所处状态
let state: TokenizerState = TokenizerState.Static
// 前一个状态
let previousState: TokenizerState = state
const tokens: Array<Token[]> = []
// 声明一个片段,该片段最终会被存入tokens中
let segment!: Token[]
// 添加segment至tokens中,同时segment重新变为空数组
function finalizeSegment() {
if (segment) tokens.push(segment)
segment = []
}
let i = 0
let char: string
let buffer: string = ''
// custom regexp for a param
let customRe: string = ''
// 消费buffer,即生成token添加到segment中
function consumeBuffer() {
if (!buffer) return
if (state === TokenizerState.Static) {
segment.push({
type: TokenType.Static,
value: buffer,
})
} else if (
state === TokenizerState.Param ||
state === TokenizerState.ParamRegExp ||
state === TokenizerState.ParamRegExpEnd
) {
if (segment.length > 1 && (char === '*' || char === '+'))
crash(
`A repeatable param (${buffer}) must be alone in its segment. eg: '/:ids+.`
)
segment.push({
type: TokenType.Param,
value: buffer,
regexp: customRe,
repeatable: char === '*' || char === '+',
optional: char === '*' || char === '?',
})
} else {
crash('Invalid state to consume buffer')
}
// 消费完后置空
buffer = ''
}
function addCharToBuffer() {
buffer += char
}
// 遍历path
while (i < path.length) {
char = path[i++]
// path='/\:'
if (char === '\' && state !== TokenizerState.ParamRegExp) {
previousState = state
state = TokenizerState.EscapeNext
continue
}
switch (state) {
case TokenizerState.Static:
if (char === '/') {
if (buffer) {
consumeBuffer()
}
// char === /时说明已经遍历完一层路由,这时需要将segment添加到tokens中
finalizeSegment()
} else if (char === ':') { // char为:时,因为此时状态是TokenizerState.Static,所以:后是参数,此时要把state变为TokenizerState.Param
consumeBuffer()
state = TokenizerState.Param
} else { // 其他情况拼接buffer
addCharToBuffer()
}
break
case TokenizerState.EscapeNext:
addCharToBuffer()
state = previousState
break
case TokenizerState.Param:
if (char === '(') { // 碰到(,因为此时state为TokenizerState.Param,说明后面是正则表达式,所以修改state为TokenizerState.ParamRegExp
state = TokenizerState.ParamRegExp
} else if (VALID_PARAM_RE.test(char)) {
addCharToBuffer()
} else { // 例如/:id/one,当遍历到第二个/时,消费buffer,state变为Static,并让i回退,回退后进入Static
consumeBuffer()
state = TokenizerState.Static
if (char !== '*' && char !== '?' && char !== '+') i--
}
break
case TokenizerState.ParamRegExp:
// it already works by escaping the closing )
// TODO: is it worth handling nested regexp? like :p(?:prefix_([^/]+)_suffix)
// https://paths.esm.dev/?p=AAMeJbiAwQEcDKbAoAAkP60PG2R6QAvgNaA6AFACM2ABuQBB#
// is this really something people need since you can also write
// /prefix_:p()_suffix
if (char === ')') {
// 如果是\)的情况,customRe = customRe去掉\ + char
if (customRe[customRe.length - 1] == '\')
customRe = customRe.slice(0, -1) + char
else state = TokenizerState.ParamRegExpEnd // 如果不是\)说明正则表达式已经遍历完
} else {
customRe += char
}
break
case TokenizerState.ParamRegExpEnd: // 正则表达式已经遍历完
// 消费buffer
consumeBuffer()
// 重置state为Static
state = TokenizerState.Static
// 例如/:id(\d+)new,当遍历到n时,使i回退,下一次进入Static分支中处理
if (char !== '*' && char !== '?' && char !== '+') i--
customRe = ''
break
default:
crash('Unknown state')
break
}
}
// 如果遍历结束后,state还是ParamRegExp状态,说明正则是没有结束的,可能漏了)
if (state === TokenizerState.ParamRegExp)
crash(`Unfinished custom RegExp for param "${buffer}"`)
// 遍历完path,进行最后一次消费buffer
consumeBuffer()
// 将segment放入tokens
finalizeSegment()
// 最后返回tokens
return tokens
}
tokenToParser
函数接收一个token数组,和一个可选的extraOptions,在函数中会构造出path对应的正则表达式,动态参数列表keys,token对应的分数,最后返回一个对象
- 正则生成:根据令牌创建匹配用的正则表达式
- 参数映射:建立参数名和正则捕获组的对应关系
- 优先级计算:确定路由匹配的优先顺序
const enum PathScore {
_multiplier = 10,
Root = 9 * _multiplier, // 只有一个/时的分数
Segment = 4 * _multiplier, // segment的基础分数
SubSegment = 3 * _multiplier, // /multiple-:things-in-one-:segment
Static = 4 * _multiplier, // type=TokenType.Static时的分数
Dynamic = 2 * _multiplier, // 动态参数分数 /:someId
BonusCustomRegExp = 1 * _multiplier, // 用户自定义正则的分数 /:someId(\d+)
BonusWildcard = -4 * _multiplier - BonusCustomRegExp, // /:namedWildcard(.*) we remove the bonus added by the custom regexp
BonusRepeatable = -2 * _multiplier, // 当正则是可重复时的分数 /:w+ or /:w*
BonusOptional = -0.8 * _multiplier, // 当正则是可选择时的分数 /:w? or /:w*
// these two have to be under 0.1 so a strict /:page is still lower than /:a-:b
BonusStrict = 0.07 * _multiplier, // options.strict: true时的分数
BonusCaseSensitive = 0.025 * _multiplier, // options.strict:true时的分数
}
const BASE_PATH_PARSER_OPTIONS: Required<_PathParserOptions> = {
sensitive: false,
strict: false,
start: true,
end: true,
}
const REGEX_CHARS_RE = /[.+*?^${}()[]/\]/g
export function tokensToParser(
segments: Array<Token[]>,
extraOptions?: _PathParserOptions
): PathParser {
const options = assign({}, BASE_PATH_PARSER_OPTIONS, extraOptions)
// 除了根段“/”之外,分数的数量与segments的长度相同
const score: Array<number[]> = []
// 正则的字符串形式
let pattern = options.start ? '^' : ''
// 保存路由中的动态参数
const keys: PathParserParamKey[] = []
for (const segment of segments) {
// 用一个数组保存token的分数,如果segment.length为0,使用PathScore.Root
const segmentScores: number[] = segment.length ? [] : [PathScore.Root]
// options.strict代表是否禁止尾部/,如果禁止了pattern追加/
if (options.strict && !segment.length) pattern += '/'
// 开始遍历每个token
for (let tokenIndex = 0; tokenIndex < segment.length; tokenIndex++) {
const token = segment[tokenIndex]
// 当前子片段(单个token)的分数:基础分数+区分大小写 ? PathScore.BonusCaseSensitive : 0
let subSegmentScore: number =
PathScore.Segment +
(options.sensitive ? PathScore.BonusCaseSensitive : 0)
if (token.type === TokenType.Static) {
// 在开始一个新的片段(tokenIndex !== 0)前pattern需要添加/
if (!tokenIndex) pattern += '/'
// 将token.value追加到pattern后。追加前token.value中的.、+、*、?、^、$等字符前面加上\
// 关于replace,参考MDN:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String/replace
pattern += token.value.replace(REGEX_CHARS_RE, '\$&')
subSegmentScore += PathScore.Static
} else if (token.type === TokenType.Param) {
const { value, repeatable, optional, regexp } = token
// 添加参数
keys.push({
name: value,
repeatable,
optional,
})
const re = regexp ? regexp : BASE_PARAM_PATTERN
// 用户自定义的正则需要验证正则的正确性
if (re !== BASE_PARAM_PATTERN) {
subSegmentScore += PathScore.BonusCustomRegExp
// 使用前确保正则是正确的
try {
new RegExp(`(${re})`)
} catch (err) {
throw new Error(
`Invalid custom RegExp for param "${value}" (${re}): ` +
(err as Error).message
)
}
}
// /:chapters*
// 如果是重复的,必须注意重复的前导斜杠
let subPattern = repeatable ? `((?:${re})(?:/(?:${re}))*)` : `(${re})`
// prepend the slash if we are starting a new segment
if (!tokenIndex)
subPattern =
// avoid an optional / if there are more segments e.g. /:p?-static
// or /:p?-:p2
optional && segment.length < 2
? `(?:/${subPattern})`
: '/' + subPattern
if (optional) subPattern += '?'
pattern += subPattern
subSegmentScore += PathScore.Dynamic
if (optional) subSegmentScore += PathScore.BonusOptional
if (repeatable) subSegmentScore += PathScore.BonusRepeatable
if (re === '.*') subSegmentScore += PathScore.BonusWildcard
}
segmentScores.push(subSegmentScore)
}
score.push(segmentScores)
}
// only apply the strict bonus to the last score
if (options.strict && options.end) {
const i = score.length - 1
score[i][score[i].length - 1] += PathScore.BonusStrict
}
// TODO: dev only warn double trailing slash
if (!options.strict) pattern += '/?'
if (options.end) pattern += '$'
// allow paths like /dynamic to only match dynamic or dynamic/... but not dynamic_something_else
else if (options.strict) pattern += '(?:/|$)'
// 根据组装好的pattern创建正则表达式,options.sensitive决定是否区分大小写
const re = new RegExp(pattern, options.sensitive ? '' : 'i')
// 根据path获取动态参数对象
function parse(path: string): PathParams | null {
const match = path.match(re)
const params: PathParams = {}
if (!match) return null
for (let i = 1; i < match.length; i++) {
const value: string = match[i] || ''
const key = keys[i - 1]
params[key.name] = value && key.repeatable ? value.split('/') : value
}
return params
}
// 根据传入的动态参数对象,转为对应的path
function stringify(params: PathParams): string {
let path = ''
// for optional parameters to allow to be empty
let avoidDuplicatedSlash: boolean = false
for (const segment of segments) {
if (!avoidDuplicatedSlash || !path.endsWith('/')) path += '/'
avoidDuplicatedSlash = false
for (const token of segment) {
if (token.type === TokenType.Static) {
path += token.value
} else if (token.type === TokenType.Param) {
const { value, repeatable, optional } = token
const param: string | string[] = value in params ? params[value] : ''
if (Array.isArray(param) && !repeatable)
throw new Error(
`Provided param "${value}" is an array but it is not repeatable (* or + modifiers)`
)
const text: string = Array.isArray(param) ? param.join('/') : param
if (!text) {
if (optional) {
// if we have more than one optional param like /:a?-static and there are more segments, we don't need to
// care about the optional param
if (segment.length < 2 && segments.length > 1) {
// remove the last slash as we could be at the end
if (path.endsWith('/')) path = path.slice(0, -1)
// do not append a slash on the next iteration
else avoidDuplicatedSlash = true
}
} else throw new Error(`Missing required param "${value}"`)
}
path += text
}
}
}
return path
}
return {
re,
score,
keys,
parse,
stringify,
}
}
createRouterMatcher的实现
export function createRouteRecordMatcher(
record: Readonly<RouteRecord>,
parent: RouteRecordMatcher | undefined,
options?: PathParserOptions
): RouteRecordMatcher {
// 生成parser对象
const parser = tokensToParser(tokenizePath(record.path), options)
// 如果有重复的动态参数命名进行提示
if (__DEV__) {
const existingKeys = new Set<string>()
for (const key of parser.keys) {
if (existingKeys.has(key.name))
warn(
`Found duplicated params with name "${key.name}" for path "${record.path}". Only the last one will be available on "$route.params".`
)
existingKeys.add(key.name)
}
}
// 将record,parent合并到parser中,同时新增children,alias属性,默认值为空数组
const matcher: RouteRecordMatcher = assign(parser, {
record,
parent,
// these needs to be populated by the parent
children: [],
alias: [],
})
if (parent) {
// 两者都是alias或两者都不是alias
if (!matcher.record.aliasOf === !parent.record.aliasOf)
parent.children.push(matcher)
}
return matcher
}
resolve
解析路由信息的核心方法,作用是将抽象的路由描述(路径,命名路由,参数等)转换成包含完整匹配信息路由对象,核心作用就是“翻译路由信息”,将开发者提供的简介的路由描述转换成包含完整路径的结构化信息
resolve根据传进来的location进行路由的匹配,找到对应的matcher的路由信息,方法接收一个location``currentLocation参数,返回一个MatcherLocation类型的对象,该对象的属性包括:name path params matched meta
function resolve(
location: Readonly<MatcherLocationRaw>,
currentLocation: Readonly<MatcherLocation>
): MatcherLocation {
let matcher: RouteRecordMatcher | undefined
let params: PathParams = {}
let path: MatcherLocation['path']
let name: MatcherLocation['name']
if ('name' in location && location.name) { // 如果location存在name属性,可根据name从matcherMap获取matcher
matcher = matcherMap.get(location.name)
if (!matcher)
throw createRouterError<MatcherError>(ErrorTypes.MATCHER_NOT_FOUND, {
location,
})
name = matcher.record.name
// 合并location.params和currentLocation中的params
params = assign(
paramsFromLocation(
currentLocation.params,
matcher.keys.filter(k => !k.optional).map(k => k.name)
),
location.params
)
// 如果不能通过params转为path抛出错误
path = matcher.stringify(params)
} else if ('path' in location) { // 如果location存在path属性,根据path从matchers获取对应matcher
path = location.path
if (__DEV__ && !path.startsWith('/')) {
warn(
`The Matcher cannot resolve relative paths but received "${path}". Unless you directly called `matcher.resolve("${path}")`, this is probably a bug in vue-router. Please open an issue at https://new-issue.vuejs.org/?repo=vuejs/router.`
)
}
matcher = matchers.find(m => m.re.test(path))
if (matcher) {
// 通过parse函数获取params
params = matcher.parse(path)!
name = matcher.record.name
}
} else { // 如果location中没有name、path属性,就使用currentLocation的name或path获取matcher
matcher = currentLocation.name
? matcherMap.get(currentLocation.name)
: matchers.find(m => m.re.test(currentLocation.path))
if (!matcher)
throw createRouterError<MatcherError>(ErrorTypes.MATCHER_NOT_FOUND, {
location,
currentLocation,
})
name = matcher.record.name
params = assign({}, currentLocation.params, location.params)
path = matcher.stringify(params)
}
// 使用一个数组存储匹配到的所有路由
const matched: MatcherLocation['matched'] = []
let parentMatcher: RouteRecordMatcher | undefined = matcher
while (parentMatcher) {
// 父路由始终在数组的开头
matched.unshift(parentMatcher.record)
parentMatcher = parentMatcher.parent
}
return {
name,
path,
params,
matched,
meta: mergeMetaFields(matched),
}
}
removeRoute
删除路由
接收参数matcherRef,removeRoute会将matcherRef对应的matcher从matcherMap和matchers中删除,并清空matcherRef对应matcher中的children和alias属性,由于matcherRef对应的matcher删除,子孙以及别名就没用了,也需要将他们删除
function removeRoute(matcherRef: RouteRecordName | RouteRecordMatcher) {
// 如果是路由名字:string或symbol
if (isRouteName(matcherRef)) {
const matcher = matcherMap.get(matcherRef)
if (matcher) {
// 删除matcher
matcherMap.delete(matcherRef)
matchers.splice(matchers.indexOf(matcher), 1)
// 清空matcher中的children与alias,
matcher.children.forEach(removeRoute)
matcher.alias.forEach(removeRoute)
}
} else {
const index = matchers.indexOf(matcherRef)
if (index > -1) {
matchers.splice(index, 1)
if (matcherRef.record.name) matcherMap.delete(matcherRef.record.name)
matcherRef.children.forEach(removeRoute)
matcherRef.alias.forEach(removeRoute)
}
}
}
getRoutes
获取所有的matcher
function getRoutes() {
return matchers
}
getRecordMatcher
根据路由的名字获取对应的matcher
function getRecordMatcher(name: RouteRecordName) {
return matcherMap.get(name)
}
insertMatcher
核心是路由的注册机制,负责将路由模式,处理函数和配选项组织起来,按照优先级插入到匹配器当中
添加matcher的时候,并不直接add的,而是根据matcher.score进行排序,比较分数时根据从数组中的每一项挨个比较,而不是总分,主要是实现动态的添加,经过对比之后将不同的地方(类似diff算法优化)进行对比操作
更新过程:
-
更新matcher的内部规则
- 将新的路由配置插入到
matcher维护的路由数或者映射表中,确保新路由能被后续的路由能被后续的匹配的逻辑识别
- 将新的路由配置插入到
-
保持匹配的优先级
- 按照路由匹配定义的顺序插入新的规则,保证“先定义者优先”的匹配原则不受动态添加影响
-
实时生效
- 插入后立即对路由跳转和URL变化生效,无需重新初始化路由实例
function insertMatcher(matcher: RouteRecordMatcher) {
let i = 0
while (
i < matchers.length &&
// matcher与matchers[i]比较,matchers[i]应该在前面
comparePathParserScore(matcher, matchers[i]) >= 0 &&
// matcher的path与matchers[i]不同或matcher不是matchers[i]的孩子
(matcher.record.path !== matchers[i].record.path ||
!isRecordChildOf(matcher, matchers[i]))
)
i++
// 插入matcher
matchers.splice(i, 0, matcher)
// 只添加原始matcher到map中
if (matcher.record.name && !isAliasRecord(matcher))
matcherMap.set(matcher.record.name, matcher)
}
// 返回0表示a与b相等;返回>0,b先排序;返回<0,a先排序
export function comparePathParserScore(a: PathParser, b: PathParser): number {
let i = 0
const aScore = a.score
const bScore = b.score
while (i < aScore.length && i < bScore.length) {
const comp = compareScoreArray(aScore[i], bScore[i])
if (comp) return comp
i++
}
return bScore.length - aScore.length
}
function compareScoreArray(a: number[], b: number[]): number {
let i = 0
while (i < a.length && i < b.length) {
const diff = b[i] - a[i]
// 一旦a与b对位索引对应的值有差值,直接返回
if (diff) return diff
i++
}
if (a.length < b.length) {
// 如果a.length为1且第一个值的分数为PathScore.Static + PathScore.Segment,返回-1,表示a先排序,否则返回1,表示b先排序
return a.length === 1 && a[0] === PathScore.Static + PathScore.Segment
? -1
: 1
} else if (a.length > b.length) {
// 如果b.length为1且第一个值的分数为PathScore.Static + PathScore.Segment,返回-1,表示b先排序,否则返回1,表示a先排序
return b.length === 1 && b[0] === PathScore.Static + PathScore.Segment
? 1
: -1
}
return 0
}
考虑token的类型,主要是路由路径解析逻辑和匹配优先级相关
token在这里指的路径解析得到的不同的片段(静态片段,动态参数),
enum TokenType {
STATIC = 'static', // 静态片段,如 /users
PARAM = 'param', // 参数,如 :id
OPTIONAL = 'optional', // 可选参数,如 :id?
WILDCARD = 'wildcard', // 通配符,如 *
REGEX = 'regex', // 正则表达式参数,如 :id(\d+)
}
总结
主要了解到了matcher是什么,以及实现
主要使用matchers和matcherMap来存储matcher,matcher中权重高matcher会优先进行匹配,matcherMap中的key主要是注册路由表的name,只存放原始的matcher
matcher是核心控制器,matcherMap是管理的数据结构
matcherMap为matcher简化matcher的查询
matcher中包含了path路由对应的正则,路由的分数,动态参数列表, 以及其他的一些相关的信息
生成matcher的过程中,会将path转化成token数组,用来正则的生成,动态参数的提取,分数的计算,等等
createRouter
基本使用
import { createRouter } from 'vue-router'
const router = defineRouter({
history: routerHistory,
strict: true,
routes: [
{
path: '/',
componet: .....
}
]
})
createRouter
这个函数主要是声明了一些函数
首先使用createRouterMatcher方法创建了一个路由匹配器matcher,从options中提取了parseQuery,stringifyQuery``history属性,没有相关的属性抛出错误
const matcher = createRouterMatcher(options.routes, options)
const parseQuery = options.parseQuery || originalParseQuery
const stringifyQuery = options.stringifyQuery || originalStringifyQuery
const routerHistory = options.history
if (__DEV__ && !routerHistory)
throw new Error(
'Provide the "history" option when calling "createRouter()":' +
' https://next.router.vuejs.org/api/#history.'
)
紧接着声明了一些全局守卫相关的变量,和一些相关的params的处理方法,其中有关全局守卫的变量都是通过useCallbacks创建的,params相关方法通过applyToParams创建
// 全局前置守卫相关方法
const beforeGuards = useCallbacks<NavigationGuardWithThis<undefined>>()
// 全局解析守卫相关方法
const beforeResolveGuards = useCallbacks<NavigationGuardWithThis<undefined>>()
// 全局后置钩子方法
const afterGuards = useCallbacks<NavigationHookAfter>()
// 当前路由,浅层响应式对象
const currentRoute = shallowRef<RouteLocationNormalizedLoaded>(
START_LOCATION_NORMALIZED
)
let pendingLocation: RouteLocation = START_LOCATION_NORMALIZED
// 如果浏览器环境下设置了scrollBehavior,那么需要防止页面自动恢复页面位置
// https://developer.mozilla.org/zh-CN/docs/Web/API/History/scrollRestoration
if (isBrowser && options.scrollBehavior && 'scrollRestoration' in history) {
history.scrollRestoration = 'manual'
}
// 标准化params,转字符串
const normalizeParams = applyToParams.bind(
null,
paramValue => '' + paramValue
)
// 编码param
const encodeParams = applyToParams.bind(null, encodeParam)
// 解码params
const decodeParams: (params: RouteParams | undefined) => RouteParams =
applyToParams.bind(null, decode)
关于callback的实现,在useCallbacks中声明一个handlers数组用来保存所有添加的方法,useCallback的返回值包括三个方法,add(添加一个handler并返回一个删除handler函数),list(返回所有的handler),reset(清空所有的handler)
export function useCallbacks<T>() {
let handlers: T[] = []
function add(handler: T): () => void {
handlers.push(handler)
return () => {
const i = handlers.indexOf(handler)
if (i > -1) handlers.splice(i, 1)
}
}
function reset() {
handlers = []
}
return {
add,
list: () => handlers,
reset,
}
}
addToParams的实现:接收一个处理函数和params对象,遍历params对象,并对每一个属性值执行fn并将结果赋值给一个新的对象
export function applyToParams(
fn: (v: string | number | null | undefined) => string,
params: RouteParamsRaw | undefined
): RouteParams {
const newParams: RouteParams = {}
for (const key in params) {
const value = params[key]
newParams[key] = Array.isArray(value) ? value.map(fn) : fn(value)
}
return newParams
}
然后就是声明了大量的函数,包括addRoute ,removeRoute,getRoutes,这个函数也是我们日常使用的
在createaRouter的最后创建了一个router对象,并将其返回,这个对象几乎包含了声明的所有函数
总结
createRouter函数声明了一些全局钩子所需的变量和很多的钩子,这些函数就是我们日常使用的一些方法,add,remove之类的,后续直接使用就行
router API
基本使用
router.addRoute({name: 'admin', path: '/admin', component: Admin})||
router.addRoute('admin', { path: 'setting', component: AdminSettings })
// add 方法不同取决于是否是嵌套路由
router.removeRoute()
router.hasRoute()
router.getRoutes()
addRoute
后续的也差不多,以addRoute为例子
在vue-router当中,matcher实现的addRoute和router的addRoute相比
本质上是同一个功能的不同层级的使用,前者是底层的依赖,后者是上层封装
- 核心功能一致:都是添加新的路由规则
- 层级不同:router是对matcher方法的封装
- 差异:使用场景和暴露范围的不同,router是暴露在外使用的api,matcher是底层实现的不支持直接操作
但是我们为什么不直接使用呢?
- 简化开发者的使用成本,使得开发者不需要关系底层的实现以及传入什么参数
- 处理边界情况和错误校验
- 隔离内部实现,保证api的稳定性
- 提高扩展的灵活性
实现:
addRoute可以接收两个参数,parentRoute(父路由的name或者一个新的路由,如果是父路由的name,name的第二个参数是必须的),record添加的新路由,返回一个删除路由的函数
function addRoute(
parentOrRoute: RouteRecordName | RouteRecordRaw,
route?: RouteRecordRaw
) {
let parent: Parameters<typeof matcher['addRoute']>[1] | undefined
let record: RouteRecordRaw
// 如果parentOrRoute是路由名称,parent为parentOrRoute对应的matcher,被添加的route是个嵌套路由
if (isRouteName(parentOrRoute)) {
parent = matcher.getRecordMatcher(parentOrRoute)
record = route!
} else { // 如果parentOrRoute不是路由名称,parentOrRoute就是要添加的路由
record = parentOrRoute
}
// 调用matcher.addRoute添加新的记录,返回一个移除路由的函数
return matcher.addRoute(record, parent)
}
其中:isRouteName:是通过name是否是string|symbol类型判断的
export function isRouteName(name: any): name is RouteRecordName {
return typeof name === 'string' || typeof name === 'symbol'
}
removeRoute
删除路由,接收一个name(现有路由的名称)参数
function removeRoute(name: RouteRecordName) {
// 根据name获取对应的routeRecordMatcher
const recordMatcher = matcher.getRecordMatcher(name)
if (recordMatcher) {
// 如果存在recordMatcher,调用matcher.removeRoute
matcher.removeRoute(recordMatcher)
} else if (__DEV__) {
warn(`Cannot remove non-existent route "${String(name)}"`)
}
}
hasRoute
判断路由是否存在,接收一个name字符串,返回一个boolean类型的值
通过matcher.getRecordMatcher来获取对应的matcher,没找到就是不存在
function hasRoute(name: RouteRecordName): boolean {
return !!matcher.getRecordMatcher(name)
}
getRoutes
获取标准化的路由列表,标准化的路由存储在matcher.record
function getRoutes() {
// 遍历matchers,routeMatcher.record中存储着路由的标准化版本
return matcher.getRoutes().map(routeMatcher => routeMatcher.record)
}
使用的都是matcher的已经实现的API
resolve
主要作用是将路由信息(如路径,命名路由,参数)解析成一个包含完整路由信息的对象,包含各种细节
接收两个参数,rawLocation,currentLocation(可选),其中的rawLocation是待转化的路由,rawLocation可以是字符串或者对象
在resolve的分支当中:
-
如果
rawLocation是string类型,调用parseURLconst locationNormalized = parseURL( parseQuery, rawLocation, currentLocation.path )parseURL接收三个参数,
parseQuery location currentLocationexport function parseURL( parseQuery: (search: string) => LocationQuery, location: string, currentLocation: string = '/' ): LocationNormalized { let path: string | undefined, query: LocationQuery = {}, searchString = '', hash = '' // location中?的位置 const searchPos = location.indexOf('?') // location中#的位置,如果location中有?,在?之后找# const hashPos = location.indexOf('#', searchPos > -1 ? searchPos : 0) // 如果 if (searchPos > -1) { // 从location中截取[0, searchPos)位置的字符串作为path path = location.slice(0, searchPos) // 从location截取含search的字符串,不包含hash部分 searchString = location.slice( searchPos + 1, hashPos > -1 ? hashPos : location.length ) // 调用parseQuery生成query对象 query = parseQuery(searchString) } // 如果location中有hash if (hashPos > -1) { path = path || location.slice(0, hashPos) // 从location中截取[hashPos, location.length)作为hash(包含#) hash = location.slice(hashPos, location.length) } // 解析以.开头的相对路径 path = resolveRelativePath(path != null ? path : location, currentLocation) // empty path means a relative query or hash `?foo=f`, `#thing` return { // fullPath = path + searchString + hash fullPath: path + (searchString && '?') + searchString + hash, path, query, hash, } }路径的解析过程 对路径进行解析,方便后续的匹配的过程
export function resolveRelativePath(to: string, from: string): string { // 如果to以/开头,说明是个绝对路径,直接返回即可 if (to.startsWith('/')) return to // 如果from不是以/开头,那么说明from不是绝对路径,也就无法推测出to的绝对路径,此时直接返回to if (__DEV__ && !from.startsWith('/')) { warn( `Cannot resolve a relative location without an absolute path. Trying to resolve "${to}" from "${from}". It should look like "/${from}".` ) return to } if (!to) return from // 使用/分割from与to const fromSegments = from.split('/') const toSegments = to.split('/') // 初始化position默认为fromSegments的最后一个索引 let position = fromSegments.length - 1 let toPosition: number let segment: string for (toPosition = 0; toPosition < toSegments.length; toPosition++) { segment = toSegments[toPosition] // 保证position不会小于0 if (position === 1 || segment === '.') continue if (segment === '..') position-- else break } return ( fromSegments.slice(0, position).join('/') + '/' + toSegments .slice(toPosition - (toPosition === toSegments.length ? 1 : 0)) .join('/') ) }最后返回对象
return assign(locationNormalized, matchedRoute, { // 对params中的value进行decodeURIComponent params:decodeParams(matchedRoute.params), // 对hash进行decodeURIComponent hash: decode(locationNormalized.hash), redirectedFrom: undefined, href, }) -
rawLocation不是string类型,是对象类型操作即可,找到对应的位置
push*
使用:
会直接进行路由的跳转,导航到对应的位置
router.push('/search?name=pen')
router.push({ path: '/search', query: { name: 'pen' } })
router.push({ name: 'search', query: { name: 'pen' } })
// 以上三种方式是等效的。
router.replace('/search?name=pen')
router.replace({ path: '/search', query: { name: 'pen' } })
router.replace({ name: 'search', query: { name: 'pen' } })
// 以上三种方式是等效的。
实现:
push方法接收参数到跳转的地方,可以是字符串或者对象,push方法当中调用了一个pushWithRediect函数,并返回结果
pushWithRedirect
方法接收两个参数,.to redirectFrom,并返回函数的结果,redirectForm就是代表to是从哪个路由重定向来的,多次的重定向只会指向最初重定向的那个路由
function pushWithRedirect(
to: RouteLocationRaw | RouteLocation,
redirectedFrom?: RouteLocation
): Promise<NavigationFailure | void | undefined> {
// ...
}
因为使用的to中可能存在重定向,所以pushWithRedirect中首先要对重定向进行处理,to中仍然存在重定向的时候,递归的调用pushWithRedirect方法
// 将to处理为规范化的路由
const targetLocation: RouteLocation = (pendingLocation = resolve(to))
// 当前路由
const from = currentRoute.value
// 使用 History API(history.state) 保存的状态
const data: HistoryState | undefined = (to as RouteLocationOptions).state
// force代表强制触发导航,即使与当前位置相同
const force: boolean | undefined = (to as RouteLocationOptions).force
// replace代表是否替换当前历史记录
const replace = (to as RouteLocationOptions).replace === true
// 获取要重定向的记录
const shouldRedirect = handleRedirectRecord(targetLocation)
// 如果需要重定向,递归调用pushWithRedirect方法
if (shouldRedirect)
return pushWithRedirect(
assign(locationAsObject(shouldRedirect), {
state: data,
force,
replace,
}),
// 重定向的根来源
redirectedFrom || targetLocation
)
处理重定向之后, 接下来就是检测即将跳转的路由和当前的路由是否是同一个路由,如果是同一个路由并且路由不强制跳转,会创建一个失败的函数,并且赋值,然后处理滚动行为
滚动行为的实现,首先从options找到scrollBehavior选项,如果不是浏览器的环境下或者不存在scollBehavior,返回一个Promise对象,相反,获取滚动位置,然后在下一次DOM刷新之后,执行定义的滚动行为函数,滚动指定位置
function handleScroll(
to: RouteLocationNormalizedLoaded,
from: RouteLocationNormalizedLoaded,
isPush: boolean,
isFirstNavigation: boolean
): Promise<any> {
const { scrollBehavior } = options
if (!isBrowser || !scrollBehavior) return Promise.resolve()
// 获取滚动位置
const scrollPosition: _ScrollPositionNormalized | null =
(!isPush && getSavedScrollPosition(getScrollKey(to.fullPath, 0))) ||
((isFirstNavigation || !isPush) &&
(history.state as HistoryState) &&
history.state.scroll) ||
null
// 下一次DOM更新后触发滚动行为,滚动行为执行完后,滚动到指定位置
return nextTick()
.then(() => scrollBehavior(to, from, scrollPosition))
.then(position => position && scrollToPosition(position))
.catch(err => triggerError(err, to, from))
}
export function getScrollKey(path: string, delta: number): string {
// history.state.position记录着当前路由在历史记录中的位置,该位置从0开始
const position: number = history.state ? history.state.position - delta : -1
// key值为 在历史记录中的位置+path
return position + path
}
export function getSavedScrollPosition(key: string) {
// 根据key值查找滚动位置
const scroll = scrollPositions.get(key)
// 查完后,删除对应记录
scrollPositions.delete(key)
return scroll
}
pushWithRedirect最后返回一个Promise,如果有failure返回failure,执行navigate(toLocation,to)
navigate
接收两个参数 to from
navigator,进行相应的操作:
- 解析目标路由 resolve
- 执行导航守卫,处理相对应的钩子函数
- 更新浏览器的历史记录
- 触发组件渲染
负责将开发者的跳转指令push转化成实际URL变化和页面更新,他是框架内部的实现细节
导航的完整的解析流程,执行过程中维护一个队列按顺序执行
- 导航被触发
- 调用失活组件中的
beforeRouteLeave钩子 - 调用全局的
beforeEach钩子 - 调用重用组件中的
beforeRouteUpdate钩子 - 调用路由配置中的
beforeEnter钩子 - 解析异步组件路由
- 调用激活组件当中的
beforeRouteEnter钩子 - 调用全局的
beforeResolve钩子
执行过程中存在runGuardQueue,只有当前的钩子执行完毕之后才会继续执行
navigate之后执行顺利最后会调用全局afterEach方法,执行finalizeNavigation,主要就是确认我们的导航,改变URL,处理滚动行为
调用最后markAsReady方法
函数会标记路由的准备状态,执行通过isReady添加的回调
function markAsReady<E = any>(err?: E): E | void {
// 只在ready=false时进行以下操作
if (!ready) {
// 如果发生错误,代表还是未准备好
ready = !err
// 设置监听器
setupListeners()
// 执行ready回调
readyHandlers
.list()
.forEach(([resolve, reject]) => (err ? reject(err) : resolve()))
// 重置ready回调列表
readyHandlers.reset()
}
return err
}
最终执行完毕
replace
replace和push执行的作用几乎相同,push指定replace: true,和直接使用replace的效果是一样的
function replace(to: RouteLocationRaw | RouteLocationNormalized) {
return push(assign(locationAsObject(to), { replace: true }))
}
这里调用了一个locationAsObject,如果to是string,会调用parseURL解析成to,关于parseURL调用实现类似resolve,解析成对应的URL
go
在访问历史中进行前进或者回退
接收一个参数delta,表示相对当前页面,移动的步数
调用的过程中会调用history.go函数,进而触发popState函数,触发事件。
这些我们在createWebHostory的useHistoryListeners中已经实现过,直接会触发
// 文件位置:src/history/html5.ts useHistoryListeners方法
window.addEventListener('popstate', popStateHandler)
const popStateHandler: PopStateListener = ({
state,
}: {
state: StateEntry | null
}) => {
// 当前location,字符串
const to = createCurrentLocation(base, location)
const from: HistoryLocation = currentLocation.value
const fromState: StateEntry = historyState.value
let delta = 0
// 如果不存在state
// 关于为什么state可能为空,可参考:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/popstate_event
if (state) {
currentLocation.value = to
historyState.value = state
// 如果暂停监听了,并且暂停时的状态是from,直接return
if (pauseState && pauseState === from) {
pauseState = null
return
}
// 计算移动的步数
delta = fromState ? state.position - fromState.position : 0
} else {
replace(to)
}
// 循环调用监听函数
listeners.forEach(listener => {
listener(currentLocation.value, from, {
delta,
type: NavigationType.pop,
direction: delta
? delta > 0
? NavigationDirection.forward
: NavigationDirection.back
: NavigationDirection.unknown,
})
})
}
监听的最后,循环调用listener,那listener是什么时候添加的呢?
push的时候调用pushWidthRedirect方法,最后执行finializeNavigation方法的最后调用markAsReady方法
function markAsReady<E = any>(err?: E): E | void {
if (!ready) {
// still not ready if an error happened
ready = !err
setupListeners()
readyHandlers
.list()
.forEach(([resolve, reject]) => (err ? reject(err) : resolve()))
readyHandlers.reset()
}
return err
}
调用了setupListeners的方法,会调用listen添加一个函数
这个函数的监听和push的过程类似,但是和push不同的是,在触发监听时候,一旦出现错误信息,需要将历史回退到相应的位置
back
go(-1)
forward
go(1)
isReady
使用:
router.isReady()
.then(() => {
// 成功
})
.catch(() => {
// 失败
})
实现:
isReady不接受任何参数,如果路由器已经完成了初始化,就会立即解析Promise,相反没有完成初始化,会将resolve reject放入到一个数组中,并添加到列表中,等待执行
全局导航守卫
主要是等待路由初始化完成的时机,确保路由系统已经完成初始化
核心的作用就是解决路由初始化中的异步问题(异步路由组件的加载,初始导航的守卫处理)
使用
beforeEach:在导航之前执行,返回一个已经删除导航守卫吃的函数beforeResolve:在导航解析之前执行,返回一个删除已经注册导航守卫函数after:在任何导航之后执行,返回一个删除已经注册导航守卫的函数
实现:
全局导航守卫和onError的实现都是通过维护一个数组进行实现,通过useCallback函数可以创建一个可以重置的列表,全局钩子,以及onError就是通过useCallback实现的
const beforeGuards = useCallbacks<NavigationGuardWithThis<undefined>>()
const beforeResolveGuards = useCallbacks<NavigationGuardWithThis<undefined>>()
const afterGuards = useCallbacks<NavigationHookAfter>()
let errorHandlers = useCallbacks<_ErrorHandler>()
const router = {
// ...
beforeEach: beforeGuards.add,
beforeResolve: beforeResolveGuards.add,
afterEach: afterGuards.add,
onError: errorHandlers.add,
// ...
}
export function useCallbacks<T>() {
let handlers: T[] = []
function add(handler: T): () => void {
handlers.push(handler)
return () => {
const i = handlers.indexOf(handler)
if (i > -1) handlers.splice(i, 1)
}
}
function reset() {
handlers = []
}
return {
add,
list: () => handlers,
reset,
}
}
useCalback返回一个对象,具有三个属性,list是内部维护的列表,add是添加handler的函数,返回一个删除的函数,reset重置函数
OnBeforerouteLeave/OnBeforeRouteUpdate
使用 这两个参数是提供的是两个composition API只能用在setup当中
使用:
export default {
setup() {
onBeforeRouteLeave() {}
onBeforeRouteUpdate() {}
}
}
onBeforeRouteLeave
export function onBeforeRouteLeave(leaveGuard: NavigationGuard) {
// 开发模式下没有组件实例,进行提示并return
if (__DEV__ && !getCurrentInstance()) {
warn(
'getCurrentInstance() returned null. onBeforeRouteLeave() must be called at the top of a setup function'
)
return
}
// matchedRouteKey是在RouterView中进行provide的,表示当前组件所匹配到到的路由记录(经过标准化处理的)
const activeRecord: RouteRecordNormalized | undefined = inject(
matchedRouteKey,
// to avoid warning
{} as any
).value
if (!activeRecord) {
__DEV__ &&
warn(
'No active route record was found when calling `onBeforeRouteLeave()`. Make sure you call this function inside of a component child of <router-view>. Maybe you called it inside of App.vue?'
)
return
}
// 注册钩子
registerGuard(activeRecord, 'leaveGuards', leaveGuard)
}
钩子的注册
function registerGuard(
record: RouteRecordNormalized,
name: 'leaveGuards' | 'updateGuards',
guard: NavigationGuard
) {
// 一个删除钩子的函数
const removeFromList = () => {
record[name].delete(guard)
}
// 卸载后移除钩子
onUnmounted(removeFromList)
// 被keep-alive缓存的组件失活时移除钩子
onDeactivated(removeFromList)
// 被keep-alive缓存的组件激活时添加钩子
onActivated(() => {
record[name].add(guard)
})
// 添加钩子,record[name]是个set,在路由标准化时处理的
record[name].add(guard)
}
onBeforeRouteUpdate
实现的差不多就是调用传入的参数不同
export function onBeforeRouteUpdate(updateGuard: NavigationGuard) {
if (__DEV__ && !getCurrentInstance()) {
warn(
'getCurrentInstance() returned null. onBeforeRouteUpdate() must be called at the top of a setup function'
)
return
}
const activeRecord: RouteRecordNormalized | undefined = inject(
matchedRouteKey,
// to avoid warning
{} as any
).value
if (!activeRecord) {
__DEV__ &&
warn(
'No active route record was found when calling `onBeforeRouteUpdate()`. Make sure you call this function inside of a component child of <router-view>. Maybe you called it inside of App.vue?'
)
return
}
registerGuard(activeRecord, 'updateGuards', updateGuard)
}
use之类的
useRoute,useRouter,useLink
useRoute:获取当前的路由信息,path params query之类的,核心就是通过inject从上下文对象中获取的,开始的时候会进行注入一个响应式的路由状态对象(包括路由信息)
useRouter:获取路由实例
useLink:处理连接导航逻辑,提供连接的激活状态,导航方法等核心功能,依赖于前两个,处理路由匹配和激活状态的计算,拿到对应的i数据直接使用navigate isActive isExactActive...
基本使用
<script lant="ts" setup>
import { useRouter, useRoute } from 'vue-router'
// router为创建的router实例
const router = useRouter()
// currentRoute当前路由
const currentRoute = useRoute()
</script>
routerLink
简单的使用
<Router-link to="/index" replace custom active="active">To IndexPage</router-link>
说明:
-
声明式路由导航,代替了传统的
href,在不刷新页面的前提下完成页面的跳转 -
直接使用设置跳转的方向
-
常见属性
- to 跳转的路径或者对象
- replave 替换当前的历史(不能后退)
- active-class 自定义激活时候的class
-
内部使用router的相关api实现
-
注意:不是触发HTTP请求,是使用JavaScript实现跳转的
export const RouterLinkImpl = /*#__PURE__*/ defineComponent({
name: 'RouterLink',
props: {
// 目标路由的链接
to: {
type: [String, Object] as PropType<RouteLocationRaw>,
required: true,
},
// 决定是否调用router.push()还是router.replace()
replace: Boolean,
// 链接被激活时,用于渲染a标签的class
activeClass: String,
// inactiveClass: String,
// 链接精准激活时,用于渲染a标签的class
exactActiveClass: String,
// 是否不应该将内容包裹在<a/>标签中
custom: Boolean,
// 传递给aria-current属性的值。https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-current
ariaCurrentValue: {
type: String as PropType<RouterLinkProps['ariaCurrentValue']>,
default: 'page',
},
},
useLink,
setup(props, { slots }) {
// 使用useLink创建router-link所需的一些属性和行为
const link = reactive(useLink(props))
// createRouter时传入的options
const { options } = inject(routerKey)!
// class对象
const elClass = computed(() => ({
[getLinkClass(
props.activeClass,
options.linkActiveClass,
'router-link-active'
)]: link.isActive, // 被激活时的class
[getLinkClass(
props.exactActiveClass,
options.linkExactActiveClass,
'router-link-exact-active'
)]: link.isExactActive, // 被精准激活的class
}))
return () => {
// 默认插槽
const children = slots.default && slots.default(link)
// 如果设置了props.custom,直接显示chldren,反之需要使用a标签包裹
return props.custom
? children
: h(
'a',
{
'aria-current': link.isExactActive
? props.ariaCurrentValue
: null,
href: link.href,
onClick: link.navigate,
class: elClass.value,
},
children
)
}
},
})
export const RouterLink = RouterLinkImpl as unknown as {
new (): {
$props: AllowedComponentProps &
ComponentCustomProps &
VNodeProps &
RouterLinkProps
$slots: {
default: (arg: UnwrapRef<ReturnType<typeof useLink>>) => VNode[]
}
}
useLink: typeof useLink
}
routerView
设置路由出口,设置展示的页面出口
渲染流程:
- 导航触发
- 调用失活组件当中的
beforeRouteLeave钩子 - 调用全局组件
beforeEach钩子 - 调用重用组件中的
beforeRouteUpdate钩子 - 调用路由配置中的
beforeEnter钩子 - 解析异步路由组件
- 调用激活组件中的
beforeRouteEnter钩子 - 调用全局
afterEach钩子 - 导航被确认
- 调用全局
afterEach钩子 - DOM更新
- 调用
beforeEnter守卫传递给next的回调函数,创建好的组件,会作为回调函数的参数传入
eg:
当我们访问localhost:3000接口的时候,是怎样进行渲染的呢?(渲染流程)
首先我们知道vue-router在install的时候,会立即进行一次路由的跳转,并且立马向app注入一个默认的currentRoute,此时router-view会根据,这个currentRoute进行第一次的渲染(initalNavigation)
因为这个默认的currentRoute中的matched是空的,所以第一次渲染的结果式空的
const START_LOCATION_NORMALIZED = {
path: '/',
query: {},
params: {},
hash: '',
matched: [],
name: null,
...
}
,当第一次路由跳转完毕之后,会执行finalizeNavigator方法,
function finalizeNavigation(to, from, replaced, redirectedFrom) {
// 1. 更新 router.currentRoute
currentRoute.value = to // 响应式赋值!
// 2. 触发导航完成钩子(afterEach)
triggerAfterEach(to, from, null)
// 3. 解决首次加载的 promise(如果有 await router.isReady())
}
这个方法更新currentRoute,这时currentRoute中就可以找到对应的渲染组件(target),router-view完成第二次渲染之后,紧接着触发router-view当中的watch,将最新的组件实例赋值给to.instance[name],并循环执行callback(拿到真实的组件)
然后我们进行跳转的时候,假设使用的是push方法,同样在跳转完成之后,会执行finalizeNavigator函数,更新currentRoute,这是会监听到currentRoute的改变,找到需要渲染的组件,将其显示,在渲染前先执行旧组件的卸载功能,将对应的路由设置成null,完成渲染之后,触发watch,将最新的组件实例赋值给to.instance[name],并执行enterCallback函数
浏览器
│
▼
加载 Vue 应用
│
▼
VueRouter.install() → 初始化 router 实例
│
├── 创建响应式 currentRoute(初始值:START_LOCATION_NORMALIZED)
│ matched: [], path: '/'
│
▼
<router-view> 首次渲染
│
├── 读取 currentRoute.matched
│
└── matched 为空 → 渲染 null(页面空白)
│
▼
Vue Router 自动触发 initial navigation(初始导航)
│
├── 解析当前 URL: '/'
│
├── 匹配路由表 → 找到 { path: '/', component: Home }
│
├── 生成完整 to 对象:
│ {
│ path: '/',
│ matched: [ { path: '/', component: Home } ],
│ ...
│ }
│
▼
调用 finalizeNavigation(to)
│
├── currentRoute.value = to ← 响应式更新!
│
└── 触发导航完成钩子(afterEach)
│
▼
<router-view> 的 watch 监听到 currentRoute 变化
│
├── 重新计算 matched[0].component → Home
│
├── 创建 Home 组件实例
│
├── 执行 Home 的 beforeRouteEnter(如有)
│
└── 渲染 Home 组件到页面 ✅
总的就是
先将matched中的数据进行变化,拿到最新的路由信息
然后将currentRoute进行赋值(finalizeNavigation)to参数
最后watch自动监听到route的变化,找到对应的新组件
进行旧组件的卸载(同时调用一个callback钩子函数)
渲染新的组件,创建组件实例,其中调用特定的函数,帮助组件的渲染