搞轮子:TabBar标签栏的自救
说到 TabBar 标签栏,我们自然会想到放在 App 底部的标签栏。同时,因为之前开发 Tabs 标签页时,曾经写过一篇文章( 组件开发:从业务到组件Tabs ),讲诉开发 Tabs 的问题和困难。现在遇到的 TabBar 与之大差不差,为什么还要写一篇呢?
原因有二:一是为回顾知识,二是 TabBar 有必要写的知识点
无论是 Tabs 还是 TabBar 组件,都采用父组件控制标签和改变标签,子项提供具体内容的形式
// Tabs
<Tabs value={value}
    onChange={(index) => {
        index && setValue(index);
    }}
    >
    <Tabs.Panel title="标签1">内容 1</Tabs.Panel>
    <Tabs.Panel title="标签2">内容 2</Tabs.Panel>
    <Tabs.Panel title="标签3">内容 3</Tabs.Panel>
</Tabs>
// TabBar
<TabBar
    activeKey={activeKey}
    onChange={(key: any) => {
        setActiveKey(key);
    }}
    >
    <TabBar.Item
        itemKey="home"
        title="主页"
        icon={<IconTabbarHome />}
        />
    <TabBar.Item
        itemKey="financial"
        title="理财"
        icon={<IconTabbarFinancial />}
        />
    <TabBar.Item
        itemKey="user"
        title="我的"
        icon={<IconTabbarUser />}
        />
</TabBar>
思绪源码
他们都用了 React.Children ,这是我们要回顾的。
官网:
React.Children提供了用于处理this.props.children不透明数据结构的实用方法。
而 TabBar 不同的是,他多了 React.cloneElement 和 React.isValidElement
React.cloneElement 介绍
React.cloneElement(element, [config], [...children]);
官网:以 element 元素为样板克隆并返回新的 React 元素,config 中应包含新的 props,key 或 ref 。返回元素的 props 是将新的 props 与原始元素的 props 浅层合并后的结果。新的子元素将取代现有的子元素,如果在 config 中未出现 key 或 ref,那么原始元素的 key 和 ref 将被保留
简单来说,他是个拷贝API,一般与 React.Children 配合,在原 children 上加上其他属性
React.isValidElement 介绍
React.isValidElement(object);
官网:验证对象是否为 React 对象,返回值为 true 或 false
回过头看 TabBar。我们要做的是,不仅要赋予 TabBar.Item 本来就要有的属性,而且要多给它几个属性,如:
- selected:判断它是否被选中。因为 TabBar 控制选择的 key
- onChange:点击后的回调,这里要做”是否能切换标签“的判断,所以要处理
结合三个 React 的顶层 API 后的代码:
...
const items = React.Children.map(children, (element, index) => {
    if (!React.isValidElement(element)) return null;
    return cloneElement(element, {
        key: index,
        title: element.props.title,
        icon: element.props.icon,
        itemKey: element.props.itemKey || index,
        className: element.props.className,
        style: element.props.style,
        selected: getSelected(index, element.props.itemKey),
        onChange: () => onChildChange(element.props.itemKey),
    });
});
...
具体代码可以去看 jing-ui 的源代码,这里做分析,先把它所有的子项都遍历,判断它是否是 React 元素,如果不是,返回 null,如果是拷贝子项原来的数据,并给予两个新的 props。其中 getSelected 代码如下:
const getSelected = (index: number, itemKey: string | number) => {
  if (!activeKey) {
    if (!defaultActiveKey && index === 0) {
      return true;
    }
    return defaultActiveKey === itemKey;
  }
  return activeKey === itemKey;
};
传入当前子项的索引值以及子项(TabBar.Item)的 itemKey 值,判断 TabBar 的 属性 activeKey 是否不存在,再判断 defaultActiveKey是否有或者索引值是否为0,如果都满足,说明默认选中的key没有,那就赋予第一个子项选中;如果 defaultActiveKey 有,就与 itemKey 匹配;再如果 activeKey 存在,就让它与 itemKey 匹配。其实看代码就能明白
onChildChange 代码:
const onChildChange = (value: string | number) => {
  if (isFunction(beforeChange)) {
    let canClick = beforeChange(value);
    if (canClick) {
      canClick.then(() => {
        if (typeof onChange === "function") {
          onChange(value);
        }
      });
    }
    return;
  }
  if (isFunction(onChange)) {
    onChange(value);
  }
};
beforeChange 属性指:切换标签前的回调函数,返回 false 课阻止切换
安全区域的解决
两种方案
一是塞 padding-bottom 样式
- 
.jing-safe-area-bottom { padding-bottom: constant(safe-area-inset-bottom); padding-bottom: env(safe-area-inset-bottom); }
- 
这个方法在 TabBar 上无效,因为 TabBar 注定是底部固定的 
二是包个容器,给它一个 height
- 
.iphonex-extra-height { height: constant(safe-area-inset-bottom); height: env(safe-area-inset-bottom); }
- 
这个方法解决了 
在 props 上我们相对应提供
- enableSafeArea:是否开启底部安全区适配,设置 fixed 时默认开启
- fixed:是否固定在底部,默认固定
具体代码如下:
const enableSafeArea = () => safeAreaInsetBottom ?? fixed;
if (enableSafeArea()) {
    return (
        {/* 提供父容器包裹 */}
        <div className={classnames(`${prefixCls}-container`)}>
            <div className={classnames(prefixCls, className)}>{items}</div>
            <div className="jing-iphonex-extra-height" />
        </div>
    );
}
总结
TabBar 与 Tabs 的不同之处在于,它用了 React.cloneElement,赋予了 (TabBar 中的)children 新的属性,这样我们就能再 TabBar 上控制它是否在切换前使用回调函数,方便我们后续实际业务中的操作