vue-router 源码解析(四)-router-link、router-view实现

导语

  • 在之前的三篇文章介绍了三种路由对象的创建、创建路由匹配对象matcher、路由守卫的实现,而本文将介绍router-link、router-view是如何联动整个页面改变的,两个组件的核心都是拿到install中provide的响应式信息,从而在组件内部监听信息修改

挂载全局组建

  • 注册router-link、router-view这俩全局组件
  • 通过provide API提供currentRoute、reactiveRoute这俩响应式路由信息给组件,currentRoute会在navigate过程中改变
export function createRouter(options: RouterOptions): Router {
  const matcher = createRouterMatcher(options.routes, options)
  const routerHistory = options.history
  
  // 默认定义的route路由信息({ path:'/',...},注意这里变成了浅层代理
  // currentRoute在router-view中获取到当前路由信息
  const currentRoute = shallowRef<RouteLocationNormalizedLoaded>(
    START_LOCATION_NORMALIZED
  )
  
  // ...
  const router: Router = {
	addRoute,
	removeRoute,
	//...,
	install(app: App) {
	  const router = this
      // 创建 RouterLink、RouterView 这俩全局组件
      app.component('RouterLink', RouterLink)
      app.component('RouterView', RouterView)
      // ...
	  
	  // router-link中获取的响应式信息
      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])
      }

      app.provide(routerKey, router)
      app.provide(routeLocationKey, reactive(reactiveRoute)) // 提供给router-link
      app.provide(routerViewLocationKey, currentRoute) // 提供给router-view
	  
	  // ...
	}
  }
}

router-link

  • 组件通过渲染函数默认为a标签实现
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))
    // createRouter 中进行provide
    const { options } = inject(routerKey)!
	
	// 处理路由高亮 class
	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 () => {
      // 获取子组件,并将link信息传入props
      const children = slots.default && slots.default(link)
      // 自定义标签,默认为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
        )
    }
	

  }
})

useLink

  • 返回路由相关信息:路由高亮、导航、路由信息
  • router-link 跳转会通过a标签进行,但会阻止a标签的默认行为,调用push->pushWithRedirect…,和通过API导航一致
export function useLink(props: UseLinkOptions) {
  // 在install中 provide,获取到router信息
  const router = inject(routerKey)!
  // currentRoute始终指向当前页面路由信息
  const currentRoute = inject(routeLocationKey)!
  // 路由匹配,并返回解析结果
  const route = computed(() => router.resolve(unref(props.to)))

  // 返回当前页面路由记录的index
  const activeRecordIndex = computed<number>(() => {
    // 根据传递的参数to获取路由匹配记录
    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
    // 获取路由记录的path
    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
    )
  })
  // 当前路由以及父路径上的路由都会active
  const isActive = computed<boolean>(
    () =>
      activeRecordIndex.value > -1 &&
      includesParams(currentRoute.params, route.value.params)
  )
  // 精确匹配,只有当前路由会active
  const isExactActive = computed<boolean>(
    () =>
      activeRecordIndex.value > -1 &&
      activeRecordIndex.value === currentRoute.matched.length - 1 &&
      isSameRouteLocationParams(currentRoute.params, route.value.params)
  )

  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()
  }


  /**
 * NOTE: update {@link _RouterLinkI}'s `$slots` type when updating this
   */
  return {
    route,
    href: computed(() => route.value.href),
    isActive,
    isExactActive,
    navigate,
  }
}

guardEvent 事件守卫

  • 阻止a标签的一些行为
function guardEvent(e: MouseEvent) {
  // 不能通过按键导航
  if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return
  // 表明当前事件是否调用了 event.preventDefault()方法。
  if (e.defaultPrevented) return
  if (e.preventDefault) e.preventDefault()
  // 不能通过右键导航
  if (e.button !== undefined && e.button !== 0) return
  // 当设置了a标签的属性 target="_blank" 时,不允许导航
  if (e.currentTarget && e.currentTarget.getAttribute) {
    const target = e.currentTarget.getAttribute('target')
    if (/\b_blank\b/i.test(target)) return
  }
  return true
}

router-view

在这里插入图片描述

