如何实现一个<BAvatar />

92 阅读3分钟

从 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. badgeVariantbadgeStyle 用于控制 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. 技术总结

  1. 给不同的角色留好位置

这里的 render function 中, $content其实才是 avatar 的主体部分, $badge只是锦上添花.

return h(tag, componentData, [$content, $badge]);
  1. wrapper 与 children 的关系

BAvatarGroup充当BAvatar的 wrapper 组件, 优先级要统一定义好, 到底是 wrapper 高还是 children 高.

这里设计的是优先用BAvatarGroup的设置.