从 0 到 1 实现一个框架BootstrapVue: Lesson 5
如何实现一个<BAvatar />
1. 首先看最核心的 render function
h 的第一个参数是 tag, 比如'div', 表示一种 html 元素
h 的第二个参数是 VNode 的属性, 这里是componentData
h 的第三个参数是 VNode 的 children VNode, 这里有$content, $badge
return h(tag, componentData, [$content, $badge]);
1.1 componentData
staticClass 和 class, style 这些是控制样式的, 也就是最后的显示形式.
attrs 用于无障碍技术
props 控制里面生成的内容
on 作为事件监听的处理
const componentData = {
staticClass: CLASS_NAME,
class: {
// Apply size class
[`${CLASS_NAME}-${size}`]: size && SIZES.indexOf(size) !== -1,
// We use badge styles for theme variants when not rendering `BButton`
[`badge-${variant}`]: !button && variant,
// Rounding/Square
rounded: rounded === true,
[`rounded-${rounded}`]: rounded && rounded !== true,
// Other classes
disabled,
},
style: { ...marginStyle, width: size, height: size },
attrs: { "aria-label": ariaLabel || null },
props: button
? { variant, disabled, type }
: link
? pluckProps(linkProps, this)
: {},
on: button || link ? { click: this.onClick } : {},
};
这里我们对照一下组件的 props 进行阅读:
variant, size, square, rounded 这些在上面的 class 里面都有体现.
variant: {
type: String,
default: 'secondary'
},
size: {
type: [Number, String]
// default: null
},
square: {
type: Boolean,
default: false
},
rounded: {
type: [Boolean, String],
default: false
},
我们这里看一下, size 的转换:
如果 size 是 String 类型, 而且符合数字的正则, 首先会尝试将这个转化为数字, 最后转换成 'px'. 否则直接返回她本身.
这就实现了, 如果是 'sm', 'lg'则使用 class 进行样式的控制, 如果是'24'这样的数字, 则使用 style 进行样式的控制.
export const computeSize = (value) => {
// Parse to number when value is a float-like string
value = isString(value) && RX_NUMBER.test(value) ? toFloat(value, 0) : value;
// Convert all numbers to pixel values
return isNumber(value) ? `${value}px` : value || null;
};
然后看一下, 针对 button 和 link 的处理:
如果是 button, 则只有{ variant, disabled, type }
这三个 prop
如果是 link, 则是linkProps
, 这个之前在BLink
中讲过.
然后两者都可以接收 click 事件并做处理.
props: button
? { variant, disabled, type }
: link
? pluckProps(linkProps, this)
: {},
on: button || link ? { click: this.onClick } : {},
1.2 $content
首先判断是否有 slot, 有的话, 优先级最高.
然后看是否有 src, icon, text 这些 prop. 有的话则分别处理成对应的图片, 图标, 文字.
如果上面所有的都没有, 则处理成默认的内容.
let $content = null;
if (this.hasNormalizedSlot()) {
// Default slot overrides props
$content = h("span", { staticClass: "b-avatar-custom" }, [
this.normalizeSlot(),
]);
} else if (src) {
$content = h("img", {
style: variant ? {} : { width: "100%", height: "100%" },
attrs: { src, alt },
on: { error: this.onImgError },
});
$content = h("span", { staticClass: "b-avatar-img" }, [$content]);
} else if (icon) {
$content = h(BIcon, {
props: { icon },
attrs: { "aria-hidden": "true", alt },
});
} else if (text) {
$content = h(
"span",
{
staticClass: "b-avatar-text",
style: fontStyle,
},
[h("span", text)]
);
} else {
// Fallback default avatar content
$content = h(BIconPersonFill, { attrs: { "aria-hidden": "true", alt } });
}
1.3 $badge
slot 优先于 badge
prop. badgeVariant
和 badgeStyle
用于控制 badge 的样式.
let $badge = h();
const hasBadgeSlot = this.hasNormalizedSlot("badge");
if (badge || badge === "" || hasBadgeSlot) {
const badgeText = badge === true ? "" : badge;
$badge = h(
"span",
{
staticClass: "b-avatar-badge",
class: { [`badge-${badgeVariant}`]: !!badgeVariant },
style: badgeStyle,
},
[hasBadgeSlot ? this.normalizeSlot("badge") : badgeText]
);
}
badge
的位置是靠 top bottom left right 这些来确定的
badgeStyle() {
const { computedSize: size, badgeTop, badgeLeft, badgeOffset } = this
const offset = badgeOffset || '0px'
return {
fontSize: SIZES.indexOf(size) === -1 ? `calc(${size} * ${BADGE_FONT_SIZE_SCALE} )` : null,
top: badgeTop ? offset : null,
bottom: badgeTop ? null : offset,
left: badgeLeft ? offset : null,
right: badgeLeft ? null : offset
}
}
2. <BAvatarGroup />
用于给<BAvatar />
充当 wrapper, 提供统一的样式配置. 样式配置的优先级高于单个的<BAvatar />
.
const $inner = h(
"div",
{
staticClass: "b-avatar-group-inner",
style: this.paddingStyle,
},
this.normalizeSlot()
);
return h(
this.tag,
{
staticClass: "b-avatar-group",
attrs: { role: "group" },
},
[$inner]
);
下面是<BAvatar />
里的 computed:
可以看到, 基本都是先判断 bvAvatarGroup 的存在, 然后优先采用 group 的值.
computedSize() {
// Always use the avatar group size
const { bvAvatarGroup } = this
return computeSize(bvAvatarGroup ? bvAvatarGroup.size : this.size)
},
computedVariant() {
const { bvAvatarGroup } = this
return bvAvatarGroup && bvAvatarGroup.variant ? bvAvatarGroup.variant : this.variant
},
computedRounded() {
const { bvAvatarGroup } = this
const square = bvAvatarGroup && bvAvatarGroup.square ? true : this.square
const rounded = bvAvatarGroup && bvAvatarGroup.rounded ? bvAvatarGroup.rounded : this.rounded
return square ? '0' : rounded === '' ? true : rounded || 'circle'
},
fontStyle() {
const { computedSize: size } = this
const fontSize = SIZES.indexOf(size) === -1 ? `calc(${size} * ${FONT_SIZE_SCALE})` : null
return fontSize ? { fontSize } : {}
},
marginStyle() {
const { computedSize: size, bvAvatarGroup } = this
const overlapScale = bvAvatarGroup ? bvAvatarGroup.overlapScale : 0
const value = size && overlapScale ? `calc(${size} * -${overlapScale})` : null
return value ? { marginLeft: value, marginRight: value } : {}
},
3. 技术总结
- 给不同的角色留好位置
这里的 render function 中, $content
其实才是 avatar 的主体部分, $badge
只是锦上添花.
return h(tag, componentData, [$content, $badge]);
- wrapper 与 children 的关系
BAvatarGroup
充当BAvatar
的 wrapper 组件, 优先级要统一定义好, 到底是 wrapper 高还是 children 高.
这里设计的是优先用BAvatarGroup
的设置.