export const RouterViewImpl = /*#__PURE__*/ defineComponent({
  name: 'RouterView',
  inheritAttrs: false,
  props: {
    name: {
      type: String as PropType<string>,
      default: 'default',
    },
    route: Object as PropType<RouteLocationNormalizedLoaded>,
  },

  // Better compat for @vue/compat users
  // https://github.com/vuejs/router/issues/1315
  compatConfig: { MODE: 3 }, // 装饰器模式相关
  setup(props, { attrs, slots }) {
  	// install中进行provide
  	const injectedRoute = inject(routerViewLocationKey)!
    // 当前要展示的路由信息
    const routeToDisplay = computed<RouteLocationNormalizedLoaded>(
      () => props.route || injectedRoute.value
    )
    // 匹配的路由记录深度,因为matched中会包含当前路由记录以及上层记录
    const injectedDepth = inject(viewDepthKey, 0)
	
	 // 获取匹配的路由记录深度
    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]
    )
    
    // 向外提供信息,比如 matchedRouteKey 会在setup中使用的路由守卫inject,拿到对应的路由记录,往记录上保存守卫回调
	provide(
      viewDepthKey,
      computed(() => depth.value + 1)
    )
    provide(matchedRouteKey, matchedRouteRef)
    provide(routerViewLocationKey, routeToDisplay)
	
	// 用于监听组件是否挂载、路由记录是否改变
	const viewRef = ref<ComponentPublicInstance>()
	    watch(
      () => [viewRef.value, matchedRouteRef.value, props.name] as const,
      ([instance, to, name], [oldInstance, from, oldName]) => {
        // copy reused instances
        if (to) {
		  // 路由跳转复用组件
          to.instances[name] = instance

          // 不同路由记录会复用同一个组件,但是路由记录不同,所以这里拷贝之前路由记录中,如果在setup中使用了守卫
          if (from && from !== to && instance && instance === oldInstance) {
            if (!to.leaveGuards.size) {
              to.leaveGuards = from.leaveGuards
            }
            if (!to.updateGuards.size) {
              to.updateGuards = from.updateGuards
            }
          }
        }

        // 当监听到viewRef,即组件ref挂载后,调用beforeRouteEnter中next传入的回调,使得回调中能获取到组件实例
        if (
          instance &&
          to &&
          // if there is no instance but to and from are the same this might be
          // the first visit
          (!from || !isSameRouteRecord(to, from) || !oldInstance)
        ) {
          ; (to.enterCallbacks[name] || []).forEach(callback =>
            callback(instance)
          )
        }
      },
      { flush: 'post' } // 组件更新后触发
    )
	
	// vnode方式创建组件
    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
      
      // 获取命名路由,router-view 不传入name,默认为'default'
      const ViewComponent =
        matchedRoute && matchedRoute.components![currentName]	
      // 没有匹配到对应路由,啥也不展示
      if (!ViewComponent) {
        return normalizeSlot(slots.default, { Component: ViewComponent, route })
      }
      
      // 路由配置中开发者传递给路由组件的参数
      const routePropsOption = matchedRoute.props[currentName]
      const routeProps = routePropsOption
        ? routePropsOption === true
          ? route.params
          : typeof routePropsOption === 'function'
            ? routePropsOption(route)
            : routePropsOption
        : null
	  
	  // onVnodeUnmounted vnode的生命周期,类似的还有:onVnodeBeforeMount ...
      const onVnodeUnmounted: VNodeProps['onVnodeUnmounted'] = vnode => {
        // remove the instance reference to prevent leak
        if (vnode.component!.isUnmounted) {
          matchedRoute.instances[currentName] = null  // 避免内存泄漏
        }
      }
      
      // 通过 vnode 格式创建匹配的路由组件
      const component = h(
        ViewComponent, // 第一个参数可以直接传入组件
        assign({}, routeProps, attrs, {
          onVnodeUnmounted,
          ref: viewRef,  // ref 在这里添加
        })
      )
      
      return (
        // pass the vnode to the slot as a prop.
        // h and <component :is="..."> both accept vnodes
        // router-view 是否存在slot内容
        normalizeSlot(slots.default, { Component: component, route }) ||
        component
      )
	  
    }
    
  }
}

/**
 * 处理这种情况:
 * <router-view v-slot="{ Component, route }">
 *   <transition :name="route.meta.transition">
 *     <component :is="Component" />
 *   </transition>
 * </router-view>
 * 
 */

// 处理route-view插槽的情况
function normalizeSlot(slot: Slot | undefined, data: any) {
  if (!slot) return null
  // 将data作为props传入,使得外面能通过v-slot获取到,具体查看vue文档的scope slot
  const slotContent = slot(data) //slot()会返回虚拟node, slot.default默认为非命名插槽元素
  return slotContent.length === 1 ? slotContent[0] : slotContent
}

composition 守卫中获取记录并保存回调

// setup中使用的路由守卫
export function onBeforeRouteLeave(leaveGuard: NavigationGuard) {
  // matchedRouteKey在router-view中provide,返回当前匹配的路由记录
  const activeRecord: RouteRecordNormalized | undefined = inject(
    matchedRouteKey,
    // to avoid warning
    {} as any
  ).value

  if (!activeRecord) {
    __DEV__ &&
      warn(
        'No active route record was found when calling `onBeforeRouteLeave()`. Make sure you call this function inside a component child of <router-view>. Maybe you called it inside of App.vue?'
      )
    return
  }

  registerGuard(activeRecord, 'leaveGuards', leaveGuard)
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值