前端路由原理:vue-router源码剖析
vue-router 入口分析
createRouter 如何实现,完整的代码 在gitHub上router/router.ts at main · vuejs/router · GitHub
参数 RouterOptions 是规范我们配置的路由对象,主要包含 history、routes 等数据。routes 就是我们需要配置的路由对象,类型是 RouteRecordRaw 组成的数组,并且 RouteRecordRaw 的类型是三个类型的合并。然后返回值的类型 Router 就是包含了 addRoute、push、beforeEnter、install 方法的一个对象,并且维护了 currentRoute 和 options 两个属性。
并且每个类型方法还有详细的注释,这也极大降低了阅读源码的门槛,可以帮助我们在看到函数的类型时就知道函数大概的功能。我们知道 Vue 中 app.use 实际上执行的就是 router 对象内部的 install 方法,我们先进入到 install 方法看下是如何安装的
// createRouter传递参数的类型export interface RouterOptions extends PathParserOptions {history: RouterHistoryroutes: RouteRecordRaw[]scrollBehavior?: RouterScrollBehavior...}// 每个路由配置的类型export type RouteRecordRaw =| RouteRecordSingleView| RouteRecordMultipleViews| RouteRecordRedirect//... other config// Router接口的全部方法和属性export interface Router {readonly currentRoute: Ref<RouteLocationNormalizedLoaded>readonly options: RouterOptionsaddRoute(parentName: RouteRecordName, route: RouteRecordRaw):() => voidaddRoute(route: RouteRecordRaw): () => voidRoute(name: RouteRecordName): voidhasRoute(name: RouteRecordName): booleangetRoutes(): RouteRecord[]resolve(to: RouteLocationRaw,currentLocation?: RouteLocationNormalizedLoaded): RouteLocation & { href: string }push(to: RouteLocationRaw): Promise<NavigationFailure | void |undefined>replace(to: RouteLocationRaw): Promise<NavigationFailure | void |undefined>back(): ReturnType<Router['go']>forward(): ReturnType<Router['go']>go(delta: number): voidbeforeEach(guard: NavigationGuardWithThis<undefined>): () => voidbeforeResolve(guard: NavigationGuardWithThis<undefined>): () =>voidafterEach(guard: NavigationHookAfter): () => voidonError(handler: _ErrorHandler): () => voidisReady(): Promise<void>install(app: App): void}export function createRouter(options: RouterOptions): Router {}
路由安装
createRouter 的最后,创建了包含 addRoute、push 等方法的对象,并且 install 方法内部注册了 RouterLink 和 RouterView 两个组件。所以我们可以在任何组件内部直接使用
然后使用 computed 把路由变成响应式对象,存储在 reactiveRoute 对象中,再通过 app.provide 给全局注册了 route 和 reactive 包裹后的 reactiveRoute 对象。我们之前介绍 provide 函数的时候也介绍了,provide 提供的数据并没有做响应式的封装,需要响应式的时候需要自己使用 ref 或者 reactive 封装为响应式对象,最后注册 unmount 方法实现 vue-router 的安装。
export function createRouter(options: RouterOptions): Router {....let started: boolean | undefinedconst installedApps = new Set<App>()// 路由对象const router: Router = {currentRoute,addRoute,removeRoute,hasRoute,getRoutes,resolve,options,push,replace,go,back: () => go(-1),forward: () => go(1),beforeEach: beforeGuards.add,beforeResolve: beforeResolveGuards.add,afterEach: afterGuards.add,onError: errorHandlers.add,isReady,// 插件按章install(app: App) {const router = this// 注册全局组件 router-link和router-viewapp.component('RouterLink', RouterLink)app.component('RouterView', RouterView)app.config.globalProperties.$router = routerObject.defineProperty(app.config.globalProperties, '$route', {enumerable: true,get: () => unref(currentRoute),})if (isBrowser &&!started &¤tRoute.value === START_LOCATION_NORMALIZED) {// see abovestarted = truepush(routerHistory.location).catch(err => {if (__DEV__) warn('Unexpected error when starting therouter:', err)})}const reactiveRoute = {} as {[k in keyof RouteLocationNormalizedLoaded]: ComputedRef<RouteLocationNormalizedLoaded[k]>}for (const key in START_LOCATION_NORMALIZED) {// @ts-expect-error: the key matchesreactiveRoute[key] = computed(() => currentRoute.value[key])}// 提供全局配置app.provide(routerKey, router)app.provide(routeLocationKey, reactive(reactiveRoute))app.provide(routerViewLocationKey, currentRoute)const unmountApp = app.unmountinstalledApps.add(app)app.unmount = function () {installedApps.delete(app)// ...unmountApp()}if ((__DEV__ || __FEATURE_PROD_DEVTOOLS__) && isBrowser) {addDevtools(app, router, matcher)}},}return router}
路由对象创建和安装之后,我们下一步需要了解的就是 router-link 和 router-view 两个组件的实现方式
RouterView 的 setup 函数返回了一个函数,这个函数就是 RouterView 组件的 render 函数。大部分我们使用的方式就是一个
matchedRoute 依赖的 matchedRouteRef 的计算逻辑在如下代码的第 12~15 行,数据来源 injectedRoute 就是上面我们注入的 currentRoute 对象。
export const RouterViewImpl = /*#__PURE__*/ defineComponent({name: 'RouterView',props: {name: {type: String as PropType<string>,default: 'default',},route: Object as PropType<RouteLocationNormalizedLoaded>,},// router-view组件源码setup(props, { attrs, slots }) {// 全局的reactiveRoute对象注入const injectedRoute = inject(routerViewLocationKey)!const routeToDisplay = computed(() => props.route ||injectedRoute.value)const depth = inject(viewDepthKey, 0)const matchedRouteRef = computed<RouteLocationMatched |undefined>(() => routeToDisplay.value.matched[depth])// 嵌套层级provide(viewDepthKey, depth + 1)// 匹配的router对象provide(matchedRouteKey, matchedRouteRef)provide(routerViewLocationKey, routeToDisplay)const viewRef = ref<ComponentPublicInstance>()// 返回的render函数return () => {const route = routeToDisplay.valueconst matchedRoute = matchedRouteRef.valueconst ViewComponent = matchedRoute && matchedRoute.components[props.name]const currentName = props.nameif (!ViewComponent) {return normalizeSlot(slots.default, { Component:ViewComponent, route })}// props from route configurationconst routePropsOption = matchedRoute!.props[props.name]const routeProps = routePropsOption? routePropsOption === true? route.params: typeof routePropsOption === 'function'? routePropsOption(route): routePropsOption: nullconst onVnodeUnmounted: VNodeProps['onVnodeUnmounted'] =vnode => {// remove the instance reference to prevent leakif (vnode.component!.isUnmounted) {matchedRoute!.instances[currentName] = null}}// 创建需要渲染组件的虚拟domconst component = h(ViewComponent,assign({}, routeProps, attrs, {onVnodeUnmounted,ref: viewRef,}))return (// pass the vnode to the slot as a prop.// h and <component :is="..."> both accept vnodesnormalizeSlot(slots.default, { Component: component, route})||component)}},})
路由更新
RouterView 渲染的组件是由当前匹配的路由变量 matchedRoute 决定的。接下来我们回到 createRouter 函数中,可以看到 matcher 对象是由 createRouterMatcher 创建,createRouterMatcher 函数传入 routes 配置的路由数组,并且返回创建的 RouterMatcher 对象,内部遍历 routes 数组,通过 addRoute 挨个处理路由配置。
export function createRouter(options: RouterOptions): Router {const matcher = createRouterMatcher(options.routes, options)///....}export function createRouterMatcher(routes: RouteRecordRaw[],globalOptions: PathParserOptions): RouterMatcher {// matchers数组const matchers: RouteRecordMatcher[] = []// matcher对象const matcherMap = new Map<RouteRecordName, RouteRecordMatcher>()globalOptions = mergeOptions({ strict: false, end: true, sensitive: false } as PathParserOptions,globalOptions)function addRoute(){}function remoteRoute(){}function getRoutes(){return matchers}function insertMatcher(){}function resolve(){}// add initial routesroutes.forEach(route => addRoute(route))return { addRoute, resolve, removeRoute, getRoutes, getRecordMatcher }}
addRoute 函数内部通过 createRouteRecordMatcher 创建扩展之后的 matcher 对象,包括了 record、parent、children 等树形,可以很好地描述路由之间的嵌套父子关系。这样整个路由对象就已经创建完毕,那我们如何在路由切换的时候寻找到正确的路由对象呢?
function addRoute(record: RouteRecordRaw,parent?: RouteRecordMatcher,originalRecord?: RouteRecordMatcher){if ('alias' in record) {// 标准化alias}for (const normalizedRecord of normalizedRecords) {// ...matcher = createRouteRecordMatcher(normalizedRecord, parent, options)insertMatcher(matcher)}return originalMatcher? () => {// since other matchers are aliases, they should be removed by the original matcherremoveRoute(originalMatcher!)}: noop}export function createRouteRecordMatcher(record: Readonly<RouteRecord>,parent: RouteRecordMatcher | undefined,options?: PathParserOptions): RouteRecordMatcher {const parser = tokensToParser(tokenizePath(record.path), options)const matcher: RouteRecordMatcher = assign(parser, {record,parent,// these needs to be populated by the parentchildren: [],alias: [],})if (parent) {if (!matcher.record.aliasOf === !parent.record.aliasOf)parent.children.push(matcher)}return matcher}
在 vue-router 中,路由更新可以通过 router-link 渲染的链接实现,也可以使用 router 对象的 push 等方法实现。下面的代码中,router-link 组件内部也是渲染一个 a 标签,并且注册了 a 标签的 onClick 函数,内部也是通过 router.replace 或者 router.push 来实现。
export const RouterLinkImpl = /*#__PURE__*/ defineComponent({name: 'RouterLink',props: {to: {type: [String, Object] as PropType<RouteLocationRaw>,required: true,},...},// router-link源码setup(props, { slots }) {const link = reactive(useLink(props))const { options } = inject(routerKey)!const elClass = computed(() => ({...}))return () => {const children = slots.default && slots.default(link)return props.custom? children: h('a',{href: link.href,onClick: link.navigate,class: elClass.value,},children)}},})// 跳转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()}
createRouter 函数中,可以看到 push 函数直接调用了 pushWithRedirect 函数来实现,内部通过 resolve(to) 生成 targetLocation 变量。这个变量会赋值给 toLocation,然后执行 navigate(toLocation) 函数。而这个函数内部会执行一系列的导航守卫函数,最后会执行 finalizeNavigation 函数完成导航。
function push(to: RouteLocationRaw | RouteLocation) {return pushWithRedirect(to)}function replace(to: RouteLocationRaw | RouteLocationNormalized) {return push(assign(locationAsObject(to), { replace: true }))}// 路由跳转函数function pushWithRedirect(to: RouteLocationRaw | RouteLocation,redirectedFrom?: RouteLocation): Promise<NavigationFailure | void | undefined> {const targetLocation: RouteLocation = (pendingLocation = resolve(to))const from = currentRoute.valueconst data: HistoryState | undefined = (to as RouteLocationOptions).stateconst force: boolean | undefined = (to as RouteLocationOptions).force// to could be a string where `replace` is a functionconst replace = (to as RouteLocationOptions).replace === trueconst toLocation = targetLocation as RouteLocationNormalizedreturn (failure ? Promise.resolve(failure) : navigate(toLocation, from)).catch((error: NavigationFailure | NavigationRedirectError) =>isNavigationFailure(error)? error: // reject any unknown errortriggerError(error, toLocation, from)).then((failure: NavigationFailure | NavigationRedirectError | void) => {failure = finalizeNavigation(toLocation as RouteLocationNormalizedLoaded,from,true,replace,data)triggerAfterEach(toLocation as RouteLocationNormalizedLoaded,from,failure)return failure})}
finalizeNavigation 函数内部通过 routerHistory.push 或者 replace 实现路由跳转,并且更新 currentRoute.value。
currentRoute 就是我们在 install 方法中注册的全局变量 route,每次页面跳转currentRoute都会更新为toLocation,在任意组件中都可以通过route 变量来获取当前路由的数据,最后在 handleScroll 设置滚动行为
routerHistory 在 createRouter 中通过 option.history 获取,就是我们创建 vue-router 应用时通过 createWebHistory 或者 createWebHashHistory 创建的对象。createWebHistory 返回的是 HTML5 的 history 模式路由对象,createWebHashHistory 是 Hash 模式的路由对象。
function finalizeNavigation(toLocation: RouteLocationNormalizedLoaded,from: RouteLocationNormalizedLoaded,isPush: boolean,replace?: boolean,data?: HistoryState): NavigationFailure | void {const isFirstNavigation = from === START_LOCATION_NORMALIZEDconst state = !isBrowser ? {} : history.stateif (isPush) {if (replace || isFirstNavigation)routerHistory.replace(toLocation.fullPath)else routerHistory.push(toLocation.fullPath, data)}// accept current navigationcurrentRoute.value = toLocationhandleScroll(toLocation, from, isPush, isFirstNavigation)markAsReady()}function markAsReady(err?: any): void {if (ready) returnready = truesetupListeners()readyHandlers.list().forEach(([resolve, reject]) => (err ? reject(err) : resolve()))readyHandlers.reset()}
createWebHashHistory 和 createWebHistory 的实现,内部都是通过 useHistoryListeners 实现路由的监听,通过 useHistoryStateNavigation 实现路由的切换。useHistoryStateNavigation 会返回 push 或者 replace 方法来更新路由,这两个函数你可以在GitHub上自行学习github.com
export function createWebHashHistory(base?: string): RouterHistory {base = location.host ? base || location.pathname + location.search : ''// allow the user to provide a `#` in the middle: `/base/#/app`if (!base.includes('#')) base += '#'return createWebHistory(base)}export function createWebHistory(base?: string): RouterHistory {base = normalizeBase(base)const historyNavigation = useHistoryStateNavigation(base)const historyListeners = useHistoryListeners(base,historyNavigation.state,historyNavigation.location,historyNavigation.replace)function go(delta: number, triggerListeners = true) {if (!triggerListeners) historyListeners.pauseListeners()history.go(delta)}const routerHistory: RouterHistory = assign({// it's overridden right afterlocation: '',base,go,createHref: createHref.bind(null, base),},historyNavigation,historyListeners)Object.defineProperty(routerHistory, 'location', {enumerable: true,get: () => historyNavigation.location.value,})Object.defineProperty(routerHistory, 'state', {enumerable: true,get: () => historyNavigation.state.value,})return routerHistory}
createRouter 函数入口函数,createRouter 函数返回了 router 对象,router 对象提供了 addRoute、push 等方法,并且在 install 方法中实现了路由,注册了组件 router-link 和 router-view
然后通过 createRouterMatcher 创建路由匹配对象,并且在路由变化的时候维护 currentRoute,让你可以在每个组件内部 router和route 获取路由匹配的数据,并且动态渲染当前路由匹配的组件到 router-view 组件内部,实现了前端的路由系统。
