简介

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,      },    ],  },]

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

原理

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

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

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

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和命名路由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记录保存到MatcherLocationmatched数组中,后续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-viewrouteToDisplay值变化,重新计算matchedRouteRef获得新的ViewComponent,完成页面刷新。 上面还有两点,routerresolve会调用到matcherresolve,填充刚刚说过的matched数组,navigate方法会执行导航上的守卫,这两步就不看了,感兴趣同学可以自己查阅《住院证明图片》,至此主要的流程已经分析完了。