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

91 阅读4分钟

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

如何实现一个<BButton />

首先看最核心的一行

// src/components/button/button.js
const componentData = {
      staticClass: 'btn',
      class: computeClass(props),
      props: computeLinkProps(props),
      attrs: computeAttrs(props, data),
      on
    }
return h(link ? BLink : props.tag, mergeData(data, componentData), children)

这里的 h 相当于 createElement, 是 vue 里面用于构造VNode节点的一个函数

// packages/runtime-core/src/h.ts
export function h(type: any, propsOrChildren?: any, children?: any): VNode

第一个参数type 表示, 要构造的节点类型, 第二个参数 propsOrChildren表示节点上的属性, 第三个参数children表示里面的子节点.

bootstrapvue 这里使用了 mergeData 这个函数来实现props的合并. 我们一个个看传入的参数:

1.staticClass. 顾名思义, 就是静态的class. 2.class. 这里使用了computeClass进行合并.

// src/components/button/button.js
// Compute required classes (non static classes)
const computeClass = props => [
  `btn-${props.variant || 'secondary'}`,
  {
    [`btn-${props.size}`]: props.size,
    'btn-block': props.block,
    'rounded-pill': props.pill,
    'rounded-0': props.squared && !props.pill,
    disabled: props.disabled,
    active: props.pressed
  }
]

这个是为了将传递给<BButton />组件的显示相关的属性进行合并. 我们可以结合文档, 来观察.

// src/components/button/package.json
{
  "prop": "size",
  "description": "Set the size of the component's appearance. 'sm', 'md' (default), or 'lg'"
},
{
  "prop": "variant",
  "description": "Applies one of the Bootstrap theme color variants to the component"
},
{
  "prop": "block",
  "description": "Renders a 100% width button (expands to the width of its parent container)"
},
{
  "prop": "type",
  "description": "The value to set the button's 'type' attribute to. Can be one of 'button', 'submit', or 'reset'"
},
{
  "prop": "pill",
  "description": "Renders the button with the pill style appearance when set to 'true'"
},
{
  "prop": "squared",
  "description": "Renders the button with non-rounded corners when set to 'true'"
},
{
  "prop": "pressed",
  "description": "When set to 'true', gives the button the appearance of being pressed and adds attribute 'aria-pressed=\"true\"'. When set to `false` adds attribute 'aria-pressed=\"false\"'. Tri-state prop. Syncable with the .sync modifier"
}

computeClass这个函数里面, 进行了variant, size, block, pill, squared, disabled, pressed这些属性的合并.

使用的是 vue 的class array 与 class object的混合形式.

在温故知新Vue3 系列里面, 我们曾提过, class array 形式是将array中的class最后用空格拼接在一起, class object形式, 则是key是class, value是控制class是否存在的bool变量.

像 variant, size都是 String类型. 而 block, pill, squared, disabled, pressed都是 Boolean类型. 这在组件定义中的props选项中也有体现.

// src/components/button/button.js
export const props = makePropsConfigurable(
  {
    block: {
      type: Boolean,
      default: false
    },
    disabled: {
      type: Boolean,
      default: false
    },
    size: {
      type: String
      // default: null
    },
    variant: {
      type: String,
      default: 'secondary'
    },
    type: {
      type: String,
      default: 'button'
    },
    tag: {
      type: String,
      default: 'button'
    },
    pill: {
      type: Boolean,
      default: false
    },
    squared: {
      type: Boolean,
      default: false
    },
    pressed: {
      // Tri-state: `true`, `false` or `null`
      // => On, off, not a toggle
      type: Boolean,
      default: null
    },
    ...linkProps
  },
  NAME_BUTTON
)

3.props 的合并.

使用computeLinkProps进行合并. 这里先判断是不是 link. 如果 href或to 属性存在, 或者 tag设置为 'a', 则认定为 <BLink/>. 如果是link, 则返回link需要的props.

// Compute the link props to pass to b-link (if required)
const computeLinkProps = props => (isLink(props) ? pluckProps(linkProps, props) : {})

4.attrs的合并

我们先看他的返回值. 返回的对象为{type, disabled, role, 'aria-disabled', 'aria-pressed', autocomplete, tabindex}的形式

