Vue Router
源码分析(三)-- RouterLink
作为Vue Router
提供的两大组件之一,RouterLink
想必没有人会陌生。虽然都知道是渲染了个a
标签,想着今日无事,若仔细观摩,兴许还能得到点 ,嗯,自我满足感。
1. RouterLinkOptions
RouterLinkOptions
只有to
和replace
两个属性,很好理解。
export interface RouterLinkOptions {
/**
* Route Location the link should navigate to when clicked on.
*/
to: RouteLocationRaw
/**
* Calls `router.replace` instead of `router.push`.
*/
replace?: boolean
// TODO: refactor using extra options allowed in router.push. Needs RFC
}
2. RouterLinkProps
RouterLinkProps
是RouterLink
实际接收的参数类型,在继承了RouterLinkOptions
的to
和replace
的基础上,还多了一些属性,除了custom
用于使用插槽自定义RouterLink
内部的内容之外,其它的属性无需多大关注。
export interface RouterLinkProps extends RouterLinkOptions {
/**
* Whether RouterLink should not wrap its content in an `a` tag. Useful when
* using `v-slot` to create a custom RouterLink
*/
custom?: boolean
/**
* Class to apply when the link is active
*/
activeClass?: string
/**
* Class to apply when the link is exact active
*/
exactActiveClass?: string
/**
* Value passed to the attribute `aria-current` when the link is exact active.
*
* @defaultValue `'page'`
*/
ariaCurrentValue?:
| 'page'
| 'step'
| 'location'
| 'date'
| 'time'
| 'true'
| 'false'
}
3. useLink
用于得到link
对象,link
对象包含了各种路由相关的信息。
- 解析
props.to
得到route
; - 通过
route
和当前路由currentRoute
计算出activeRecordIndex
; - 进而根据
activeRecordIndex
以及route
和currentRoute
中的params
比对,得到isActive
,来标记路由是否激活; navigate
是点击链接触发的事件,用于导航至其它路由;- 最后组合起来得到
link
对象。
// TODO: we could allow currentRoute as a prop to expose `isActive` and
// `isExactActive` behavior should go through an RFC
export function useLink(props: UseLinkOptions) {
const router = inject(routerKey)!
const currentRoute = inject(routeLocationKey)!
let hasPrevious = false
let previousTo: unknown = null
const route = computed(() => {
const to = unref(props.to)
if (__DEV__ && (!hasPrevious || to !== previousTo)) {
if (!isRouteLocation(to)) {
if (hasPrevious) {
warn(
`Invalid value for prop "to" in useLink()\n- to:`,
to,
`\n- previous to:`,
previousTo,
`\n- props:`,
props
)
} else {
warn(
`Invalid value for prop "to" in useLink()\n- to:`,
to,
`\n- props:`,
props
)
}
}
previousTo = to
hasPrevious = true
}
return router.resolve(to)
})
const activeRecordIndex = computed<number>(() => {
const { matched } = route.value
const { length } = matched
const routeMatched: RouteRecord | undefined = matched[length - 1]
const currentMatched = currentRoute.matched
if (!routeMatched || !currentMatched.length) return -1
const index = currentMatched.findIndex(
isSameRouteRecord.bind(null, routeMatched)
)
if (index > -1) return index
// possible parent record
const parentRecordPath = getOriginalPath(
matched[length - 2] as RouteRecord | undefined
)
return (
// we are dealing with nested routes
length > 1 &&
// if the parent and matched route have the same path, this link is
// referring to the empty child. Or we currently are on a different
// child of the same parent
getOriginalPath(routeMatched) === parentRecordPath &&
// avoid comparing the child with its parent
currentMatched[currentMatched.length - 1].path !== parentRecordPath
? currentMatched.findIndex(
isSameRouteRecord.bind(null, matched[length - 2])
)
: index
)
})
const isActive = computed<boolean>(
() =>
activeRecordIndex.value > -1 &&
// 还要判断`params`是否一致
includesParams(currentRoute.params, route.value.params)
)
const isExactActive = computed<boolean>(
() =>
activeRecordIndex.value > -1 &&
// `activeRecordIndex.vaue`值必须等于`matched`的最后一个,才是精准激活的路由
activeRecordIndex.value === currentRoute.matched.length - 1 &&
isSameRouteLocationParams(currentRoute.params, route.value.params)
)
// 有效的导航事件会触发`replace`或`push`
function navigate(
e: MouseEvent = {} as MouseEvent
): Promise<void | NavigationFailure> {
if (guardEvent(e)) {
return router[unref(props.replace) ? 'replace' : 'push'](
unref(props.to)
// avoid uncaught errors are they are logged anyway
).catch(noop)
}
return Promise.resolve()
}
// devtools only
if ((__DEV__ || __FEATURE_PROD_DEVTOOLS__) && isBrowser) {
const instance = getCurrentInstance()
if (instance) {
const linkContextDevtools: UseLinkDevtoolsContext = {
route: route.value,
isActive: isActive.value,
isExactActive: isExactActive.value,
error: null,
}
// @ts-expect-error: this is internal
instance.__vrl_devtools = instance.__vrl_devtools || []
// @ts-expect-error: this is internal
instance.__vrl_devtools.push(linkContextDevtools)
watchEffect(
() => {
linkContextDevtools.route = route.value
linkContextDevtools.isActive = isActive.value
linkContextDevtools.isExactActive = isExactActive.value
linkContextDevtools.error = isRouteLocation(unref(props.to))
? null
: 'Invalid "to" value'
},
{ flush: 'post' }
)
}
}
/**
* NOTE: update {@link _RouterLinkI}'s `$slots` type when updating this
*/
return {
route,
href: computed(() => route.value.href),
isActive,
isExactActive,
navigate,
}
}
4. RouterLinkImpl
到这里,也就只有一个setup
是新的内容。通过useLink
得到link
对象,最后渲染出RouterLink
的内容,默认是渲染a
标签,各项属性都来自于link
对象;但如果custom
属性为真值,那useLink
得到的东西也用不上咯,就只渲染插槽里自定义的东西。
export const RouterLinkImpl = /*#__PURE__*/ defineComponent({
name: 'RouterLink',
compatConfig: { MODE: 3 },
props: {
to: {
type: [String, Object] as PropType<RouteLocationRaw>,
required: true,
},
replace: Boolean,
activeClass: String,
// inactiveClass: String,
exactActiveClass: String,
custom: Boolean,
ariaCurrentValue: {
type: String as PropType<RouterLinkProps['ariaCurrentValue']>,
default: 'page',
},
},
useLink,
setup(props, { slots }) {
const link = reactive(useLink(props))
const { options } = inject(routerKey)!
const elClass = computed(() => ({
[getLinkClass(
props.activeClass,
options.linkActiveClass,
'router-link-active'
)]: link.isActive,
// [getLinkClass(
// props.inactiveClass,
// options.linkInactiveClass,
// 'router-link-inactive'
// )]: !link.isExactActive,
[getLinkClass(
props.exactActiveClass,
options.linkExactActiveClass,
'router-link-exact-active'
)]: link.isExactActive,
}))
return () => {
const children = slots.default && slots.default(link)
// 渲染`RouterLink`内部的内容,根据`custom`来决定是否加一层`a`标签
return props.custom
? children
: h(
'a',
{
'aria-current': link.isExactActive
? props.ariaCurrentValue
: null,
href: link.href,
// this would override user added attrs but Vue will still add
// the listener, so we end up triggering both
onClick: link.navigate,
class: elClass.value,
},
children
)
}
},
})
RouterLink
的原理就到这儿咯。这大热天的,出门吃个饭人都要化了,还是待在屋子里吃着雪糕吹着空调学习,才比较舒坦。至于VueRouter
,准备再看个RouterView
就结束,一天天的看这些个源码,给孩子累的。