vue-router 使用与原理分析,测试结果来啦
创始人
2024-01-29 01:15:50
0

简介

Vue Router 是Vue.js的官方路由。与Vue.js核心深度集成,让用Vue.js构建单页应用(SPA)变得更加简单。

 

对于开发和维护管理后台类的前端项目,页面结构和组合可能非常复杂,所以正确的理解和使用Vue Router就显得尤为重要。

使用

创建

1、在安装好Vue Router依赖后,在App.vue中引入router-view,它是渲染的容器

2、创建路由router/index.js


const routes = [{ path: '/', component: Home},{ path: '/login', name: 'login', component: Login},
]
const  router = createRouter({history: createWebHistory(),routes: routes,
})
export default router

3、在main.js中使用路由

import router from "./router";
const app = createApp(App)app.use(router)app.mount('#app')

然后就可以在任意组件中使用this.$router形式访问它,并且以 this.$route 的形式访问当前路由:

// Home.vue
export default {computed: {username() {// 我们很快就会看到 `params` 是什么return this.$route.params.username},},methods: {goToDashboard() {if (isAuthenticated) {this.$router.push('/dashboard')} else {this.$router.push('/login')}},},
}

嵌套路由

一些应用程序的 UI 由多层嵌套的组件组成。在这种情况下,URL 的片段通常对应于特定的嵌套组件结构,例如:

/user/johnny/profile                     /user/johnny/posts
+------------------+                  +-----------------+
| User             |                  | User            |
| +--------------+ |                  | +-------------+ |
| | Profile      | |  +------------>  | | Posts       | |
| |              | |                  | |             | |
| +--------------+ |                  | +-------------+ |
+------------------+                  +-----------------+

在上层app节点的顶层router-view下,又包含的组件自己嵌套的router-view,例如以上的user模版:

const User = {template: `

User {{ $route.params.id }}

`, }

要将组件渲染到这个嵌套的router-view中,我们需要在路由中配置 children

const routes = [{path: '/user/:id',component: User,children: [{// 当 /user/:id/profile 匹配成功// UserProfile 将被渲染到 User 的  内部path: 'profile',component: UserProfile,},{// 当 /user/:id/posts 匹配成功// UserPosts 将被渲染到 User 的  内部path: 'posts',component: UserPosts,},],},
]

下面我们从源码的角度看下页面是如何加载并显示到页面上的

原理

上面基础的使用方法可以看出,主要包含三个步骤:

  1. 创建createRouter,并在app中use使用这个路由
  2. 在模版中使用router-view标签
  3. 导航push,跳转页面

从routers声明的数组结构可以看出,声明的路由path会被注册成路由表指向component声明的组件,并在push方法调用时,从路由表查出对应组件并加载。下面看下源码是如何实现这一过程的,Vue Router源码分析版本为4.1.5

创建安装

首先看下createRouter方法实现:

/*** Creates a Router instance that can be used by a Vue app.** @param options - {@link RouterOptions}*/
export function createRouter(options: RouterOptions): Router {const matcher = createRouterMatcher(options.routes, options)// ...function addRoute(parentOrRoute: RouteRecordName | RouteRecordRaw,route?: RouteRecordRaw) {// ...}function getRoutes() {return matcher.getRoutes().map(routeMatcher => routeMatcher.record)}function hasRoute(name: RouteRecordName): boolean {return !!matcher.getRecordMatcher(name)}function push(to: RouteLocationRaw) {return pushWithRedirect(to)}function replace(to: RouteLocationRaw) {return push(assign(locationAsObject(to), { replace: true }))}// ...const router: Router = {currentRoute,listening: true,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,// 在app全局安装routerinstall(app: App) {const router = this// 全局注册组件RouterLink、RouterViewapp.component('RouterLink', RouterLink)app.component('RouterView', RouterView)// 全局声明router实例,this.$router访问app.config.globalProperties.$router = router// 全局注册this.$route 访问当前路由currentRouteObject.defineProperty(app.config.globalProperties, '$route', {enumerable: true,get: () => unref(currentRoute),})// this initial navigation is only necessary on client, on server it doesn't// make sense because it will create an extra unnecessary navigation and could// lead to problemsif (isBrowser &&// used for the initial navigation client side to avoid pushing// multiple times when the router is used in multiple apps!started &¤tRoute.value === START_LOCATION_NORMALIZED) {// see above// 浏览器情况下,push一个初始页面,不指定url默认首页‘/’started = truepush(routerHistory.location).catch(err => {if (__DEV__) warn('Unexpected error when starting the router:', err)})}// ...app.provide(routerKey, router)app.provide(routeLocationKey, reactive(reactiveRoute))// 全局注入当前路由currentRouteapp.provide(routerViewLocationKey, currentRoute)// ...},}return router
}

createRouter方法返回了当前路由实例,内部初始化了一些路由的常用方法,和在组件中打印this.$router结构是一样的,那install方法是在哪里调用的呢?在安装时调用了app.use(router),看下use方法,在runtime-core.cjs.prod.js下:

use(plugin, ...options) {if (installedPlugins.has(plugin)) ;else if (plugin && shared.isFunction(plugin.install)) {installedPlugins.add(plugin);// 如果是插件,调用插件的install方法,并把当前app传入plugin.install(app, ...options);}else if (shared.isFunction(plugin)) {installedPlugins.add(plugin);plugin(app, ...options);}else ;return app;},

至此已经完成了全局的router创建安装,并可以在代码中使用router-viewthis.$router和实例的一些方法了,那么页面上是如何展示被加载的component呢?需要看下渲染组件router-view的内部实现

渲染

install方法注册了RouterView组件,实现在RouterView.ts

/*** Component to display the current route the user is at.*/
export const RouterView = RouterViewImpl as unknown as {// ...
}
复制代码

RouterViewImpl实现:


export const RouterViewImpl = /*#__PURE__*/ defineComponent({name: 'RouterView',// ...setup(props, { attrs, slots }) {__DEV__ && warnDeprecatedUsage()// 拿到之前注册的currentRouteconst injectedRoute = inject(routerViewLocationKey)!// 当前要显示的route,监听route值变化时会刷新const routeToDisplay = computed(() => props.route || injectedRoute.value)// 获取当前router-view深度层级,在嵌套路由时使用const injectedDepth = inject(viewDepthKey, 0)// 在当前router-view深度下去匹配要显示的路由matched// matched 是个数组,在resolve方法被赋值,如果有匹配到则在当前router-view渲染const depth = computed(() => {let initialDepth = unref(injectedDepth)const { matched } = routeToDisplay.valuelet matchedRoute: RouteLocationMatched | undefinedwhile ((matchedRoute = matched[initialDepth]) &&!matchedRoute.components) {initialDepth++}return initialDepth})const matchedRouteRef = computed(() => routeToDisplay.value.matched[depth.value])provide(viewDepthKey,computed(() => depth.value + 1))provide(matchedRouteKey, matchedRouteRef)provide(routerViewLocationKey, routeToDisplay)const viewRef = ref()// watch at the same time the component instance, the route record we are// rendering, and the name// 监听匹配路由变化时,刷新  watch(() => [viewRef.value, matchedRouteRef.value, props.name] as const,([instance, to, name], [oldInstance, from, oldName]) => {// ...},{ flush: 'post' })return () => {const route = routeToDisplay.value// we need the value at the time we render because when we unmount, we// navigated to a different location so the value is differentconst currentName = props.nameconst matchedRoute = matchedRouteRef.valueconst ViewComponent =matchedRoute && matchedRoute.components![currentName]if (!ViewComponent) {return normalizeSlot(slots.default, { Component: ViewComponent, route })}// ...// 关键:h函数,渲染路由中获得的组件const component = h(ViewComponent,assign({}, routeProps, attrs, {onVnodeUnmounted,ref: viewRef,}))return (// pass the vnode to the slot as a prop.// h and  both accept vnodesnormalizeSlot(slots.default, { Component: component, route }) ||component)}},
})

实现嵌套路由的核心是使用深度depth控制,初始router-view深度为0,内部嵌套深度依次加1,比如对如下嵌套关系:

const routes = [{path: '/',component: Home,children: [{path: 'product',component: ProductManage},]},{ path: '/login', name: 'login', component: Login }]

它们在resolve中被解析成的routeToDisplay.value依次为:

matched是个数组,在pushresolve时,把当前路径path拆分解析成对应routes数组中可以匹配的对象,然后初始值的router-view,就取深度为0的值,深度1的router-view就取到mactched[1]'/product'对应的route,分别渲染

跳转

分析跳转流程之前,先看下路由注册的解析逻辑,在createRouter方法中调用了createRouterMatcher方法,该方法创建了一个路由匹配器,内部封装了路由注册和跳转的具体实现,外部创建的router是对matcher的包了一层提供API,并屏蔽实现细节。看下实现:


/*** Creates a Router Matcher.** @internal* @param routes - array of initial routes* @param globalOptions - global route options*/
export function createRouterMatcher(routes: Readonly,globalOptions: PathParserOptions
): RouterMatcher {// normalized ordered array of matchers// 匹配器的两个容器,匹配器Array和命名路由Mapconst matchers: RouteRecordMatcher[] = []const matcherMap = new Map()function getRecordMatcher(name: RouteRecordName) {return matcherMap.get(name)}function addRoute(record: RouteRecordRaw,parent?: RouteRecordMatcher,originalRecord?: RouteRecordMatcher) {// ...// 如果记录中声明'alias'别名,把别名当作path,插入一条新的记录if ('alias' in record) {const aliases =typeof record.alias === 'string' ? [record.alias] : record.alias!for (const alias of aliases) {normalizedRecords.push(assign({}, mainNormalizedRecord, {// this allows us to hold a copy of the `components` option// so that async components cache is hold on the original recordcomponents: originalRecord? originalRecord.record.components: mainNormalizedRecord.components,path: alias,// we might be the child of an aliasaliasOf: originalRecord? originalRecord.record: mainNormalizedRecord,// the aliases are always of the same kind as the original since they// are defined on the same record}) as typeof mainNormalizedRecord)}}let matcher: RouteRecordMatcherlet originalMatcher: RouteRecordMatcher | undefinedfor (const normalizedRecord of normalizedRecords) {// ...// create the object beforehand, so it can be passed to children// 遍历记录,生成一个matchermatcher = createRouteRecordMatcher(normalizedRecord, parent, options)// ...// 添加到容器insertMatcher(matcher)}return originalMatcher? () => {// since other matchers are aliases, they should be removed by the original matcherremoveRoute(originalMatcher!)}: noop}function removeRoute(matcherRef: RouteRecordName | RouteRecordMatcher) {// 删除路由元素if (isRouteName(matcherRef)) {const matcher = matcherMap.get(matcherRef)if (matcher) {matcherMap.delete(matcherRef)matchers.splice(matchers.indexOf(matcher), 1)matcher.children.forEach(removeRoute)matcher.alias.forEach(removeRoute)}} else {const index = matchers.indexOf(matcherRef)if (index > -1) {matchers.splice(index, 1)if (matcherRef.record.name) matcherMap.delete(matcherRef.record.name)matcherRef.children.forEach(removeRoute)matcherRef.alias.forEach(removeRoute)}}}function getRoutes() {return matchers}function insertMatcher(matcher: RouteRecordMatcher) {let i = 0while (i < matchers.length &&comparePathParserScore(matcher, matchers[i]) >= 0 &&// Adding children with empty path should still appear before the parent// https://github.com/vuejs/router/issues/1124(matcher.record.path !== matchers[i].record.path ||!isRecordChildOf(matcher, matchers[i])))i++// 将matcher添加到数组末尾matchers.splice(i, 0, matcher)// only add the original record to the name map// 命名路由添加到路由Mapif (matcher.record.name && !isAliasRecord(matcher))matcherMap.set(matcher.record.name, matcher)}function resolve(location: Readonly,currentLocation: Readonly): MatcherLocation {let matcher: RouteRecordMatcher | undefinedlet params: PathParams = {}let path: MatcherLocation['path']let name: MatcherLocation['name']if ('name' in location && location.name) {// 命名路由解析出pathmatcher = matcherMap.get(location.name)// ...// throws if cannot be stringifiedpath = matcher.stringify(params)} else if ('path' in location) {// no need to resolve the path with the matcher as it was provided// this also allows the user to control the encodingpath = location.path//...matcher = matchers.find(m => m.re.test(path))// matcher should have a value after the loopif (matcher) {// we know the matcher works because we tested the regexpparams = matcher.parse(path)!name = matcher.record.name}// push相对路径} else {// match by name or path of current routematcher = currentLocation.name? matcherMap.get(currentLocation.name): matchers.find(m => m.re.test(currentLocation.path))if (!matcher)throw createRouterError(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 providedparams = assign({}, currentLocation.params, location.params)path = matcher.stringify(params)}const matched: MatcherLocation['matched'] = []let parentMatcher: RouteRecordMatcher | undefined = matcherwhile (parentMatcher) {// reversed order so parents are at the beginning// 和当前path匹配的记录,插入到数组头部,让父级先匹配matched.unshift(parentMatcher.record)parentMatcher = parentMatcher.parent}return {name,path,params,matched,meta: mergeMetaFields(matched),}}// 添加初始路由routes.forEach(route => addRoute(route))return { addRoute, resolve, removeRoute, getRoutes, getRecordMatcher }
}

总结一下,createRouterMatcher方法,为每一个routres执行了addRoute方法,调用了insertMatcher,将生成的matchers插入到容器中,后边在调用的时候,通过resolve方法,将记录匹配到到Matcher.record记录保存到MatcherLocationmatched数组中,后续router-view会根据depth从数组取应该要渲染的元素。 push方法执行流程:

function push(to: RouteLocationRaw) {return pushWithRedirect(to)}// ...function pushWithRedirect(to: RouteLocationRaw | RouteLocation,redirectedFrom?: RouteLocation): Promise {// 解析出目标locationconst 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 shouldRedirect = handleRedirectRecord(targetLocation)// 重定向逻辑if (shouldRedirect)return pushWithRedirect(assign(locationAsObject(shouldRedirect), {state:typeof shouldRedirect === 'object'? assign({}, data, shouldRedirect.state): data,force,replace,}),// keep original redirectedFrom if it existsredirectedFrom || targetLocation)// if it was a redirect we already called `pushWithRedirect` aboveconst toLocation = targetLocation as RouteLocationNormalized// ...return (failure ? Promise.resolve(failure) : navigate(toLocation, from)).catch((error: NavigationFailure | NavigationRedirectError) =>// ...).then((failure: NavigationFailure | NavigationRedirectError | void) => {if (failure) {// ...} else {// if we fail we don't finalize the navigationfailure = finalizeNavigation(toLocation as RouteLocationNormalizedLoaded,from,true,replace,data)}triggerAfterEach(toLocation as RouteLocationNormalizedLoaded,from,failure)return failure})}

在没有失败情况下调用finalizeNavigation做最终跳转,看下实现:

/*** - Cleans up any navigation guards* - Changes the url if necessary* - Calls the scrollBehavior*/function finalizeNavigation(toLocation: RouteLocationNormalizedLoaded,from: RouteLocationNormalizedLoaded,isPush: boolean,replace?: boolean,data?: HistoryState): NavigationFailure | void {// a more recent navigation took placeconst error = checkCanceledNavigation(toLocation, from)if (error) return error// only consider as push if it's not the first navigationconst isFirstNavigation = from === START_LOCATION_NORMALIZEDconst state = !isBrowser ? {} : history.state// change URL only if the user did a push/replace and if it's not the initial navigation because// it's just reflecting the url// 如果是push保存历史到routerHistoryif (isPush) {// on the initial navigation, we want to reuse the scroll position from// history state if it existsif (replace || isFirstNavigation)routerHistory.replace(toLocation.fullPath,assign({scroll: isFirstNavigation && state && state.scroll,},data))else routerHistory.push(toLocation.fullPath, data)}// accept current navigation// 给当前路由赋值,会触发监听的router-view刷新currentRoute.value = toLocationhandleScroll(toLocation, from, isPush, isFirstNavigation)markAsReady()}

currentRoute.value = toLocation执行完后,会触发router-viewrouteToDisplay值变化,重新计算matchedRouteRef获得新的ViewComponent,完成页面刷新。 上面还有两点,routerresolve会调用到matcherresolve,填充刚刚说过的matched数组,navigate方法会执行导航上的守卫,这两步就不看了,感兴趣同学可以自己查阅《住院证明图片》,至此主要的流程已经分析完了。

相关内容

热门资讯

孟加拉国空军一架教练机在学校坠... 总台记者获悉,据孟加拉国消防部门消息,当地时间21日13时30分左右,孟加拉国空军一架教练机在一所学...
新华视点|多地经开区培育新质生...   近年来,多地经开区立足产业优势,以科技创新为引擎,培育新质生产力。各地通过技术攻关、政策赋能、模...
记者手记丨外国记者在江西感知中...   新华社江西赣州7月21日电记者手记|外国记者在江西感知中国高质量发展  新华社记者黎藜  “人类...
尺素金声丨三个“首次突破”,凸...   出口规模历史同期首次突破13万亿元,同比增长实现7.2%;有进出口实绩的外贸企业历史同期首次突破...
多个全球第一!这份“成绩单”与...   国新办今天举行“高质量完成‘十四五’规划”系列主题新闻发布会,多个部门相关负责人介绍了“十四五”...
欢乐过暑假,安全健康不放假 |...   暑假期间,孩子们宅家吃喝、外出玩乐时,如何做好安全防护,避免意外伤害?关于暑假期间安全与健康的几...
事关“地下生命”,有新发现!   研究显示:地震可为“地下生命”提供“燃料”  一项最新研究显示,地震等地壳内部构造活动瞬间释放的...
俄媒首次公开攻击型无人机生产基...   当地时间7月20日,俄罗斯红星电视台首次曝光位于俄联邦鞑靼斯坦共和国境内的一个无人机生产基地。这...