(二)小菜鸡Vue-Router源码阅读——Vue-Router的内置组件

271 阅读2分钟

0.前情提要

Vue.use(VueRouter)的时候注册的内置组件

function install(Vue){
    // ...
    Vue.component('RouterView', View) 
    Vue.component('RouterLink', Link)
    //...
}

1. <router-link>

props

  • to: 可以是字符串,或者是描述目标路由的对象
  • tag: 渲染的html标签名,默认是<a>
  • activeClass: 样式相关
  • exactActiveClass: 样式相关
  • event: 触发该导航的事件,默认是click点击
  • apped: 在当前路径下添加基路径 具体可查API 参考 | Vue Router (vuejs.org)
export default {
  name: 'RouterLink',
  props: {
    to: {
      type: toTypes,
      required: true
    },
    tag: {
      type: String,
      default: 'a'
    },
    custom: Boolean,
    exact: Boolean,
    exactPath: Boolean,
    append: Boolean,
    replace: Boolean,
    activeClass: String,
    exactActiveClass: String,
    ariaCurrentValue: {
      type: String,
      default: 'page'
    },
    event: {
      type: eventTypes,
      default: 'click'
    }
  },
  render (h: Function) {
    const router = this.$router
    const current = this.$route
    
    // 匹配到目标路由信息
    const { location, route, href } = router.resolve(
      this.to,
      current,
      this.append
    )

    //...

    const ariaCurrentValue = classes[exactActiveClass] ? this.ariaCurrentValue : null

    const handler = e => {
      if (guardEvent(e)) {
        if (this.replace) {
          router.replace(location, noop)
        } else {
          router.push(location, noop)
        }
      }
    }

    // 添加事件
    // 点击事件有一个默认守卫
    const on = { click: guardEvent }
    if (Array.isArray(this.event)) {
      // 循环props中的event事件名添加handler
      this.event.forEach(e => {
        on[e] = handler
      })
    } else {
      on[this.event] = handler
    }

    // 开始构造data
    // 先加样式类
    const data: any = { class: classes }

    // 暴露作用域插槽
    const scopedSlot =
      !this.$scopedSlots.$hasNormal &&
      this.$scopedSlots.default &&
      this.$scopedSlots.default({
        href,
        route,
        navigate: handler,
        isActive: classes[activeClass],
        isExactActive: classes[exactActiveClass]
      })
    if (scopedSlot) {
      // 插槽只有一个子元素,直接渲染该子元素
      if (scopedSlot.length === 1) {
        return scopedSlot[0]
      } else if (scopedSlot.length > 1 || !scopedSlot.length) {
        // 多个子元素,就用span包裹一层
        return scopedSlot.length === 0 ? h() : h('span', {}, scopedSlot)
      }
    }

   
    // 标签是a可以用href来跳转
    if (this.tag === 'a') {
      data.on = on
      data.attrs = { href, 'aria-current': ariaCurrentValue }
    } else {
      // 在插槽中找到第一个a标签
      const a = findAnchor(this.$slots.default)
      if (a) {
        a.isStatic = false
        const aData = (a.data = extend({}, a.data))
        aData.on = aData.on || {}
        // 整合原有的事件,和用户定义的事件,用数组的方式保存
        for (const event in aData.on) {
          const handler = aData.on[event]
          if (event in on) {
            aData.on[event] = Array.isArray(handler) ? handler : [handler]
          }
        }
        for (const event in on) {
          if (event in aData.on) {
            // on[event] is always a function
            aData.on[event].push(on[event])
          } else {
            aData.on[event] = handler
          }
        }
        // 增加两个属性
        const aAttrs = (a.data.attrs = extend({}, a.data.attrs))
        aAttrs.href = href
        aAttrs['aria-current'] = ariaCurrentValue
      } else {
        // 没有a标签那就把这些事件给加上
        data.on = on
      }
    }

    // 渲染
    return h(this.tag, data, this.$slots.default)
  }
}

// 阻止一些事件发生时的跳转
function guardEvent (e) {
  if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return
  if (e.defaultPrevented) return
  if (e.button !== undefined && e.button !== 0) return
  if (e.currentTarget && e.currentTarget.getAttribute) {
    const target = e.currentTarget.getAttribute('target')
    if (/\b_blank\b/i.test(target)) return
  }
  if (e.preventDefault) {
    e.preventDefault()
  }
  return true
}

