Vue-router usage and principle analysis, the test results are here

Introduction

Vue Router is the official router for Vue.js. Deep integration with Vue.js core makes it easier to build single-page applications (SPA) with Vue.js.

 

For front-end projects that develop and maintain management background classes, the page structure and composition may be very complicated, so it is particularly important to understand and use Vue Router correctly.

use

create

1. After installing the Vue Router dependency, App.vueimport it in router-view, it is the container for rendering

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

2. Create a routerouter/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.jsUse routing in

import router from "./router";
const app = createApp(App)

app.use(router)

app.mount('#app')

Then you can access it in any component this.$router, this.$routeand access the current route as:

// 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')
      }
    },
  },
}

nested routes

Some application UIs consist of multiple layers of nested components. In this case, the fragments of the URL usually correspond to specific nested component structures, for example:

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

Under the top layer of the upper app node router-view, the components contained are nested by themselves router-view, such as the above usertemplate:

const User = {
  template: `
    <div class="user">
      <h2>User {
   
   { $route.params.id }}</h2>
      <router-view></router-view>
    </div>
  `,
}

To render components into this nest router-view, we need to configure in the route 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,
      },
    ],
  },
]

Let's look at how the page is loaded and displayed on the page from the source code point of view

principle

As can be seen from the above basic usage method, it mainly includes three steps:

  1. Create and use this route createRouterin the appuse
  2. router-viewUsing tags in templates
  3. navigation push, jump page

As can be seen from the array structure declared by routers, the declared route pathwill be registered as a routing table pointing to the componentdeclared component, and when pushthe method is called, the corresponding component will be found from the routing table and loaded. Let's take a look at how the source code implements this process. The Vue Router source code analysis version is 4.1.5

create install

First look at createRouterthe method implementation:

/**
 * 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全局安装router
    install(app: App) {
      const router = this
      // 全局注册组件RouterLink、RouterView
      app.component('RouterLink', RouterLink)
      app.component('RouterView', RouterView)
    	// 全局声明router实例,this.$router访问
      app.config.globalProperties.$router = router
      // 全局注册this.$route 访问当前路由currentRoute
      Object.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 problems
      if (
        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 = true
        push(routerHistory.location).catch(err => {
          if (__DEV__) warn('Unexpected error when starting the router:', err)
        })
      }
    	// ...
      app.provide(routerKey, router)
      app.provide(routeLocationKey, reactive(reactiveRoute))
      // 全局注入当前路由currentRoute
      app.provide(routerViewLocationKey, currentRoute)
    	// ...
    },
  }

  return router
}

createRouterthis.$routerThe method returns the current routing instance, and internally initializes some common routing methods, which are the same as printing the structure in the component . installWhere is the method called? Called during installation app.use(router), look at usethe method, runtime-core.cjs.prod.jsbelow :

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

So far, the global router creation and installation has been completed, and it can be used in the code router-view, this.$routerand some methods of the example, so how to display the loaded on the page component? Need to look at router-viewthe internal implementation of the rendering component

rendering

installThe method registers RouterViewthe component and is implemented in RouterView.ts:

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

RouterViewImplaccomplish:


export const RouterViewImpl = /*#__PURE__*/ defineComponent({
  name: 'RouterView',
  
	// ...
  
  setup(props, { attrs, slots }) {
    __DEV__ && warnDeprecatedUsage()
  	// 拿到之前注册的currentRoute
    const 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.value
      let matchedRoute: RouteLocationMatched | undefined
      while (
        (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 different
      const currentName = props.name
      const matchedRoute = matchedRouteRef.value
      const 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 vnodes
        normalizeSlot(slots.default, { Component: component, route }) ||
        component
      )
    }
  },
})

The core of implementing nested routing is to use depth depthcontrol. The initial router-viewdepth is 0, and the internal nesting depth is incremented by 1. For example, for the following nesting relationships:

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

They resolveare parsed as follows in routeToDisplay.valueorder:

matchedIt is an array. When it pushis resolve, the current path pathis split and parsed into routesobjects that can be matched in the corresponding array. Then, for the initial value, router-viewthe value with a depth of 0 is taken, and for the one with a depth of 1, the corresponding route router-viewis taken and rendered separately.mactched[1]'/product'

jump

Before analyzing the jump process, first look at the parsing logic of the route registration. The method createRouteris called in the method createRouterMatcher. This method creates a route matcher, internally encapsulates the specific implementation of route registration and jump, and creates the routerright matcherpackage externally. A layer provides the API and shields the implementation details. Look at the implementation:


/**
 * 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和命名路由Map
  const 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!
      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 < 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
    // 命名路由添加到路由Map
    if (matcher.record.name && !isAliasRecord(matcher))
      matcherMap.set(matcher.record.name, matcher)
  }

  function resolve(
    location: Readonly<MatcherLocationRaw>,
    currentLocation: Readonly<MatcherLocation>
  ): 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<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 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 }
}

To sum up, the method executes the method createRouterMatcherfor each method, calls it , and inserts the generated one into the container. When it is called later, through the method, the record is matched to the array where the record is saved , and the follow-up will be based on the fetch from the array. The element that should be rendered. Method execution flow:routresaddRouteinsertMatchermatchersresolveMatcher.recordMatcherLocationmatchedrouter-viewdepthpush

function push(to: RouteLocationRaw) {
    return pushWithRedirect(to)
  }

// ...


  function pushWithRedirect(
    to: RouteLocationRaw | RouteLocation,
    redirectedFrom?: RouteLocation
  ): Promise<NavigationFailure | void | undefined> {
    // 解析出目标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
      })
  }

Call to do the final jump without failure finalizeNavigation, see the implementation:

/**
   * - 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 = toLocationAfter execution, it will trigger the router-viewmedian routeToDisplaychange, recalculate matchedRouteRefto obtain a new value ViewComponent, and complete the page refresh. There are two more points above, routerwhich will be resolvecalled , fill the array just mentioned , and the method will execute the guard on the navigation. I will not read these two steps. Interested students can check the " Hospitalization Certificate Picture " by themselves. So far the main The process has been analyzed.matcherresolvematchednavigate

Guess you like

Origin blog.csdn.net/dageliuqing/article/details/127913588