Vue Router源码分析(三)-- RouterLink

256 阅读3分钟

Vue Router源码分析(三)-- RouterLink

作为Vue Router提供的两大组件之一,RouterLink想必没有人会陌生。虽然都知道是渲染了个a标签,想着今日无事,若仔细观摩,兴许还能得到点 ,嗯,自我满足感。

1. RouterLinkOptions

RouterLinkOptions只有toreplace两个属性,很好理解。

export interface RouterLinkOptions {
  /**
   * Route Location the link should navigate to when clicked on.
   */
  to: RouteLocationRaw
  /**
   * Calls `router.replace` instead of `router.push`.
   */
  replace?: boolean
  // TODO: refactor using extra options allowed in router.push. Needs RFC
}

2. RouterLinkProps

RouterLinkPropsRouterLink实际接收的参数类型,在继承了RouterLinkOptionstoreplace的基础上,还多了一些属性,除了custom用于使用插槽自定义RouterLink内部的内容之外,其它的属性无需多大关注。

export interface RouterLinkProps extends RouterLinkOptions {
  /**
   * Whether RouterLink should not wrap its content in an `a` tag. Useful when
   * using `v-slot` to create a custom RouterLink
   */
  custom?: boolean
  /**
   * Class to apply when the link is active
   */
  activeClass?: string
  /**
   * Class to apply when the link is exact active
   */
  exactActiveClass?: string
  /**
   * Value passed to the attribute `aria-current` when the link is exact active.
   *
   * @defaultValue `'page'`
   */
  ariaCurrentValue?:
    | 'page'
    | 'step'
    | 'location'
    | 'date'
    | 'time'
    | 'true'
    | 'false'
}

3. useLink

用于得到link对象,link对象包含了各种路由相关的信息。

  • 解析props.to得到route
  • 通过route和当前路由currentRoute计算出activeRecordIndex
  • 进而根据activeRecordIndex以及routecurrentRoute中的params比对,得到isActive,来标记路由是否激活;
  • navigate是点击链接触发的事件,用于导航至其它路由;
  • 最后组合起来得到link对象。
