vue-router4源码解读

·  阅读 103
vue-router4源码解读

vue-router4是vue官方针对Vue3版本提供的路由系统,具体使用参考官方文档,本篇文章主要是从背后的源码来看vue-router的具体实现。

先通过下面的代码,看下我们日常开发中使用vue-router的方式。

// main.js
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';

const app = createApp(App);
app.use(router).mount('#app');
复制代码
// router.js
import { createRouter, createWebHistory } from 'vue-router';
const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import ('../Home.vue')
  },
  {
    path: '/about',
    name: 'About',
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
];
const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
});

export default router;
复制代码

我们看到,vue-router的主要通过createRouter方法创建路由对象实例并通过应用实例app.use()方法注册路由系统。在之前的一篇文章# Vue3源码之初始化渲染流程解读一中,我们有学习到app.use()方法是一个vue3的插件机制,通过该方法我们可以向应用添加第三方或者开发者自己的插件。

1. createRouter方法

在上面的应用代码示例中我们看到createRouter方法接受一个对象作为参数:

  • history: 路由模式
  • routes: 路由配置
// options类型定义
export interface RouterOptions extends PathParserOptions {
    // 路由模式
    history: RouterHistory
    // 应该添加到路由的初始路由列表
    routes: RouteRecordRaw[]
    // 在页面之间导航时控制滚动的函数
    scrollBehavior?: RouterScrollBehavior
    // 用于解析查询的自定义实现
    parseQuery?: typeof originalParseQuery
    // 对查询对象进行字符串化的自定义实现
    stringifyQuery?: typeof originalStringifyQuery
    // 于激活的 RouterLink 的默认类。如果什么都没提供,则会使用 router-link-active
    linkActiveClass?: string
    // 于精准激活的 RouterLink 的默认类。如果什么都没提供,则会使用 router-link-exact-active
    linkExactActiveClass?: string
}

// 返回Router实例类型定义

export interface Router {
    // 向路由配置添加路由方法
    addRoute(parentName: RouteRecordName, route: RouteRecordRaw): () => void
    addRoute(route: RouteRecordRaw): () => void
    // 从路由配置列表删除某一个路由
    removeRoute(name: RouteRecordName): void
    // 检测判断方法是否存在路由
    hasRoute(name: RouteRecordName): boolean
    // 返回路由配置
    getRoutes(): RouteRecord[]
    // 要解析的原始路由地址,返回路由地址的标准化版本
    resolve(
        to: RouteLocationRaw,
        currentLocation?: RouteLocationNormalizedLoaded
    ): RouteLocation & { href: string }
    // 跳转
    push(to: RouteLocationRaw): Promise<NavigationFailure | void | undefined>
    // 替换
    replace(to: RouteLocationRaw): Promise<NavigationFailure | void | undefined>
    // 后退
    back(): ReturnType<Router['go']>
    // 前进
    forward(): ReturnType<Router['go']>
    // 基于路由栈记录跳转
    go(delta: number): void
    // 导航守卫
    beforeEach(guard: NavigationGuardWithThis<undefined>): () => void
    // 导航守卫
    beforeResolve(guard: NavigationGuardWithThis<undefined>): () => void
    // 导航守卫
    afterEach(guard: NavigationHookAfter): () => void
    // 错误捕获
    onError(handler: _ErrorHandler): () => void
    // app.use方法调用
    install(app: App): void
}
export function createRouter(options: RouterOptions): Router {
    // 用户路由配置options树扁平化,便于操作
    const matcher = createRouterMatcher(options.routes, options)
    // 获取用户自定义的解析查询字符串或者使用系统默认
    let parseQuery = options.parseQuery || originalParseQuery
    // 获取用户自定义对对象字符串化的自定义实现或者默认
    let stringifyQuery = options.stringifyQuery || originalStringifyQuery
    // 获取路由模式
    let routerHistory = options.history
    // 当前活动的route,响应式包裹,变化更新视图
    const currentRoute = shallowRef<RouteLocationNormalizedLoaded>(
       START_LOCATION_NORMALIZED
    )
    // ....
    // 省略路由实例方法的实现
    const router: Router = {
        // ...
        install(app:App) {
           // this 指向app
           const router = this
           // 注册全局组件
          app.component('RouterLink', RouterLink)
          app.component('RouterView', RouterView)
          // vue 添加app应用路由实例,this.$router访问
          app.config.globalProperties.$router = router
          // app.$route可遍历,不可修改操作
          Object.defineProperty(app.config.globalProperties, '$route', {
            enumerable: true,
            get: () => unref(currentRoute),
          })
          if (
            isBrowser && started &&
            currentRoute.value === START_LOCATION_NORMALIZED
          ) {
            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))
          app.provide(routerViewLocationKey, currentRoute)
          // 应用卸载处理
          // 获取卸载方法
          let unmountApp = app.unmount
          installedApps.add(app)
          // 重写卸载方法
          app.unmount = function () {
            installedApps.delete(app)
            if (installedApps.size < 1) {
              removeHistoryListener()
              currentRoute.value = START_LOCATION_NORMALIZED
              started = false
              ready = false
            }
            unmountApp()
          }
        }
    }
    return router
}
复制代码

2. createWebHistory

createWebHistory方法创建基于HTML5新增的History apipushStatereplaceState的实现,关于History Api的简单使用可以参考之前的一篇文章# History Api一文详解

