vue-router 使用与原理分析

news2024/9/21 2:40:25

简介

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

使用

创建

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

<div id="app"><router-view></router-view>
</div> 

2、创建路由router/index.js

 const routes = [	{ path: '/', component: Home},{ path: '/login', name: 'login', component: Login},
]
constrouter = 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: `<div class="user"><h2>User {{ $route.params.id }}</h2><router-view></router-view></div>`,
} 

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

const routes = [{path: '/user/:id',component: User,children: [{// 当 /user/:id/profile 匹配成功// UserProfile 将被渲染到 User 的 <router-view> 内部path: 'profile',component: UserProfile,},{// 当 /user/:id/posts 匹配成功// UserPosts 将被渲染到 User 的 <router-view> 内部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 &&currentRoute.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<RouteLocationNormalizedLoaded>(() => props.route || injectedRoute.value)// 获取当前router-view深度层级,在嵌套路由时使用const injectedDepth = inject(viewDepthKey, 0)// 在当前router-view深度下去匹配要显示的路由matched// matched 是个数组,在resolve方法被赋值,如果有匹配到则在当前router-view渲染const depth = computed<number>(() => {let initialDepth = unref(injectedDepth)const { matched } = routeToDisplay.valuelet matchedRoute: RouteLocationMatched | undefinedwhile ((matchedRoute = matched[initialDepth]) &&!matchedRoute.components) {initialDepth++}return initialDepth})const matchedRouteRef = computed<RouteLocationMatched | undefined>(() => routeToDisplay.value.matched[depth.value])provide(viewDepthKey,computed(() => depth.value + 1))provide(matchedRouteKey, matchedRouteRef)provide(routerViewLocationKey, routeToDisplay)const viewRef = ref<ComponentPublicInstance>()// 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 <component :is="..."> 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依次为:

跳转

分析跳转流程之前,先看下路由注册的解析逻辑,在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<RouteRecordRaw[]>,globalOptions: PathParserOptions ): RouterMatcher {// normalized ordered array of matchers// 匹配器的两个容器,匹配器Array和命名路由Mapconst matchers: RouteRecordMatcher[] = []const matcherMap = new Map<RouteRecordName, RouteRecordMatcher>()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<img src="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<MatcherLocationRaw>,currentLocation: Readonly<MatcherLocation> ): 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<MatcherError>(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 " style="margin: auto" />
} 

总结一下,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<NavigationFailure | void | undefined> {// 解析出目标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方法会执行导航上的守卫,这两步就不看了,感兴趣同学可以自己查阅,至此主要的流程已经分析完了。

参考

tchedRouteRef获得新的ViewComponent,完成页面刷新。 上面还有两点,routerresolve会调用到matcherresolve,填充刚刚说过的matched数组,navigate`方法会执行导航上的守卫,这两步就不看了,感兴趣同学可以自己查阅,至此主要的流程已经分析完了。

最后

最近找到一个VUE的文档,它将VUE的各个知识点进行了总结,整理成了《Vue 开发必须知道的36个技巧》。内容比较详实,对各个知识点的讲解也十分到位。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/172365.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ARM X210开发板的软开关按键问题

一、X210 开发板的软启动电路详解 《x210bv3.pdf》 (1) 210 供电需要的电压比较稳定&#xff0c;而外部适配器的输出电压不一定那么稳定&#xff0c;因此板载了一个文稳压器件 MP1482. 这个稳压芯片的作用就是外部适配器电压在一定范围内变化时稳压芯片的输出电压都是 5V。 (2)…

Redis 主从复制

目录一、简介二、复制功能三、将服务器设置为从服务器3.1、手动设置3.2、REPLICAOF 配置选项3.3、取消复制四、查看服务器的角色4.1、查看主服务器4.2、查看从服务器五、其他配置5.1、无硬盘复制5.2、降低数据不一致情况出现的概率5.3、可写的从服务器5.4、选择性命令传播六、w…

SpringBoot统一功能处理

目录 一、统一用户的登录验证 1.1 Spring AOP 实现用户统一登录验证 1.2Spring拦截器实现统一用户的登录验证 1.3 实例演示&#xff08;通过url访问controller层的方法&#xff09; 二、统一异常处理 三、统一数据格式返回 3.1 统一数据返回格式的优点 3.2 统一数据返回…

旺季到来,跨境电商卖家年末冲刺!

又是一年年末时&#xff0c;随着新年的到来&#xff0c;在年底这段时间里&#xff0c;对于跨境电商卖家来说&#xff0c;又是一个关键节点。而现在&#xff0c;卖家们也将迎来一年一度的旺季收官&#xff0c;在此过程中卖家需要做好哪些准备做好年末冲刺呢&#xff1f; 在许多…

量化策略——准备3 数据、Backtrader回测框架与quantstats评价指标

我们一般使用AKShare这个库来获取股票数据或策略中用得到的数据&#xff1a; AKShare github主页&#xff1a;https://github.com/akfamily/akshare 使用Backtrader框架作为回测的框架&#xff1a; Backtrader github主页&#xff1a;https://github.com/mementum/backtrader …

【CTF】git源码泄露和代码审计

目录 源码获取 函数绕过 解法一&#xff1a; 解法二&#xff1a; 参考文章&#xff1a; 源码获取 这里做的web题目是buuctf中的&#xff1a;[GXYCTF2019]禁止套娃 打开页面&#xff0c;查看源码是没有可以利用的点的。开始进行目录扫描 这里使用的工具是dirsearch。直接…

前端js实现多次添加累加文件上传和选择删除(django+js)- 编辑回显文件并上传 (二)

前言 后端返回的是文件地址&#xff0c;并不是文件流或base64编码字符串&#xff0c;而修改数据的接口又只接受文件。 本篇文章主要是基于累加文件上传介绍的。 添加上传文件文章链接&#xff1a;https://blog.csdn.net/qq_43030934/article/details/128726549?spm1001.2014.…

jmeter 压测java代码

一、背景 直接压测、调用java工程中的方法。&#xff08;没有http等的入口&#xff09; 二、java项目改造 一个java项目&#xff0c;想要压测其中的几个方法。我们需要在该工程中&#xff0c;添加一个压测入口的类&#xff0c; 这个类必须继承或者实现jmeter提供的接口/类。…

C语言萌新如何使用scanf函数?

&#x1f40e;作者的话 如果你搜索输入输出函数&#xff0c;那么你会看到输入输出流、Turbo标准库、标准输出端、stdout什么什么乱七八糟的&#xff0c;作为一个萌新&#xff0c;哪懂这些&#xff1f; 本文介绍萌新在前期的学习中&#xff0c;常用的输入输出函数及其功能~ 跳…

HTML5+CSS3小实例:炫彩的发光字特效

前言&#xff1a; 今天我们向大家精选了一款HTML5CSS3文字特效&#xff0c;文字特效有超酷的动画类型&#xff0c;不多说&#xff0c;一起来看看。 描述&#xff1a; 这款文字特效既有倒影的效果&#xff0c;又有随机的颜色&#xff0c;看起来非常的炫酷。全文基于 HTML5CSS3 完…

log4j.properties自定义日志配置

一、通用的写法log4j.properties# 设置root logger等级为ALL&#xff0c;且appender有A1和FILE log4j.rootLoggerALL, A1,A3#设置com.example.test logger log4j.logger.com.example.testDEBUG,A1,A3 # 取消继承父类 log4j.additivity.com.example.testfalse# 设置个控制台输出…

即时通讯开发之TCP 连接的建立与中止

TCP 是一个面向连接的协议,所以在连接双方发送数据之前,都需要首先建立一条连接。这和前面讲到的协议完全不同。前面讲的所有协议都只是发送数据 而已,大多数都不关心发送的数据是不是送到,UDP 尤其明显,从编程的角度来说,UDP 编程也要简单 的多----UDP 都不用考虑数据分片。 书…

Ubuntu下源码编译VirtualBox一 —— 源码下载

VirtualBox想必大家都不陌生&#xff0c;做Linux开发的尤其是嵌入式Linux开发的人应该基本都知道或玩过VMware和VirtualBox。但通常都是为了在Windows电脑上能够使用Linux环境、即在Windows环境下通过下载可执行文件安装的VirtualBox。本文介绍在Linux环境&#xff08;Ubuntu 2…

系分 - 论文 - 总览知识点

个人总结&#xff0c;仅供参考&#xff0c;欢迎加好友一起讨论 文章目录系分 - 论文 - 总览往年论文一览论文考点考试时间考试过程论文技巧论文写作论文扣分与加分系分 - 论文 - 总览 往年论文一览 一般情况下&#xff0c;往下数5、6年的题目出题形式&#xff0c;具有参考意义…

痞子衡嵌入式:盘点国内Cortex-M内核MCU厂商高主频产品(2023)

大家好&#xff0c;我是痞子衡&#xff0c;是正经搞技术的痞子。今天痞子衡给大家介绍的是国内Cortex-M内核MCU厂商高主频产品。 在 2021 年初痞子衡写了篇 《盘点国内Cortex-M内核MCU厂商高性能产品》&#xff0c;搜罗了当时市面上主频不低于 96MHz 的 CM 核国产 MCU。如今过去…

LSTM MultiheadAttention 输入维度

最近遇到点问题&#xff0c;对于模块的输入矩阵的维度搞不清楚&#xff0c;这里在学习一下&#xff0c;记录下来&#xff0c;方便以后查阅。 LSTM & Attention 输入维度LSTM记忆单元门控机制LSTM结构LSTM的计算过程遗忘门输入门更新记忆单元输出门LSTM单元的pytorch实现Pyt…

Spring Security in Action 第七章 配置授权:限制访问

本专栏将从基础开始&#xff0c;循序渐进&#xff0c;以实战为线索&#xff0c;逐步深入SpringSecurity相关知识相关知识&#xff0c;打造完整的SpringSecurity学习步骤&#xff0c;提升工程化编码能力和思维能力&#xff0c;写出高质量代码。希望大家都能够从中有所收获&#…

[leetcode 72] 编辑距离

题目 题目&#xff1a;https://leetcode.cn/problems/edit-distance/description/ 类似题目&#xff1a;[leetcode 583] 两个字符串的删除操作 解法 动态规划 这题应该是字符串dp的终极形态了吧&#x1f923;&#xff0c;不看答案完全不会…看了答案发现原来还是dp… 以例题…

未来的竞争是认知和执行力的竞争,只有认知高,强执行才能赚钱

之前很火的一句话是&#xff1a;你永远赚不到认知范围之外的钱所以只有持续不断地提升认知才能持续成长&#xff0c;持续提升&#xff0c;持续赚钱。未来的竞争从另一方面来说也是认知的竞争。不同的认知对待同一事物、信息有不同的理解&#xff1b;不同的认知对待同一事物、信…

固高科技在创业板提交注册:业绩开始下滑,实控人均为“学院派”

近日&#xff0c;固高科技股份有限公司&#xff08;下称“固高科技”&#xff09;在深圳证券交易所创业板递交注册。据贝多财经了解&#xff0c;固高科技于2021年12月在创业板递交上市申请&#xff0c;2022年8月17日获得上市委会议通过。 本次冲刺创业板上市&#xff0c;固高科…