前言
【vue-router 源码】系列文章将带你从 0 开始理解 vue-router
的具体实现。该系列文章源码参考 vue-router v4.0.15
。源码地址:https://github.com/vuejs/router
浏览该文章的前提是你最好理解 vue-router
的根本应用,如果你没有应用过的话,可通过 vue-router 官网学习下。
该篇文章将介绍 router.resolve
的实现。
应用
router.resolve
办法返回路由地址的标准化版本。
router.resolve('admin')
router.resolve({path: '/admin'})
resolve
resolve
接管两个参数:rawLocation
、currentLocation
(可选)。其中 rawLocation
是待转换的路由,rawLocation
能够是个对象也能够是个字符串。currentLocation
不传默认是currentRoute
。
在 resolve
中有是两个分支:
-
如果
rawLocation
是string
类型
调用parseURL
解析rawLocation
:const locationNormalized = parseURL( parseQuery, rawLocation, currentLocation.path )
parseURL
接管三个参数:parseQuery
(一个 query 解析函数)、location
(被解析的location
)、currentLocation
(以后的location
)。export 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('/') ) }
to=cc
,from=/aa/bb
,通过resolveRelativePath
后:/aa/cc
to=cc
,from=/aa/bb/
,通过resolveRelativePath
后:/aa/bb/cc
to=./cc
,from=/aa/bb
,通过resolveRelativePath
后:/aa/cc
to=./cc
,from=/aa/bb/
,通过resolveRelativePath
后:/aa/bb/cc
to=../cc
,from=/aa/bb
,通过resolveRelativePath
后:/aa
to=../cc
,from=/aa/bb/
,通过resolveRelativePath
后:/aa/cc
如果from/
,to=cc
、to=./cc
、to=../cc
、to=../../cc
、to=./../cc
、to=.././cc
通过resolveRelativePath
始终返回/cc
回到 resolve
中,解析完 rawLocation
后,调用matcher.resolve
const matchedRoute = matcher.resolve({ path: locationNormalized.path},
currentLocation
)
// 应用 routerHistory.createHref 创立 href
const href = routerHistory.createHref(locationNormalized.fullPath)
最初返回对象:
return assign(locationNormalized, matchedRoute, {
// 对 params 中的 value 进行 decodeURIComponent
params:decodeParams(matchedRoute.params),
// 对 hash 进行 decodeURIComponent
hash: decode(locationNormalized.hash),
redirectedFrom: undefined,
href,
})
rawLocation
不是string
类型
let matcherLocation: MatcherLocationRaw
// 如果 rawLocation 中有 path 属性
if ('path' in rawLocation) {
// rawLocation 中的 params 会被疏忽
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.`
)
}
// 解决 path 为绝对路径
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]
}
}
// 对 params 进行编码
matcherLocation = assign({}, rawLocation, {params: encodeParams(rawLocation.params),
})
// 将以后地位的 params 编码 以后地位的参数被解码,咱们须要对它们进行编码以防匹配器合并参数
currentLocation.params = encodeParams(currentLocation.params)
}
// 调用 matcher.resolve 获取路由相干信息
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}".`
)
}
// 因为 matcher 曾经合并了以后地位的参数,所以须要进行解码
matchedRoute.params = normalizeParams(decodeParams(matchedRoute.params))
// 生成残缺 path
const fullPath = stringifyURL(
stringifyQuery,
assign({}, rawLocation, {hash: encodeHash(hash),
path: matchedRoute.path,
})
)
// routerHistory.createHref 会删除 #之前的任意字符
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:
// 如果 query 是个嵌套对象,normalizeQuery 会将嵌套的对象 toString,如果用户应用 qs 等库,咱们须要放弃 query 的状态
// https://github.com/vuejs/router/issues/328#issuecomment-649481567
stringifyQuery === originalStringifyQuery
? normalizeQuery(rawLocation.query)
: ((rawLocation.query || {}) as LocationQuery),
},
matchedRoute,
{
redirectedFrom: undefined,
href,
}
)