从 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')
}
}
技术总结
- 使用构造函数, 构造出节点
h(link ? BLink : props.tag, mergeData(data, componentData), children)
- 针对不同的情况, 合并参数
computeClass, computeLinkProps, computeAttrs
这些都是为了合并参数而创造的helper method.
- 配套使用不同的class, 渲染出不同的效果
这里的 focusin
触发增加 'focus' 这个class, 从而按钮表现出聚焦的效果.