简介
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.vueexport 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, }, ], },]
下面我们从源码的角度看下页面是如何加载并显示到页面上的
原理
上面基础的使用方法可以看出,主要包含三个步骤:
- 创建
createRouter
,并在app中use
使用这个路由 - 在模版中使用
router-view
标签 - 导航
push
,跳转页面
从routers声明的数组结构可以看出,声明的路由path
会被注册成路由表指向component
声明的组件,并在push
方法调用时,从路由表查出对应组件并加载。下面看下源码是如何实现这一过程的,Vue Router源码分析版本为4.1.5
matched
是个数组,在push
的resolve
时,把当前路径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和命名路由Map const 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 record components: originalRecord ? originalRecord.record.components : mainNormalizedRecord.components, path: alias, // we might be the child of an alias aliasOf: 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: RouteRecordMatcher let originalMatcher: RouteRecordMatcher | undefined for (const normalizedRecord of normalizedRecords) { // ... // create the object beforehand, so it can be passed to children // 遍历记录,生成一个matcher matcher = createRouteRecordMatcher(normalizedRecord, parent, options) // ... // 添加到容器 insertMatcher(matcher) } return originalMatcher ? () => { // since other matchers are aliases, they should be removed by the original matcher removeRoute(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 = 0 while ( 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 // 命名路由添加到路由Map if (matcher.record.name && !isAliasRecord(matcher)) matcherMap.set(matcher.record.name, matcher) } function resolve( location: Readonly, currentLocation: Readonly ): MatcherLocation { let matcher: RouteRecordMatcher | undefined let params: PathParams = {} let path: MatcherLocation['path'] let name: MatcherLocation['name'] if ('name' in location && location.name) { // 命名路由解析出path matcher = matcherMap.get(location.name) // ... // throws if cannot be stringified path = 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 encoding path = location.path //... matcher = matchers.find(m => m.re.test(path)) // matcher should have a value after the loop if (matcher) { // we know the matcher works because we tested the regexp params = matcher.parse(path)! name = matcher.record.name } // push相对路径 } else { // match by name or path of current route matcher = 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 provided params = assign({}, currentLocation.params, location.params) path = matcher.stringify(params) } const matched: MatcherLocation['matched'] = [] let parentMatcher: RouteRecordMatcher | undefined = matcher while (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
记录保存到MatcherLocation
的matched
数组中,后续router-view
会根据depth
从数组取应该要渲染的元素。 push
方法执行流程:
function push(to: RouteLocationRaw) { return pushWithRedirect(to) }// ... function pushWithRedirect( to: RouteLocationRaw | RouteLocation, redirectedFrom?: RouteLocation ): Promise { // 解析出目标location const targetLocation: RouteLocation = (pendingLocation = resolve(to)) const from = currentRoute.value 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 const 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 exists redirectedFrom || targetLocation ) // if it was a redirect we already called `pushWithRedirect` above const 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 navigation failure = 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 place const error = checkCanceledNavigation(toLocation, from) if (error) return error // only consider as push if it's not the first navigation const isFirstNavigation = from === START_LOCATION_NORMALIZED const 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保存历史到routerHistory if (isPush) { // on the initial navigation, we want to reuse the scroll position from // history state if it exists if (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 = toLocation handleScroll(toLocation, from, isPush, isFirstNavigation) markAsReady() }
currentRoute.value = toLocation
执行完后,会触发router-view
中routeToDisplay
值变化,重新计算matchedRouteRef
获得新的ViewComponent
,完成页面刷新。 上面还有两点,router
的resolve
会调用到matcher
的resolve
,填充刚刚说过的matched
数组,navigate
方法会执行导航上的守卫,这两步就不看了,感兴趣同学可以自己查阅《住院证明图片》,至此主要的流程已经分析完了。