我会把整个源码过一遍,整理出来并实现一个简易版的vue-router。 背源码不是目的,通过对源码的学习,理解设计思想,提升coding的能力才是关键。
1.基本使用
//https://router.vuejs.org/zh/guide/essentials/navigation.html
// 字符串路径
router.push('/users/eduardo')
// 带有路径的对象
router.push({ path: '/users/eduardo' })
// 命名的路由,并加上参数,让路由建立 url
router.push({ name: 'user', params: { username: 'eduardo' } })
// 带查询参数,结果是 /register?plan=private
router.push({ path: '/register', query: { plan: 'private' } })
// 带 hash,结果是 /about#team
router.push({ path: '/about', hash: '#team' })
2. 源码分析
下面来看看源码中的具体实现:
function push(to: RouteLocationRaw) {
return pushWithRedirect(to)
}
RouteLocationRaw类型参考: router.vuejs.org/zh/api/#rou…
进入pushWithRedirect :
const targetLocation: RouteLocation = (pendingLocation = resolve(to))
const shouldRedirect = handleRedirectRecord(targetLocation)
if (shouldRedirect)
return pushWithRedirect(
assign(locationAsObject(shouldRedirect), {
state:
typeof shouldRedirect === 'object'
? assign({}, data, shouldRedirect.state)
: data,
force,
replace,
}),
redirectedFrom || targetLocation
)
function pushWithRedirect(
to: RouteLocationRaw | RouteLocation,
redirectedFrom?: RouteLocation
): Promise<NavigationFailure | void | undefined> {
//resolve方法用于解析 URL 和路由配置之间的映射关系
const Redirect: RouteLocation = (pendingLocation = resolve(to))
const from = currentRoute.value
const data: HistoryState | undefined = (to as RouteLocationOptions).state
//force是即使跳转路由相同也会跳转
const force: boolean | undefined = (to as RouteLocationOptions).force
//如果replace是true,那么即使force为true,也不会添加一条历史记录
//如果replace为false,force为true,那么依然会跳转,并且添加一条记录
const replace = (to as RouteLocationOptions).replace === true
//这个方法主要就是判断要跳转的路由有没有redirect
const shouldRedirect = handleRedirectRecord(targetLocation)
//如果是重定向的路由, 就把重定向的目标路由传入这个方法继续调用,完成后直接返回
if (shouldRedirect)
return pushWithRedirect(
assign(locationAsObject(shouldRedirect), {
state:
typeof shouldRedirect === 'object'
? assign({}, data, shouldRedirect.state)
: data,
force,
replace,
}),
redirectedFrom || targetLocation
)
// 到这重定向的路由已经处理完了
const toLocation = targetLocation as RouteLocationNormalized
toLocation.redirectedFrom = redirectedFrom
let failure: NavigationFailure | void | undefined
//如果force为false(路由相同是不跳转)并且当前路由和跳转路由相同
if (!force && isSameRouteLocation(stringifyQuery, from, targetLocation)) {
//跳转失败
failure = createRouterError<NavigationFailure>(
ErrorTypes.NAVIGATION_DUPLICATED,
{ to: toLocation, from }
)
// 触发滚动
handleScroll(
from,
from,
//是否通过push触发
true,
//不可能是第一次导航,因为最开始的定位是不能通过手动导航到的
false
)
}
//跳转到指定的路由
return (failure ? Promise.resolve(failure) : navigate(toLocation, from))
.catch((error: NavigationFailure | NavigationRedirectError) =>
isNavigationFailure(error)
? isNavigationFailure(error, ErrorTypes.NAVIGATION_GUARD_REDIRECT)
? error: markAsReady(error)
: triggerError(error, toLocation, from)
)
.then((failure: NavigationFailure | NavigationRedirectError | void) => {
//处理跳转失败的情况, 这里的具体失败情况处理可以参照文档:
//https://router.vuejs.org/zh/guide/advanced/navigation-failures.html
if (failure) {
if (
isNavigationFailure(failure, ErrorTypes.NAVIGATION_GUARD_REDIRECT)
) {
if (
__DEV__ &&
// 重定向到已经到达的相同位置
isSameRouteLocation(
stringifyQuery,
resolve(failure.to),
toLocation
) &&
// 重复执行,如果30次后依然会进入这个判断,就报错
redirectedFrom &&
(redirectedFrom._count =
redirectedFrom._count ? redirectedFrom._count + 1 : 1) > 30
) {
warn(
`Detected a possibly infinite redirection in a navigation guard when going from "${from.fullPath}" to "${toLocation.fullPath}". Aborting to avoid a Stack Overflow. This might break in production if not fixed.`
)
return Promise.reject(
new Error('Infinite redirect in navigation guard')
)
}
//如果跳转失败就继续递归调用,并且允许重定向替换记录
return pushWithRedirect(
assign(
{replace},
locationAsObject(failure.to),
{
state:
typeof failure.to === 'object'
? assign({}, data, failure.to.state)
: data,
force,
}
),
redirectedFrom || toLocation
)
}
} else {
// 如果这个失败是不能继续递归的, 就触发路由守卫
failure = finalizeNavigation(
toLocation as RouteLocationNormalizedLoaded,
from,
true,
replace,
data
)
}
triggerAfterEach(
toLocation as RouteLocationNormalizedLoaded,
from,
failure
)
return failure
})
}
resolve方法:
function resolve(
rawLocation: Readonly<RouteLocationRaw>,
currentLocation?: RouteLocationNormalizedLoaded
): RouteLocation & { href: string } {
currentLocation = assign({}, currentLocation || currentRoute.value)
//如果传入的是字符串,如: router.push('/login')
if (typeof rawLocation === 'string') {
// 该对象包含了标准化后的 `fullpath` 和 `path` 属性,以及其他属性(如 `hash`、`query`)。
const locationNormalized = parseURL(
parseQuery,
rawLocation,
currentLocation.path
)
//字符串和routes匹配的结果
const matchedRoute = matcher.resolve(
{ path: locationNormalized.path },
currentLocation
)
//routerHistory.createHref会删除#之前的任意字符
const href = routerHistory.createHref(locationNormalized.fullPath)
if (__DEV__) {
//如果开发环境下的location是//开头,就报警告
if (href.startsWith('//'))
warn(
`Location "${rawLocation}" resolved to "${href}". A resolved location cannot start with multiple slashes.`
)
else if (!matchedRoute.matched.length) {
//没匹配到相对应的路由
warn(`No match found for location with path "${rawLocation}"`)
}
}
//
return assign(locationNormalized, matchedRoute, {
params: decodeParams(matchedRoute.params),
hash: decode(locationNormalized.hash),
redirectedFrom: undefined,
href,
})
}
let matcherLocation: MatcherLocationRaw
//如果传入的是一个对象,并且有path属性
if ('path' in rawLocation) {
if (
__DEV__ &&
'params' in rawLocation &&
!('name' in rawLocation) &&
// @ts-expect-error: the type is never
Object.keys(rawLocation.params).length
) {
//如果有params, 建议用name匹配
warn(
`Path "${rawLocation.path}" was passed with params but they will be ignored. Use a named route alongside params instead.`
)
}
//更新
matcherLocation = assign({}, rawLocation, {
path: parseURL(parseQuery, rawLocation.path, currentLocation.path).path,
})
} else {
//删除无效的params
const targetParams = assign({}, rawLocation.params)
for (const key in targetParams) {
if (targetParams[key] == null) {
delete targetParams[key]
}
}
//对params编码
matcherLocation = assign({}, rawLocation, {
params: encodeParams(rawLocation.params),
})
// 对当前location的params解码
currentLocation.params = encodeParams(currentLocation.params)
}
//和routes匹配
const matchedRoute = matcher.resolve(matcherLocation, currentLocation)
const hash = rawLocation.hash || ''
if (__DEV__ && hash && !hash.startsWith('#')) {
warn(
`A `hash` should always start with the character "#". Replace "${hash}" with "#${hash}".`
)
}
//下面的操作和上面基本类似
matchedRoute.params = normalizeParams(decodeParams(matchedRoute.params))
const fullPath = stringifyURL(
stringifyQuery,
assign({}, rawLocation, {
hash: encodeHash(hash),
path: matchedRoute.path,
})
)
const href = routerHistory.createHref(fullPath)
if (__DEV__) {
if (href.startsWith('//')) {
warn(
`Location "${rawLocation}" resolved to "${href}". A resolved location cannot start with multiple slashes.`
)
} else if (!matchedRoute.matched.length) {
warn(
`No match found for location with path "${
'path' in rawLocation ? rawLocation.path : rawLocation
}"`
)
}
}
return assign(
{
//返回完整的路径
fullPath,
hash,
query:
//stringifyQuery: routes里的query,
//originalStringifyQuery: url拼接的query
//normalizeQuery:将query中数组的每一项或其他值转换为字符串
//如果query中不存在对象,则直接标准化处理,否则不处理直接返回
stringifyQuery === originalStringifyQuery
? normalizeQuery(rawLocation.query)
: ((rawLocation.query || {}) as LocationQuery),
},
matchedRoute,
{
redirectedFrom: undefined,
href,
}
)
}
handleScroll方法:处理滚动行为
参照文档:router.vuejs.org/zh/guide/ad…
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
return nextTick()
.then(() => scrollBehavior(to, from, scrollPosition))
.then(position => position && scrollToPosition(position))
.catch(err => triggerError(err, to, from))
}
navigate 方法: 跳转的方法
function navigate(
to: RouteLocationNormalized,
from: RouteLocationNormalizedLoaded
): Promise<any> {
let guards: Lazy<any>[]
//通过 from.matched, to.matched 两个数组的操作获取差异的特征
const [leavingRecords, updatingRecords, enteringRecords] =
extractChangingRecords(to, from)
// 提取组件内的·beforeRouteLeave·钩子函数
guards = extractComponentsGuards(
leavingRecords.reverse(),
'beforeRouteLeave',
to,
from
)
for (const record of leavingRecords) {
record.leaveGuards.forEach(guard => {
guards.push(guardToPromiseFn(guard, to, from))
})
}
//如果有新的导航触发,那么取消并跳过现在的路由守卫,可能为null
const canceledNavigationCheck = checkCanceledNavigationAndReject.bind(
null,
to,
from
)
guards.push(canceledNavigationCheck)
// 按顺序执行所有的beforeRouteLeave(promise)
return (
runGuardQueue(guards)
.then(() => {
// check global guards beforeEach
guards = []
for (const guard of beforeGuards.list()) {
guards.push(guardToPromiseFn(guard, to, from))
}
guards.push(canceledNavigationCheck)
return runGuardQueue(guards)
})
.then(() => {
//收集并执行 ·beforeRouteUpdate· 的钩子(顺序执行)
// check in components beforeRouteUpdate
guards = extractComponentsGuards(
updatingRecords,
'beforeRouteUpdate',
to,
from
)
for (const record of updatingRecords) {
record.updateGuards.forEach(guard => {
guards.push(guardToPromiseFn(guard, to, from))
})
}
guards.push(canceledNavigationCheck)
// run the queue of per route beforeEnter guards
return runGuardQueue(guards)
})
.then(() => {
// 收集并顺序执行beforeEnter钩子
// 在进入特定于此记录的守卫之前。如果记录有`重定向`属性,则beforeEnter无效
guards = []
for (const record of to.matched) {
//如果有循环跳转则不重复触发
if (record.beforeEnter && !from.matched.includes(record)) {
if (isArray(record.beforeEnter)) {
for (const beforeEnter of record.beforeEnter)
guards.push(guardToPromiseFn(beforeEnter, to, from))
} else {
guards.push(guardToPromiseFn(record.beforeEnter, to, from))
}
}
}
guards.push(canceledNavigationCheck)
return runGuardQueue(guards)
})
.then(() => {
to.matched.forEach(record => (record.enterCallbacks = {}))
// 收集组件内的beforeRouteEnter钩子并顺序执行
guards = extractComponentsGuards(
enteringRecords,
'beforeRouteEnter',
to,
from
)
guards.push(canceledNavigationCheck)
return runGuardQueue(guards)
})
.then(() => {
//收集全局的beforeResolve钩子(导航即将被解析之前执行的导航守卫)
guards = []
for (const guard of beforeResolveGuards.list()) {
guards.push(guardToPromiseFn(guard, to, from))
}
guards.push(canceledNavigationCheck)
return runGuardQueue(guards)
})
// 跳转失败
.catch(err =>
isNavigationFailure(err, ErrorTypes.NAVIGATION_CANCELLED)
? err
: Promise.reject(err)
)
)
}
这个方法在源码中这里的注释应该是有问题的😄
3.总结
- 首先通过参数找到对应的route
- 判断这个路由是否有
重定向属性,如果有,则继续递归调用 - 判断即将前往的路由和当前路由是否相同,如果相同,则记录下来,并且触发滚动行为(是否滚动在这个行为中进行详细的判断处理)
- 最后就是跳转操作,所有的钩子函数会按照顺序触发,可以在官网了解到更详细的组件激活的流程。这里的具体操作在源码的
navigationGuards.ts中