说到 TabBar 标签栏,我们自然会想到放在 App 底部的标签栏。同时,因为之前开发 Tabs 标签页时,曾经写过一篇文章( 组件开发:从业务到组件Tabs ),讲诉开发 Tabs 的问题和困难。现在遇到的 TabBar 与之大差不差,为什么还要写一篇呢?

原因有二:一是为回顾知识,二是 TabBar 有必要写的知识点

无论是 Tabs 还是 TabBar 组件,都采用父组件控制标签和改变标签,子项提供具体内容的形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 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 介绍

1
2
3
4
5
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 介绍

1
React.isValidElement(object)

官网:验证对象是否为 React 对象,返回值为 true 或 false

回过头看 TabBar。我们要做的是,不仅要赋予 TabBar.Item 本来就要有的属性,而且要多给它几个属性,如:

  • selected:判断它是否被选中。因为 TabBar 控制选择的 key
  • onChange:点击后的回调,这里要做”是否能切换标签“的判断,所以要处理

结合三个 React 的顶层 API 后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...
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 代码如下:

1
2
3
4
5
6
7
8
9
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 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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);
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    - 这个方法在 TabBar 上无效,因为 TabBar 注定是底部固定的

    二是包个容器,给它一个 height

    - ```css
    .iphonex-extra-height {
    height: constant(safe-area-inset-bottom);
    height: env(safe-area-inset-bottom);
    }
  • 这个方法解决了

在 props 上我们相对应提供

  • enableSafeArea:是否开启底部安全区适配,设置 fixed 时默认开启
  • fixed:是否固定在底部,默认固定

具体代码如下:

1
2
3
4
5
6
7
8
9
10
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 上控制它是否在切换前使用回调函数,方便我们后续实际业务中的操作