前言
在上一篇文章Vue-Router入门(三) :嵌套路由和编程式导航介绍了编程式导航,了解了ts类型及底层的一部分源码,明白了路由导航是通过path或name进行相关导航,path是路径,name是命名路由,本片文章我们来学习命名路由的相关知识点。
命名路由
name表示路由的命名,任何一个路由都能够设置name属性,当我们想要跳转到某个路由时可以用name属性表示。
js
// 使用router-link跳转
<router-link :to="{ name: 'home'}">home</router-link>
// 使用router跳转
router.push({ name: 'home' })
相比于 path
,name
有以下优点:
- 没有硬编码的 URL
params
的自动编码/解码。- 防止你在 url 中出现打字错误。
- 绕过路径排序(如显示一个)
name就是路由的名称,ts类型为RouteRecordName**:**
js
type RouteRecordName = string | symbol
源码解析
本部分所涉及的所有源码都在这里源码位置,作者也在文章末尾附加了用到了的函数源码。
name导航解析
上节讲解编程式导航时看了一下实现源码,导航方法是使用history API导航到具体的location,不管是path还是name都要找到对应的location,path本身就是url路径一部分比较好理解,name路由名称跟location关联性就不大了,我们看下源码是怎么处理的。
首先在pushWithRedirect
函数中通过这段代码
js
const targetLocation: RouteLocation = (pendingLocation = resolve(to))
使用resolve处理传过来的to路由,得到location,
接着name
会使用matcher
中的resolve
。
js
const matchedRoute = matcher.resolve(matcherLocation, currentLocation);
我们看一下matcher
的定义,通过matcher
文件夹中的index.ts
中的createRouterMatcher
函数创建的。具体源码在末尾
js
const matcher = createRouterMatcher(options.routes, options)
接着在createRouterMatcher
函数中的resolve
函数
js
matcher = matcherMap.get(location.name)
通过matcherMap获取了matcher,matcherMap是一个字典,我们来看下它的定义:
js
const matcherMap = new Map<RouteRecordName, RouteRecordMatcher>()
小结
根据上面的源码我们可以梳理下用name导航时的过程:
- 在添加routes时,通过matcherMap将路由name与路由route映射起来。
- 在调用编程式导航方法时,会判断to参数中是否包含name字段。
- 通过matcher.resolve方法去获取name对应的route信息。
- 根据route信息获取跳转的url进行跳转。
paht导航解析
path处理就很简单,只有path时,把path拼接一下调用parseURL;path与name一起时会从params参数入手。
js
if (rawLocation.path != null) {
if (
__DEV__ &&
'params' in rawLocation &&
!('name' in rawLocation) &&
Object.keys(rawLocation.params).length
) {
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 {
const targetParams = assign({}, rawLocation.params)
for (const key in targetParams) {
if (targetParams[key] == null) {
delete targetParams[key]
}
}
matcherLocation = assign({}, rawLocation, {
params: encodeParams(targetParams),
})
currentLocation.params = encodeParams(currentLocation.params)
}
总结
path与name两者都能用于路由导航,name命名路由的出现感觉更多是为了方便开发,具体的区别可以简单总结一下:
- path会经过百分号编码。
- path用query传参,name用params传参。
- 两者源码处理逻辑不同。
附加源码
router.ts中的resolve函数
js
function resolve(
rawLocation: Readonly<RouteLocationRaw>,
currentLocation?: RouteLocationNormalizedLoaded
): RouteLocation & { href: string } {
// const objectLocation = routerLocationAsObject(rawLocation)
// we create a copy to modify it later
currentLocation = assign({}, currentLocation || currentRoute.value)
if (typeof rawLocation === 'string') {
const locationNormalized = parseURL(
parseQuery,
rawLocation,
currentLocation.path
)
const matchedRoute = matcher.resolve(
{ path: locationNormalized.path },
currentLocation
)
const href = routerHistory.createHref(locationNormalized.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 "${rawLocation}"`)
}
}
// locationNormalized is always a new object
return assign(locationNormalized, matchedRoute, {
params: decodeParams(matchedRoute.params),
hash: decode(locationNormalized.hash),
redirectedFrom: undefined,
href,
})
}
if (__DEV__ && !isRouteLocation(rawLocation)) {
warn(
`router.resolve() was passed an invalid location. This will fail in production.\n- Location:`,
rawLocation
)
rawLocation = {}
}
let matcherLocation: MatcherLocationRaw
// path could be relative in object as well
if (rawLocation.path != null) {
if (
__DEV__ &&
'params' in rawLocation &&
!('name' in rawLocation) &&
// @ts-expect-error: the type is never
Object.keys(rawLocation.params).length
) {
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 {
// remove any nullish param
const targetParams = assign({}, rawLocation.params)
for (const key in targetParams) {
if (targetParams[key] == null) {
delete targetParams[key]
}
}
// pass encoded values to the matcher, so it can produce encoded path and fullPath
matcherLocation = assign({}, rawLocation, {
params: encodeParams(targetParams),
})
// current location params are decoded, we need to encode them in case the
// matcher merges the params
currentLocation.params = encodeParams(currentLocation.params)
}
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}".`
)
}
// the matcher might have merged current location params, so
// we need to run the decoding again
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 "${
rawLocation.path != null ? rawLocation.path : rawLocation
}"`
)
}
}
return assign(
{
fullPath,
// keep the hash encoded so fullPath is effectively path + encodedQuery +
// hash
hash,
query:
// if the user is using a custom query lib like qs, we might have
// nested objects, so we keep the query as is, meaning it can contain
// numbers at `$route.query`, but at the point, the user will have to
// use their own type anyway.
// https://github.com/vuejs/router/issues/328#issuecomment-649481567
stringifyQuery === originalStringifyQuery
? normalizeQuery(rawLocation.query)
: ((rawLocation.query || {}) as LocationQuery),
},
matchedRoute,
{
redirectedFrom: undefined,
href,
}
)
}
matcher中的createRouterMatcher函数源码
js
export function createRouterMatcher(
routes: Readonly<RouteRecordRaw[]>,
globalOptions: PathParserOptions
): RouterMatcher {
// normalized ordered array of matchers
const matchers: RouteRecordMatcher[] = []
const matcherMap = new Map<RouteRecordName, RouteRecordMatcher>()
globalOptions = mergeOptions(
{ strict: false, end: true, sensitive: false } as PathParserOptions,
globalOptions
)
function getRecordMatcher(name: RouteRecordName) {
return matcherMap.get(name)
}
function addRoute(
}
function removeRoute(matcherRef: RouteRecordName | RouteRecordMatcher) {
}
function getRoutes() {
return matchers
}
function insertMatcher(matcher: RouteRecordMatcher) {
}
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) {
matcher = matcherMap.get(location.name)
if (!matcher)
throw createRouterError<MatcherError>(ErrorTypes.MATCHER_NOT_FOUND, {
location,
})
// warn if the user is passing invalid params so they can debug it better when they get removed
if (__DEV__) {
const invalidParams: string[] = Object.keys(
location.params || {}
).filter(paramName => !matcher!.keys.find(k => k.name === paramName))
if (invalidParams.length) {
warn(
`Discarded invalid param(s) "${invalidParams.join(
'", "'
)}" when navigating. See https://github.com/vuejs/router/blob/main/packages/router/CHANGELOG.md#414-2022-08-22 for more details.`
)
}
}
name = matcher.record.name
params = assign(
// paramsFromLocation is a new object
paramsFromLocation(
currentLocation.params,
// only keep params that exist in the resolved location
// only keep optional params coming from a parent record
matcher.keys
.filter(k => !k.optional)
.concat(
matcher.parent ? matcher.parent.keys.filter(k => k.optional) : []
)
.map(k => k.name)
),
// discard any existing params in the current location that do not exist here
// #1497 this ensures better active/exact matching
location.params &&
paramsFromLocation(
location.params,
matcher.keys.map(k => k.name)
)
)
// throws if cannot be stringified
path = matcher.stringify(params)
} else if (location.path != null) {
// no need to resolve the path with the matcher as it was provided
// this also allows the user to control the encoding
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://github.com/vuejs/router/issues/new/choose.`
)
}
matcher = matchers.find(m => m.re.test(path))
// matcher should have a value after the loop
if (matcher) {
// we know the matcher works because we tested the regexp
params = matcher.parse(path)!
name = matcher.record.name
}
// location is a relative path
} else {
// match by name or path of current route
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
// since we are navigating to the same location, we don't need to pick the
// params like when `name` is provided
params = assign({}, currentLocation.params, location.params)
path = matcher.stringify(params)
}
const matched: MatcherLocation['matched'] = []
let parentMatcher: RouteRecordMatcher | undefined = matcher
while (parentMatcher) {
// reversed order so parents are at the beginning
matched.unshift(parentMatcher.record)
parentMatcher = parentMatcher.parent
}
return {
name,
path,
params,
matched,
meta: mergeMetaFields(matched),
}
}
// add initial routes
routes.forEach(route => addRoute(route))
return { addRoute, resolve, removeRoute, getRoutes, getRecordMatcher }
}