文章目录
前言
- 本文仅做大纲整理,方便日后回顾。所以很少贴代码(贴上太多代码看着乱遭)。
- 本次阅读源码原因:项目需要多窗口
一、源码大纲
名词解释
- 当前路由信息(currentRouter):当前匹配到的路由–this.$route
- 路由实例(router):this.$router
第一部分:创建
- 入口
// src/router.ts
/**
* 创建一个路由实例,且包含install注册方法
* 参数options中必须包含routes数组与history管理实例
* @param options - {@link RouterOptions}
* @return router
*/
export function createRouter(options: RouterOptions): Router {
}
- 创建做了这些事:
- 分析路由树配置信息routes,得到matcher:储存路由匹配规则,负责匹配path工作。
- 保存历史管理容器。
- 创建路由守卫队列;当前路由信息容器(reactive响应式,方便viewRouter组件监听)
- 创键操作路由方法,主要有:新增、删除、重定向(pushWithRedirect)、事件处理(事件注册、navigate、triggerAfterEach)
第二部分:注册到vue(vue.use) src/router.ts: install
1. 注册组件: RouterLink、RouterView
2. 提供外部访问变量:currentRoute =>$route; router实例 => app.config.globalProperties.$router
3. 调用push方法初始化页面
4. 将路由实例(router)、当前路由信息(currentRouter)注入到app
5. 包装app.unmount方法,触发时销毁自身
第三部分:使用
1. 导航方式:routerLink、push、replace
2. 导航触发后调用:重定向入口(pushWithRedirect)
3. 渲染组件:routerView,
监听当前路由信息
渲染组件(组件搬运工):匹配路由,将得到的组件返回
二、执行路由变更
1.起点
- 该方法为处理路由变更的入口。一般由push和repleace调用
// /src/router.ts
function pushWithRedirect(
to: RouteLocationRaw | RouteLocation,
redirectedFrom?: RouteLocation
): Promise<NavigationFailure | void | undefined> {
console.log('重定向路由');
// 1. 将to标准化成路由描述对象,更新pendingLocation
const targetLocation: RouteLocation = (pendingLocation = resolve(to))
// 2. 当前路由信息转给from
const from = currentRoute.value
// 3. 获取路由配置信息(数据、?、更新当前路由)
const data: HistoryState | undefined = (to as RouteLocationOptions).state
const force: boolean | undefined = (to as RouteLocationOptions).force
// to could be a string where `replace` is a function
const replace = (to as RouteLocationOptions).replace === true
// 4. 是否需要重定向
const shouldRedirect = handleRedirectRecord(targetLocation)
// 需要重定向重新调用此方法
if (shouldRedirect)
return pushWithRedirect(
assign(shouldRedirect, { state: data, force, replace }),
// keep original redirectedFrom if it exists
redirectedFrom || targetLocation
)
// if it was a redirect we already called `pushWithRedirect` above
const toLocation = targetLocation as RouteLocationNormalized
// 5. 记录重定向来源(如果有的话)
toLocation.redirectedFrom = redirectedFrom
let failure: NavigationFailure | void | undefined
// 6. from与to是否相同
if (!force && isSameRouteLocation(stringifyQuery, from, targetLocation)) {
failure = createRouterError<NavigationFailure>(
ErrorTypes.NAVIGATION_DUPLICATED,
{ to: toLocation, from }
)
// trigger scroll to allow scrolling to the same anchor
handleScroll(
from,
from,
// this is a push, the only way for it to be triggered from a
// history.listen is with a redirect, which makes it become a push
true,
// This cannot be the first navigation because the initial location
// cannot be manually navigated to
false
)
}
// 7. 如果未失败,正常调用navigate进行导航
return (failure ? Promise.resolve(failure) : navigate(toLocation, from))
.catch((error: NavigationFailure | NavigationRedirectError) =>
isNavigationFailure(error)
? error
: // reject any unknown error
triggerError(error)
)
.then((failure: NavigationFailure | NavigationRedirectError | void) => {
if (failure) {
if (
isNavigationFailure(failure, ErrorTypes.NAVIGATION_GUARD_REDIRECT)
) {
if (
__DEV__ &&
// we are redirecting to the same location we were already at
isSameRouteLocation(
stringifyQuery,
resolve(failure.to),
toLocation
) &&
// and we have done it a couple of times
redirectedFrom &&
// @ts-ignore
(redirectedFrom._count = redirectedFrom._count
? // @ts-ignore
redirectedFrom._count + 1
: 1) > 10
) {
warn(
`Detected an infinite redirection in a navigation guard when going from "${from.fullPath}" to "${toLocation.fullPath}". Aborting to avoid a Stack Overflow. This will break in production if not fixed.`
)
return Promise.reject(
new Error('Infinite redirect in navigation guard')
)
}
return pushWithRedirect(
// keep options
assign(locationAsObject(failure.to), {
state: data,
force,
replace,
}),
// preserve the original redirectedFrom if any
redirectedFrom || toLocation
)
}
} else {
// 8. 开始执行导航
// if we fail we don't finalize the navigation
failure = finalizeNavigation(
toLocation as RouteLocationNormalizedLoaded,
from,
true,
replace,
data
)
}
// 9. 调用After事件
triggerAfterEach(
toLocation as RouteLocationNormalizedLoaded,
from,
failure
)
return failure
})
}
2.navigate 处理事件
-
=> 局部离开守卫(beforeRouteLeave)
=> leavingRecords(匹配到的from数组)组件中的守卫
=> leavingRecords(匹配到的from数组)路由配置中的守卫
=> 检查from、to -
=> 全局离开守卫:beforeGuards
-
=> 局部路由更新(beforeRouteUpdate)
-
=> 局部(路由配置)的beforeEnter
=> to.matched(匹配到的新路由数组)路由配置中的守卫
=> 检查from、to -
=> enterCallbacks
=> enteringRecords(匹配到的to数组)组件中的守卫
=> 检查from、to -
=> 全局进入新路由守卫beforeResolveGuards
=> ebeforeResolveGuards中的守卫
=> 检查from、to(全局完成导航守卫,下一步进行)
-
=> finalizeNavigation(执行导航)
=> history操作:添加/覆盖
=> 更新当前路由信息
=> wait Render(此时触发routerView监听,具体下面会说到)
=> 调用scrollBehavior -
=> triggerAfterEach 执行导航后事件
注:处理事件是同步执行,利用promise + Array.reduce实现的
三、routerView组件
routerView主要职责=>
=> 创建路由(setup)
=> 1. 获取渲染所需路由信息
=> 2. 向下传递路由信息,使子viewRouter找到渲染的组件:1)已匹配路由;2)下层路由层数
=> 3. 监听路由变化
=> 4. 返回渲染函数
=> 1. 得到匹配组件
=> 2. 返回组件
=> 注:如使用插槽返回插槽所需数据;未使用插槽将根据注册组件进行渲染
=> 各层级如何匹配并选择自己的路由,matched 根据自身深度寻找组件
// src\RouterView.ts
export const RouterViewImpl = /*#__PURE__*/ defineComponent({
name: 'RouterView',
props: {
name: {
type: String as PropType<string>,
default: 'default',
},
route: Object as PropType<RouteLocationNormalizedLoaded>,
},
setup(props, { attrs, slots }) {
__DEV__ && warnDeprecatedUsage()
console.log('router-view创建')
/** 1. 获取渲染所需路由信息 */
const injectedRoute = inject(routeLocationKey)!
const depth = inject(viewDepthKey, 0)
const matchedRouteRef = computed<RouteLocationMatched | undefined>(
() => {
return (props.route || injectedRoute).matched[depth]
}
)
/** 2. 向下传递路由信息,使子viewRouter找到渲染的组件:1)已匹配路由;2)下层路由层数 */
provide(viewDepthKey, depth + 1)
provide(matchedRouteKey, matchedRouteRef)
const viewRef = ref<ComponentPublicInstance>()
// watch at the same time the component instance, the route record we are
// rendering, and the name
/** 3. 监听路由变化 */
watch(
() => [viewRef.value, matchedRouteRef.value, props.name] as const,
([instance, to, name], [oldInstance, from, oldName]) => {
// copy reused instances
if (to) {
// this will update the instance for new instances as well as reused
// instances when navigating to a new route
to.instances[name] = instance
// the component instance is reused for a different route or name so
// we copy any saved update or leave guards
if (from && from !== to && instance && instance === oldInstance) {
to.leaveGuards = from.leaveGuards
to.updateGuards = from.updateGuards
}
}
// trigger beforeRouteEnter next callbacks
if (
instance &&
to &&
// if there is no instance but to and from are the same this might be
// the first visit
(!from || !isSameRouteRecord(to, from) || !oldInstance)
) {
;(to.enterCallbacks[name] || []).forEach(callback =>
callback(instance)
)
}
},
{ flush: 'post' }
)
return () => {
console.log('创建一个routerView实例');
// 1. 得到匹配组件
const route = props.route || injectedRoute
const matchedRoute = matchedRouteRef.value
const ViewComponent = matchedRoute && matchedRoute.components[props.name]
// we need the value at the time we render because when we unmount, we
// navigated to a different location so the value is different
const currentName = props.name
// 未匹配到:有插槽加入变量;没有返回空
if (!ViewComponent) {
if(slots.default) {
console.log(slots.default.toString());
let r = slots.default({ Component: ViewComponent, route })
return r;
}
return null
}
// 匹配到
// props from route configuration
const routePropsOption = matchedRoute!.props[props.name]
const routeProps = routePropsOption
? routePropsOption === true
? route.params
: typeof routePropsOption === 'function'
? routePropsOption(route)
: routePropsOption
: null
const onVnodeUnmounted: VNodeProps['onVnodeUnmounted'] = vnode => {
// remove the instance reference to prevent leak
if (vnode.component!.isUnmounted) {
matchedRoute!.instances[currentName] = null
}
}
// 创建vnode
const component = h(
ViewComponent,
assign({}, routeProps, attrs, {
onVnodeUnmounted,
ref: viewRef,
})
)
// pass the vnode to the slot as a prop.
// h and <component :is="..."> both accept vnodes
// 返回(规则同上,需要判断是否有插槽)
let r = slots.default
? slots.default({ Component: component, route })
: component
return r
}
},
})