Skip to content
导航

app.use(router)

文件位置:src/router.ts

vue-router 4.x中,使用createRouter创建一个路由实例,并调用app.use(router)使整个应用支持路由。

ts
import { createRouter, createWebHashHistory } from 'vue-router'
import { createApp } from 'vue'

const Home = { template: '<div>Home</div>' }
const About = { template: '<div>About</div>' }

const routes = [
  { path: '/', component: Home },
  { path: '/about', component: About },
]

const router = createRouter({
  history: createWebHashHistory(),
  routes,
})

const app = createApp({})
app.use(router)
app.mount('#app')

当调用app.use时,会调用router实例中的install的方法,并将app作为参数传入。让我们看看router.install做了些什么?

ts
export function createRouter(options: RouterOptions): Router {
  // ...
  const router: Router = {
    // ...
    install(app: App) {
      const router = this
      // 注册RouterLink与RouterView组件
      app.component('RouterLink', RouterLink)
      app.component('RouterView', RouterView)

      // 注册全局属性,$router为当前router实例
      app.config.globalProperties.$router = router
      // 注册$route属性,当前路由信息
      Object.defineProperty(app.config.globalProperties, '$route', {
        enumerable: true,
        get: () => unref(currentRoute),
      })

      // 只在浏览器环境下初始化导航,在服务器上会创建额外的不必要的导航并可能导致问题
      if (
        isBrowser &&
        // 用于初始化客户端,避免路由被应用多个应用
        !started &&
        currentRoute.value === START_LOCATION_NORMALIZED
      ) {
        // 初始化后,started设置为true
        started = true
        // 第一次跳转 跳转到浏览器url中对应的路由
        push(routerHistory.location).catch(err => {
          if (__DEV__) warn('Unexpected error when starting the router:', err)
        })
      }

      // currentRoute转为响应式对象,以方便或许对其进行变化追踪
      const reactiveRoute = {} as {
        [k in keyof RouteLocationNormalizedLoaded]: ComputedRef<
          RouteLocationNormalizedLoaded[k]
          >
      }
      for (const key in START_LOCATION_NORMALIZED) {
        // @ts-expect-error: the key matches
        reactiveRoute[key] = computed(() => currentRoute.value[key])
      }

      // 向vue实例中注入相关provider,在组件中可使用inject进行接收:useRoute与useRouter就是使用inject实现的
      // 注意这里的key都是Symbol类型
      app.provide(routerKey, router)
      app.provide(routeLocationKey, reactive(reactiveRoute))
      app.provide(routerViewLocationKey, currentRoute)

      // 重写vue实例的卸载方法,以便重置一些属性并解除一些监听
      const unmountApp = app.unmount
      // 记录vue实例,installedApps是一个Set集合
      installedApps.add(app)
      app.unmount = function () {
        // 当app被卸载时,从集合中删除
        installedApps.delete(app)
        // 如果没有任何vue实例了,代表router不会被使用了,那么重置一些属性和解绑监听
        if (installedApps.size < 1) {
          // invalidate the current navigation
          pendingLocation = START_LOCATION_NORMALIZED
          removeHistoryListener && removeHistoryListener()
          removeHistoryListener = null
          currentRoute.value = START_LOCATION_NORMALIZED
          started = false
          ready = false
        }
        // 最后执行卸载方法
        unmountApp()
      }

      if ((__DEV__ || __FEATURE_PROD_DEVTOOLS__) && isBrowser) {
        addDevtools(app, router, matcher)
      }
    },
  }
  
  return router
}

可以看到在install方法中,首先会注册RouterLinkRouterView两个组件,然后设置了两个全局的属性$router(路由实例)、$route(当前的路由信息),紧接着进行第一次跳转。紧接着又向app中注入了三个属性:routerKeyrouteLocationKeyrouterViewLocationKey,分别表示路由实例、深度响应式的当前路由信息对象、浅层响应式的当前路由信息对象,注意这里的key值实际是Symbol类型,这里列举的key只是变量的名称。然后对app.unmountvue实例的卸载方法进行了拦截,拦截的主要目的是在路由实例不被使用时,将一些属性重置并解绑一些监听事件。

总的来说,install函数做了以下几件事:

  1. 注册RouterLinkRouterView组件
  2. 设置全局属性$router$route
  3. 根据地址栏进行首次的路由跳转
  4. app中注入一些路由相关信息,如路由实例、响应式的当前路由信息对象
  5. 拦截app.unmount方法,在卸载之前重置一些属性、删除一些监听函数
app.use(router) has loaded