这里 type 和 disabled只有判定为button时, 才会使用, 否则为null.

role, 'aria-disabled', 'aria-pressed' 是用于 ARIA的属性.(ARIA是针对障碍人士的HTML属性规范)

autocomplete, tabindex 分别表示自动完成和tab聚焦.

// Compute the attributes for a button
const computeAttrs = (props, data) => {
  const button = isButton(props)
  const link = isLink(props)
  const toggle = isToggle(props)
  const nonStandardTag = isNonStandardTag(props)
  const hashLink = link && props.href === '#'
  const role = data.attrs && data.attrs.role ? data.attrs.role : null
  let tabindex = data.attrs ? data.attrs.tabindex : null
  if (nonStandardTag || hashLink) {
    tabindex = '0'
  }
  return {
    // Type only used for "real" buttons
    type: button && !link ? props.type : null,
    // Disabled only set on "real" buttons
    disabled: button ? props.disabled : null,
    // We add a role of button when the tag is not a link or button for ARIA
    // Don't bork any role provided in `data.attrs` when `isLink` or `isButton`
    // Except when link has `href` of `#`
    role: nonStandardTag || hashLink ? 'button' : role,
    // We set the `aria-disabled` state for non-standard tags
    'aria-disabled': nonStandardTag ? String(props.disabled) : null,
    // For toggles, we need to set the pressed state for ARIA
    'aria-pressed': toggle ? String(props.pressed) : null,
    // `autocomplete="off"` is needed in toggle mode to prevent some browsers
    // from remembering the previous setting when using the back button
    autocomplete: toggle ? 'off' : null,
    // `tabindex` is used when the component is not a button
    // Links are tabbable, but don't allow disabled, while non buttons or links
    // are not tabbable, so we mimic that functionality by disabling tabbing
    // when disabled, and adding a `tabindex="0"` to non buttons or non links
    tabindex: props.disabled && !button ? '-1' : tabindex
  }
}

5.on的合并

on里面都是 listener, 用于监听事件, 并对事件进行处理

针对 href="#" 添加 空格和回车事件的listener

针对 pressed 触发对应的listener

以及针对focus, 增加或去除聚焦的class

const on = {
  keydown(evt) {
    // When the link is a `href="#"` or a non-standard tag (has `role="button"`),
    // we add a keydown handlers for CODE_SPACE/CODE_ENTER
    /* istanbul ignore next */
    if (props.disabled || !(nonStandardTag || hashLink)) {
      return
    }
    const { keyCode } = evt
    // Add CODE_SPACE handler for `href="#"` and CODE_ENTER handler for non-standard tags
    if (keyCode === CODE_SPACE || (keyCode === CODE_ENTER && nonStandardTag)) {
      const target = evt.currentTarget || evt.target
      stopEvent(evt, { propagation: false })
      target.click()
    }
  },
  click(evt) {
    /* istanbul ignore if: blink/button disabled should handle this */
    if (props.disabled && isEvent(evt)) {
      stopEvent(evt)
    } else if (toggle && listeners && listeners['update:pressed']) {
      // Send `.sync` updates to any "pressed" prop (if `.sync` listeners)
      // `concat()` will normalize the value to an array without
      // double wrapping an array value in an array
      concat(listeners['update:pressed']).forEach(fn => {
        if (isFunction(fn)) {
          fn(!props.pressed)
        }
      })
    }
  }
}

if (toggle) {
  on.focusin = handleFocus
  on.focusout = handleFocus
}

// Focus handler for toggle buttons
// Needs class of 'focus' when focused
const handleFocus = evt => {
  if (evt.type === 'focusin') {
    addClass(evt.target, 'focus')
  } else if (evt.type === 'focusout') {
    removeClass(evt.target, 'focus')
  }
}

技术总结

  1. 使用构造函数, 构造出节点
h(link ? BLink : props.tag, mergeData(data, componentData), children)
  1. 针对不同的情况, 合并参数

computeClass, computeLinkProps, computeAttrs这些都是为了合并参数而创造的helper method.

  1. 配套使用不同的class, 渲染出不同的效果

这里的 focusin触发增加 'focus' 这个class, 从而按钮表现出聚焦的效果.