搞轮子: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.cloneElementReact.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 上控制它是否在切换前使用回调函数,方便我们后续实际业务中的操作