export function createWebHistory(base?: string): RouterHistory {
  // 主要提供hash的复用
  base = normalizeBase(base)
  // 提供路由当前信息:路径、状态、切换方法push replace
  const historyNavigation = useHistoryStateNavigation(base)
  // 地址栏前进、后退更新维护state
  const historyListeners = useHistoryListeners(
    base,
    historyNavigation.state,
    historyNavigation.location,
    historyNavigation.replace
  )
  function go(delta: number, triggerListeners = true) {
    if (!triggerListeners) historyListeners.pauseListeners()
    history.go(delta)
  }

  const routerHistory: RouterHistory = assign(
    {
      // it's overridden right after
      location: '',
      base,
      go,
      createHref: createHref.bind(null, base),
    },

    historyNavigation,
    historyListeners
  )
  // 代理模式 routerHistory.location代表当前路径
  Object.defineProperty(routerHistory, 'location', {
    enumerable: true,
    get: () => historyNavigation.location.value,
  })
  // routerHistory.state代表当前状态
  Object.defineProperty(routerHistory, 'state', {
    enumerable: true,
    get: () => historyNavigation.state.value,
  })

  return routerHistory
}
复制代码

3. createWebHashHistory

注意:在vue2中,我们使用vue-router是区分hash和history两种模式的,主要是在ie9中不支持新增History Api pushStatereplaceState方法,为了兼容ie9,之前的vue-router提供了两种路由模式实现:

  • 基于History pushState和replaceState的浏览地址栈维护,地址记录发生变化监听window.onpopstate事件实现
  • 基于location.hash的实现,hash值改变监听window.onhashChange事件

而在vue3中,由于放弃了对ie9的支持,所以真正意义上,我们使用的都是基于history的路由模式,看源码实现

export function createWebHashHistory(base?: string): RouterHistory {
  // 基础路径处理
  base = location.host ? base || location.pathname + location.search : ''
  // allow the user to provide a `#` in the middle: `/base/#/app`
  // base会再追加一个#符号
  if (!base.includes('#')) base += '#'
  // 看这里-----
  return createWebHistory(base)
}
复制代码

一定要知道,vue3中vue-router是没有真正的hash模式的。采用history模式实现,只是在地址栏添加了#号。

4. RouterLink组件

router-link组件是vue-router注册的一个应用全局组件,提供以标签的形式跳转路由,使用示例

<router-link to="/home">Home</router-link>
复制代码
<router-link :to="{ name: 'user', params: { userId: '123' }}">User</router-link>
复制代码
export const RouterLinkImpl = /*#__PURE__*/ defineComponent({
  name: 'RouterLink',
  props: {
    // 路由目标
    to: {
      type: [String, Object] as PropType<RouteLocationRaw>,
      required: true,
    },
    // 添加or替换栈记录
    replace: Boolean,
    // 自定义激活class样式
    activeClass: String,
    // inactiveClass: String,
    exactActiveClass: String,
    // 自定义标签
    custom: Boolean,
    ariaCurrentValue: {
      type: String as PropType<RouterLinkProps['ariaCurrentValue']>,
      default: 'page',
    },
  },

  useLink,

  setup(props, { slots }) {
    const link = reactive(useLink(props))
    // 获取createRouter提供路由实例
    const { options } = inject(routerKey)!
    // 样式计算
    const elClass = computed(() => ({
      [getLinkClass(
        props.activeClass,
        options.linkActiveClass,
        'router-link-active'
      )]: link.isActive,
      [getLinkClass(
        props.exactActiveClass,
        options.linkExactActiveClass,
        'router-link-exact-active'
      )]: link.isExactActive,
    }))
    // 返回render
    return () => {
      // 
      const children = slots.default && slots.default(link)
      return props.custom
        ? children
        : h(
            'a',
            {
              'aria-current': link.isExactActive
                ? props.ariaCurrentValue
                : null,
              href: link.href,
              // this would override user added attrs but Vue will still add
              // the listener so we end up triggering both
              onClick: link.navigate,
              class: elClass.value,
            },
            children
          )
    }
  },
})
复制代码

router-link的实现比较简单,默认会被渲染为a标签,通多劫持a标签的点击事件来维护路由栈记录并更新视图

5. router-view组件

export const RouterViewImpl = /*#__PURE__*/ defineComponent({
  name: 'RouterView',
  inheritAttrs: false,
  props: {
    // 命名路由视图
    name: {
      type: String as PropType<string>,
      default: 'default',
    }
  },

  setup(props, { attrs, slots }) {
    // 根据location key获取createRouter内部暴露的路由信息
    const injectedRoute = inject(routerViewLocationKey)!
    const routeToDisplay = computed(() => props.route || injectedRoute.value)
    // 路由层级,起始为0
    const depth = inject(viewDepthKey, 0)
    // 获取匹配路由
    const matchedRouteRef = computed<RouteLocationMatched | undefined>(
      () => routeToDisplay.value.matched[depth]
    )
    // 再暴露路由层级,子router-view +1
    provide(viewDepthKey, depth + 1)
    provide(matchedRouteKey, matchedRouteRef)
    provide(routerViewLocationKey, routeToDisplay)

    const viewRef = ref<ComponentPublicInstance>()
  

    return () => {
      // 获取渲染路由信息
      const route = routeToDisplay.value
      const matchedRoute = matchedRouteRef.value
      const ViewComponent = matchedRoute && matchedRoute.components[props.name]
      const currentName = props.name
      // 没有匹配路由,渲染router-view标签内容
      if (!ViewComponent) {
        return normalizeSlot(slots.default, { Component: ViewComponent, route })
      }
      // 渲染匹配路由组件
      const component = h(ViewComponent)
      return (
        component
      )
    }
  },
})
复制代码

router-view组件主要通过判断当前组件嵌套层次,通过层次获取route.matched匹配需要渲染的组件,最后调用h渲染函数渲染匹配路由组件。

分类:
前端
标签:
分类:
前端
标签: