从 0 到 1 实现一个框架BootstrapVue: Lesson 2

227 阅读4分钟

从 0 到 1 实现一个框架BootstrapVue: Lesson 2

如何实现一个<BLink />

1.首先看最核心的 render function

render 里面的代码, 用于控制最终渲染出来的效果

render() {
  const { active, disabled, computedTag, isRouterLink, bvAttrs } = this

  return h(
    isRouterLink ? resolveComponent(computedTag) : computedTag,
    {
      class: [{ active, disabled }, bvAttrs.class],
      style: bvAttrs.style,
      attrs: this.computedAttrs,
      props: this.computedProps,
      // We must use `nativeOn` for `<router-link>`/`<nuxt-link>` instead of `on`
      [isRouterLink ? 'nativeOn' : 'on']: this.computedListeners
    },
    this.normalizeSlot()
  )
}

h 相当于 createElement, 是构造 VNode 的一个方法. 你所看到的视图是由很多个 VNode 构成的.

h的第一个参数, 表示 VNode 的类型. 这里用 isRouterLink 做了一个判断, 如果这个 BLink 代表一个 Vue 的 Router Link, 则使用resolveComponent(computedTag), 否则使用computedTag.

这里使用了一个 helper function. 判断 tag 存在且不为'a'的, 就是 routerLink.

export const isRouterLink = (tag) => !!(tag && !isTag(tag, "a"));

如果是 RouterLink, 则使用 resolveComponent(computedTag) 作为 VNode 的 type. 此时 type 是组件类型. 否则用 computedTag, 比如 div.

h的第二个参数, 表示 VNode 的属性.

这里控制样式的有 class 和 style, class 中使用了 array 和 object 的混合形式. 只有当 BLink active 和 disabled 为 true 时, 才会添加对应的 active 和 disabled class.

bvAttrs.class 和 bvAttrs.style 是统一表示其它的 class 和 style.

computedAttrs 和 computedProps 也是表示处理过后的 attrs 和 props.

最后加上 listeners 用于处理各种交互事件.

h的第三个参数, 表示这个 VNode 的子节点.

这里使用了 slot.

2.过一遍组件的参数

({
  "prop": "href",
  "description": "Denotes the target URL of the link for standard a links"
},
{
  "prop": "rel",
  "description": "Sets the 'rel' attribute on the rendered link"
},
{
  "prop": "target",
  "description": "Sets the 'target' attribute on the rendered link"
})

上面这些是BLink基本的 props 参数. 还有部分 props 是属于nuxt-linkrouter-link里的.

router-link 部分. 这部分和 vue-router 里面定义的部分基本一致.

// <router-link> specific props
export const routerLinkProps = {
  to: {
    type: [String, Object],
    default: null,
  },
  append: {
    type: Boolean,
    default: false,
  },
  replace: {
    type: Boolean,
    default: false,
  },
  event: {
    type: [String, Array],
    default: "click",
  },
  activeClass: {
    type: String,
    // default: undefined
  },
  exact: {
    type: Boolean,
    default: false,
  },
  exactActiveClass: {
    type: String,
    // default: undefined
  },
  routerTag: {
    type: String,
    default: "a",
  },
};

nuxt-link 部分. 与 nuxt 里描述的一致.

// <nuxt-link> specific props
export const nuxtLinkProps = {
  prefetch: {
    type: Boolean,
    // Must be `null` to fall back to the value defined in the
    // `nuxt.config.js` configuration file for `router.prefetchLinks`
    // We convert `null` to `undefined`, so that Nuxt.js will use the
    // compiled default. Vue treats `undefined` as default of `false`
    // for Boolean props, so we must set it as `null` here to be a
    // true tri-state prop
    default: null,
  },
  noPrefetch: {
    type: Boolean,
    default: false,
  },
};

3.看一下参数是如何实现可配置的

这里有一个makePropsConfigurable, 第一项参数是 props options, 第二项参数是一个 String, 用来表示配置的这个 props 属于谁.

export const props = makePropsConfigurable(
  {
    href: {
      type: String,
      default: null,
    },
    rel: {
      type: String,
      // Must be `null` if no value provided
      default: null,
    },
    target: {
      type: String,
      default: "_self",
    },
    active: {
      type: Boolean,
      default: false,
    },
    disabled: {
      type: Boolean,
      default: false,
    },
    ...routerLinkProps,
    ...nuxtLinkProps,
    // To support 3rd party router links based on `<router-link>` (i.e. `g-link` for Gridsome)
    // Default is to auto choose between `<router-link>` and `<nuxt-link>`
    // Gridsome doesn't provide a mechanism to auto detect and has caveats
    // such as not supporting FQDN URLs or hash only URLs
    routerComponentName: {
      type: String,
      // default: undefined
    },
  },
  NAME_LINK
);

makePropsConfigurable里面用了一个 reduce, reduce 的形式是

arr.reduce(callback( accumulator, currentValue[, index[, array]] ) {
  // return result from executing something for accumulator or currentValue
}[, initialValue])

这里将传进去的 props 的每一项, 即每个单独的 prop, 都用 getComponentConfig根据 componentKey 去获取配置的值, 如果没有, 则 falllback 到当前的默认值

// Make a props object configurable by global configuration
// Replaces the current `default` key of each prop with a `getComponentConfig()`
// call that falls back to the current default value of the prop
export const makePropsConfigurable = (props, componentKey) =>
  keys(props).reduce((result, prop) => {
    const currentProp = props[prop];
    const defaultValue = currentProp.default;

    result[prop] = {
      ...cloneDeep(currentProp),
      default() {
        return getComponentConfig(
          componentKey,
          prop,
          isFunction(defaultValue) ? defaultValue() : defaultValue
        );
      },
    };

    return result;
  }, {});

4.看一下 Mixins

这里使用了 3 个 mixins, 分别是 attrsMixin, listenersMixin, normalizeSlotMixin

// @vue/component
export const BLink = /*#__PURE__*/ defineComponent({
  name: NAME_LINK,
  // Mixin order is important!
  mixins: [attrsMixin, listenersMixin, normalizeSlotMixin],
  inheritAttrs: false,
  props,
  ...
})

先看到 attrsMixin, 这个 mixin 的作用主要是把$attrs代理到bvAttrs

import { makePropCacheMixin } from "../utils/cache";

export default makePropCacheMixin("$attrs", "bvAttrs");

$attrs的属性深拷贝到bvAttrs

export const makePropCacheMixin = (
  propName,
  proxyPropName,
  normalizePropFn = null
) =>
  defineComponent({
    data() {
      return {
        [proxyPropName]: cloneDeep(
          isFunction(normalizePropFn)
            ? normalizePropFn(this[propName])
            : this[propName]
        ),
      };
    },
    watch: {
      // Work around unwanted re-renders: https://github.com/vuejs/vue/issues/10115
      [propName]: makePropWatcher(proxyPropName, normalizePropFn),
    },
  });

再看 listenersMixin, 这个 mixin 的作用主要是把$listeners$attrs代理到bvListeners

// --- Mixin ---
export default makePropCacheMixin(
  isVue2 ? "$listeners" : "$attrs",
  "bvListeners",
  isVue2 ? null : normalizeProp
);

normalizeSlotMixin, 这个 mixin 的作用主要是提供判断 slot 是否存在, 以及获取 slot 对应的 VNode 的方法.

export default defineComponent({
  methods: {
    hasNormalizedSlot(name = SLOT_NAME_DEFAULT) {
      // Returns `true` if the either a `$scopedSlot` or `$slot` exists with the specified name
      // `name` can be a string name or an array of names
      return hasNormalizedSlot(
        name,
        isVue2 ? this.$scopedSlots : {},
        this.$slots
      );
    },
    normalizeSlot(name = SLOT_NAME_DEFAULT, scope = {}) {
      // Returns an array of rendered VNodes if slot found
      // Returns `undefined` if not found
      // `name` can be a string name or an array of names
      const vNodes = normalizeSlot(
        name,
        scope,
        isVue2 ? this.$scopedSlots : {},
        this.$slots
      );
      return vNodes ? concat(vNodes) : vNodes;
    },
  },
});

5.看一下事件处理

首先会对 evt 进行校验, 看是不是真的事件对象, 以及当前状态是否为 disabled

const { isRouterLink } = this
const evtIsEvent = isEvent(evt)
const suppliedHandler = this.bvListeners.click
if (evtIsEvent && this.disabled) {
  // Stop event from bubbling up
  // Kill the event loop attached to this specific `EventTarget`
  // Needed to prevent `vue-router` for doing its thing
  stopEvent(evt, { immediatePropagation: true })
}
...

然后判断是不是 RouterLink, 如果是, 则触发对应的点击事件.

...
else {
  // TODO: Check if this is relevant for Vue 3 / Vue Router 4
  /* istanbul ignore next */
  if (isRouterLink && evt.currentTarget.__vue__) {
    // Router links do not emit instance `click` events, so we
    // add in an `$emit('click', evt)` on its Vue instance
    evt.currentTarget.__vue__.$emit(EVENT_NAME_CLICK, evt)
  }

接着对于该点击事件, 触发对应的 handler. 最后在$root 上 emit 对应的合成事件.这里是'bv::link::click'.

如果 href 是'#', 则阻止页面滚动

// Call the suppliedHandler(s), if any provided
concat(suppliedHandler)
  .filter(h => isFunction(h))
  .forEach(handler => {
    handler(...arguments)
  })
// Emit the global `$root` click event
this.$root.$emit(ROOT_EVENT_NAME_LINK_CLICKED, evt)
}
// Stop scroll-to-top behavior or navigation on
// regular links when href is just '#'
if (evtIsEvent && !isRouterLink && this.computedHref === '#') {
  stopEvent(evt, { propagation: false })
}

6.技术总结

  1. 使用构造函数, 构造出节点

通过 isRouterLink 进行判断, 有选择的构造出 VNode

  1. 使用 Mixins, 减少重复代码

mixins: [attrsMixin, listenersMixin, normalizeSlotMixin]

分别提供了代理, 和 slot 的 helper function.

  1. 事件处理

disabled 的时候不处理, 正常状态下该 emit 到上层组件.