Headless 组件即无 UI 组件,框架仅提供逻辑,UI 交给业务实现。这样带来的好处是业务有极大的 UI 自定义空间,而对框架来说,只考虑逻辑可以让自己更轻松的覆盖更多场景,满足更多开发者不同的诉求。
我们以 headlessui-tabs 为例看看它的用法,并读一读 源码。
headless tabs 最简单的用法如下:
import { Tab } from "@headlessui/react";
function MyTabs() {
return (
<Tab.Group>
<Tab.List>
<Tab>Tab 1</Tab>
<Tab>Tab 2</Tab>
<Tab>Tab 3</Tab>
</Tab.List>
<Tab.Panels>
<Tab.Panel>Content 1</Tab.Panel>
<Tab.Panel>Content 2</Tab.Panel>
<Tab.Panel>Content 3</Tab.Panel>
</Tab.Panels>
</Tab.Group>
);
}
以上代码没有做任何逻辑定制,只用 Tab
及其提供的标签把 tabs 的结构描述出来,此时框架能提供最基础的 tabs 切换特性,即按照顺序,点击 Tab
时切换内容到对应的 Tab.Panel
。
此时没有任何额外的 UI 样式,甚至连 Tab
选中态都没有,如果需要进一步定制,需要用框架提供的 RenderProps 能力拿到状态后做业务层的定制,比如选中态:
<Tab as={Fragment}>
{({ selected }) => (
<button
className={selected ? "bg-blue-500 text-white" : "bg-white text-black"}
>
Tab 1
</button>
)}
</Tab>
要实现选中态就要自定义 UI,如果使用 RenderProps 拓展,那么 Tab
就不应该提供任何 UI,所以 as={Fragment}
就表示该节点作为一个逻辑节点而非 UI 节点(不产生 dom 节点)。
类似的,框架将 tabs 组件拆分为 Tab 标题区域 Tab
与 Tab 内容区域 Tab.Panel
,每个部分都可以用 RenderProps 定制,而框架早已根据业务逻辑规定好了每个部分可以做哪些逻辑拓展,比如 Tab
就提供了 selected
参数告知当前 Tab 是否处于选中态,业务就可以根据它对 UI 进行高亮处理,而框架并不包含如何做高亮的处理,因此才体现出该 tabs 组件的拓展性,但响应的业务开发成本也较高。
Headless 的拓展性可以拿一个场景举例:如果业务侧要定制 Tab 标题,我们可以将 Tab.List
包裹在一个更大的标题容器内,在任意位置添加标题 jsx,而不会破坏原本的 tabs 逻辑,然后将这个组件作为业务通用组件即可。
再看更多的配置参数:
控制某个 Tab 是否可编辑:
<Tab disabled>Tab 2</Tab>
Tab 切换是否为手动按 Enter
或 Space
键:
<Tab.Group manual>
默认激活 Tab:
<Tab.Group defaultIndex={1}>
监听激活 Tab 变化:
<Tab.Group
onChange={(index) => {
console.log('Changed selected tab to:', index)
}}
>
受控模式:
<Tab.Group selectedIndex={selectedIndex} onChange={setSelectedIndex}>
用法就介绍到这里。
由此可见,Headless 组件在 React 场景更多使用 RenderProps 的方式提供 UI 拓展能力,因为 RenderProps 既可以自定义 UI 元素,又可以拿到当前上下文的状态,天然适合对 UI 的自定义。
还有一些 Headless 框架如 TanStack table 还提供了 Hooks 模式,如:
const table = useReactTable(options)
return <table {table.getTableProps()}></table>
Hooks 模式的好处是没有 RenderProps 那么多层回调,代码层级看起来舒服很多,而且 Hooks 模式在其他框架也逐渐被支持,使组件库跨框架适配的成本比较低。但 Hooks 模式在 React 场景下会引发不必要的全局 ReRender,相比之下,RenderProps 只会将重渲染限定在回调函数内部,在性能上 RenderProps 更优。
分析的差不多,我们看看 headlessui-tabs 的 源码。
首先组件要封装的好,一定要把内部组件通信问题给解决了,即为什么包裹了 Tab.Group
后,Tab
与 Tab.Panel
就可以产生联动?它们一定要访问共同的上下文数据。答案就是 Context:
首先在 Tab.Group
利用 ContextProvider
包裹一层上下文容器,并封装一个 Hook 从该容器提取数据:
// 导出的别名就叫 Tab.Group
const Tabs = () => {
return (
<TabsDataContext.Provider value={tabsData}>
{render({
ourProps,
theirProps,
slot,
defaultTag: DEFAULT_TABS_TAG,
name: "Tabs",
})}
</TabsDataContext.Provider>
);
};
// 提取数据方法
function useData(component: string) {
let context = useContext(TabsDataContext);
if (context === null) {
let err = new Error(
`<${component} /> is missing a parent <Tab.Group /> component.`
);
if (Error.captureStackTrace) Error.captureStackTrace(err, useData);
throw err;
}
return context;
}
所有子组件如 Tab
、Tab.Panel
、Tab.List
都从 useData
获取数据,而这些数据都可以从当前最近的 Tab.Group
上下文获取,所以多个 tabs 之间数据可以相互隔离。
另一个重点就是 RenderProps 的实现。其实早在 75.精读《Epitath 源码 - renderProps 新用法》 我们就讲过 RenderProps 的实现方式,今天我们来看一下 headlessui 的封装吧。
核心代码精简后如下:
function _render<TTag extends ElementType, TSlot>(
props: Props<TTag, TSlot> & { ref?: unknown },
slot: TSlot = {} as TSlot,
tag: ElementType,
name: string
) {
let {
as: Component = tag,
children,
refName = 'ref',
...rest
} = omit(props, ['unmount', 'static'])
let resolvedChildren = (typeof children === 'function' ? children(slot) : children) as
| ReactElement
| ReactElement[]
if (Component === Fragment) {
return cloneElement(
resolvedChildren,
Object.assign(
{},
// Filter out undefined values so that they don't override the existing values
mergeProps(resolvedChildren.props, compact(omit(rest, ['ref']))),
dataAttributes,
refRelatedProps,
mergeRefs((resolvedChildren as any).ref, refRelatedProps.ref)
)
)
}
return createElement(
Component,
Object.assign(
{},
omit(rest, ['ref']),
Component !== Fragment && refRelatedProps,
Component !== Fragment && dataAttributes
),
resolvedChildren
)
}
首先为了支持 Fragment 模式,所以当制定 as={Fragment}
时,就直接把 resolvedChildren
作为子元素,否则自己就作为 dom 载体 createElement(Component, ..., resolvedChildren)
来渲染。
而体现 RenderProps 的点就在于 resolvedChildren
处理的这段:
let resolvedChildren =
typeof children === "function" ? children(slot) : children;
如果 children
是函数类型,就把它当做函数执行并传入上下文(此处为 slot
),返回值是 JSX 元素,这就是 RenderProps 的本质。
再看上面 Tab.Group
的用法:
render({
ourProps,
theirProps,
slot,
defaultTag: DEFAULT_TABS_TAG,
name: "Tabs",
});
其中 slot
就是当前 RenderProps 能拿到的上下文,比如在 Tab.Group
中就提供 selectedIndex
,在 Tab
就提供 selected
等等,在不同的 RenderProps 位置提供便捷的上下文,对用户使用比较友好是比较关键的。
比如 Tab
内已知该 Tab
的 index
与 selectedIndex
,那么给用户提供一个组合变量 selected
就可能比分别提供这两个变量更方便。
我们总结一下 Headless 的设计与使用思路。
作为框架作者,首先要分析这个组件的业务功能,并抽象出应该拆分为哪些 UI 模块,并利用 RenderProps 将这些 UI 模块以 UI 无关方式提供,并精心设计每个 UI 模块提供的状态。
作为使用者,了解这些组件分别支持哪些模块,各模块提供了哪些状态,并根据这些状态实现对应的 UI 组件,响应这些状态的变化。由于最复杂的状态逻辑已经被框架内置,所以对于 UI 状态多样的业务甚至可以每个组件重写一遍 UI 样式,对于样式稳定的场景,业务也可以按照 Headless + UI 作为整体封装出包含 UI 的组件,提供给各业务场景调用。
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/99ZZdL0vLECKX9-P7Sgx5A
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。