// 递归查找第一个a标签
function findAnchor (children) {
  if (children) {
    let child
    for (let i = 0; i < children.length; i++) {
      child = children[i]
      if (child.tag === 'a') {
        return child
      }
      if (child.children && (child = findAnchor(child.children))) {
        return child
      }
    }
  }
}

2. <router-view>

  • 该组件是函数式组件,不会创建实例,所以他的h方法也得从parent去拿
  • props只有一个name,这是为了做命名视图的功能的
export default {
  name: 'RouterView',
  functional: true,
  props: {
    name: {
      type: String,
      default: 'default'
    }
  },
  render (_, { props, children, parent, data }) {
    data.routerView = true

    const h = parent.$createElement
    const name = props.name
    const route = parent.$route
    const cache = parent._routerViewCache || (parent._routerViewCache = {})

  
    // 组件嵌套的层次,为了后面知道需要渲染的是路由表中的哪个路由状态
    let depth = 0
    // 是否在keep-alive中
    let inactive = false
    while (parent && parent._routerRoot !== parent) {
      const vnodeData = parent.$vnode ? parent.$vnode.data : {}
      if (vnodeData.routerView) {
        depth++
      }
      if (vnodeData.keepAlive && parent._directInactive && parent._inactive) {
        inactive = true
      }
      parent = parent.$parent
    }
    data.routerViewDepth = depth

    if (inactive) {
      const cachedData = cache[name]
      const cachedComponent = cachedData && cachedData.component
      if (cachedComponent) {
        if (cachedData.configProps) {
          fillPropsinData(cachedComponent, data, cachedData.route, cachedData.configProps)
        }
        return h(cachedComponent, data, children)
      } else {
        return h()
      }
    }
    
    // 通过matched和depth获取到当前组件应该渲染的路由对象
    const matched = route.matched[depth]
    // 获取到需要渲染的组件名
    const component = matched && matched.components[name]

    // 有可能到这一层已经没有匹配的东西了
    // 或者匹配到了但是没有对应的组件渲染
    if (!matched || !component) {
      cache[name] = null
      return h()
    }

    // 缓存组件
    cache[name] = { component }

    data.registerRouteInstance = (vm, val) => {
      const current = matched.instances[name]
      if (
        (val && current !== vm) ||
        (!val && current === vm)
      ) {
        matched.instances[name] = val
      }
    }

   
    ;(data.hook || (data.hook = {})).prepatch = (_, vnode) => {
      matched.instances[name] = vnode.componentInstance
    }

    // register instance in init hook
    // in case kept-alive component be actived when routes changed
    data.hook.init = (vnode) => {
      if (vnode.data.keepAlive &&
        vnode.componentInstance &&
        vnode.componentInstance !== matched.instances[name]
      ) {
        matched.instances[name] = vnode.componentInstance
      }
      handleRouteEntered(route)
    }

    const configProps = matched.props && matched.props[name]
    if (configProps) {
      extend(cache[name], {
        route,
        configProps
      })
      fillPropsinData(component, data, route, configProps)
    }

    return h(component, data, children)
  }
}

function fillPropsinData (component, data, route, configProps) {
  // resolve props
  let propsToPass = data.props = resolveProps(route, configProps)
  if (propsToPass) {
    // clone to prevent mutation
    propsToPass = data.props = extend({}, propsToPass)
    // pass non-declared props as attrs
    const attrs = data.attrs = data.attrs || {}
    for (const key in propsToPass) {
      if (!component.props || !(key in component.props)) {
        attrs[key] = propsToPass[key]
        delete propsToPass[key]
      }
    }
  }
}

function resolveProps (route, config) {
  switch (typeof config) {
    case 'undefined':
      return
    case 'object':
      return config
    case 'function':
      return config(route)
    case 'boolean':
      return config ? route.params : undefined
    default:
      if (process.env.NODE_ENV !== 'production') {
        warn(
          false,
          `props in "${route.path}" is a ${typeof config}, ` +
          `expecting an object, function or boolean.`
        )
      }
  }
}