// TODO: we could allow currentRoute as a prop to expose `isActive` and
// `isExactActive` behavior should go through an RFC
export function useLink(props: UseLinkOptions) {
  const router = inject(routerKey)!
  const currentRoute = inject(routeLocationKey)!
​
  let hasPrevious = false
  let previousTo: unknown = null
​
  const route = computed(() => {
    const to = unref(props.to)
​
    if (__DEV__ && (!hasPrevious || to !== previousTo)) {
      if (!isRouteLocation(to)) {
        if (hasPrevious) {
          warn(
            `Invalid value for prop "to" in useLink()\n- to:`,
            to,
            `\n- previous to:`,
            previousTo,
            `\n- props:`,
            props
          )
        } else {
          warn(
            `Invalid value for prop "to" in useLink()\n- to:`,
            to,
            `\n- props:`,
            props
          )
        }
      }
​
      previousTo = to
      hasPrevious = true
    }
​
    return router.resolve(to)
  })
​
  const activeRecordIndex = computed<number>(() => {
    const { matched } = route.value
    const { length } = matched
    const routeMatched: RouteRecord | undefined = matched[length - 1]
    const currentMatched = currentRoute.matched
    if (!routeMatched || !currentMatched.length) return -1
    const index = currentMatched.findIndex(
      isSameRouteRecord.bind(null, routeMatched)
    )
    if (index > -1) return index
    // possible parent record
    const parentRecordPath = getOriginalPath(
      matched[length - 2] as RouteRecord | undefined
    )
    return (
      // we are dealing with nested routes
      length > 1 &&
        // if the parent and matched route have the same path, this link is
        // referring to the empty child. Or we currently are on a different
        // child of the same parent
        getOriginalPath(routeMatched) === parentRecordPath &&
        // avoid comparing the child with its parent
        currentMatched[currentMatched.length - 1].path !== parentRecordPath
        ? currentMatched.findIndex(
            isSameRouteRecord.bind(null, matched[length - 2])
          )
        : index
    )
  })
​
  const isActive = computed<boolean>(
    () =>
      activeRecordIndex.value > -1 &&
      // 还要判断`params`是否一致
      includesParams(currentRoute.params, route.value.params)
  )
  const isExactActive = computed<boolean>(
    () =>
      activeRecordIndex.value > -1 &&
      // `activeRecordIndex.vaue`值必须等于`matched`的最后一个,才是精准激活的路由
      activeRecordIndex.value === currentRoute.matched.length - 1 &&
      isSameRouteLocationParams(currentRoute.params, route.value.params)
  )
​
  // 有效的导航事件会触发`replace`或`push`
  function navigate(
    e: MouseEvent = {} as MouseEvent
  ): Promise<void | NavigationFailure> {
    if (guardEvent(e)) {
      return router[unref(props.replace) ? 'replace' : 'push'](
        unref(props.to)
        // avoid uncaught errors are they are logged anyway
      ).catch(noop)
    }
    return Promise.resolve()
  }
​
  // devtools only
  if ((__DEV__ || __FEATURE_PROD_DEVTOOLS__) && isBrowser) {
    const instance = getCurrentInstance()
    if (instance) {
      const linkContextDevtools: UseLinkDevtoolsContext = {
        route: route.value,
        isActive: isActive.value,
        isExactActive: isExactActive.value,
        error: null,
      }
​
      // @ts-expect-error: this is internal
      instance.__vrl_devtools = instance.__vrl_devtools || []
      // @ts-expect-error: this is internal
      instance.__vrl_devtools.push(linkContextDevtools)
      watchEffect(
        () => {
          linkContextDevtools.route = route.value
          linkContextDevtools.isActive = isActive.value
          linkContextDevtools.isExactActive = isExactActive.value
          linkContextDevtools.error = isRouteLocation(unref(props.to))
            ? null
            : 'Invalid "to" value'
        },
        { flush: 'post' }
      )
    }
  }
​
  /**
   * NOTE: update {@link _RouterLinkI}'s `$slots` type when updating this
   */
  return {
    route,
    href: computed(() => route.value.href),
    isActive,
    isExactActive,
    navigate,
  }
}

4. RouterLinkImpl

到这里,也就只有一个setup是新的内容。通过useLink得到link对象,最后渲染出RouterLink的内容,默认是渲染a标签,各项属性都来自于link对象;但如果custom属性为真值,那useLink得到的东西也用不上咯,就只渲染插槽里自定义的东西。

export const RouterLinkImpl = /*#__PURE__*/ defineComponent({
  name: 'RouterLink',
  compatConfig: { MODE: 3 },
  props: {
    to: {
      type: [String, Object] as PropType<RouteLocationRaw>,
      required: true,
    },
    replace: Boolean,
    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))
    const { options } = inject(routerKey)!
​
    const elClass = computed(() => ({
      [getLinkClass(
        props.activeClass,
        options.linkActiveClass,
        'router-link-active'
      )]: link.isActive,
      // [getLinkClass(
      //   props.inactiveClass,
      //   options.linkInactiveClass,
      //   'router-link-inactive'
      // )]: !link.isExactActive,
      [getLinkClass(
        props.exactActiveClass,
        options.linkExactActiveClass,
        'router-link-exact-active'
      )]: link.isExactActive,
    }))
​
    return () => {
      const children = slots.default && slots.default(link)
      // 渲染`RouterLink`内部的内容,根据`custom`来决定是否加一层`a`标签
      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
          )
    }
  },
})

RouterLink的原理就到这儿咯。这大热天的,出门吃个饭人都要化了,还是待在屋子里吃着雪糕吹着空调学习,才比较舒坦。至于VueRouter,准备再看个RouterView就结束,一天天的看这些个源码,给孩子累的。