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