金桔
金币
威望
贡献
回帖0
精华
在线时间 小时
|
做好了「被喷」的准备,我来贡献一个「文不对题」的回答。我们不妨先把题干中的「React hooks」这个主语剥离,从「 ui」和「逻辑」这两个关键词切入分析,结合近几年前端流行的 headless ui,从更大的背景下,来重新思考这个问题。
<hr/>UI 和逻辑
就拿知乎-创作中心举例,我们看几个页面截图:
创作中心-潜力问题

创作中心-近期热点

创作中心-瓦力保镖

你有没有觉得这些页面都「长得很像」?
我们当然可以将 table / list 抽象,用类似 scheme 驱动的方式终结各个页面烟囱式实现的状况,最大化提升开发效率。但是,你再看看下面这个截图:
创作中心-热点问题模块

哎呦,长得好像不一样了。前端开发同学应该深有体会,阻碍表单 / 列表类统一的一个原因是各场景样式、ui 都有差异。在设计层面,可能由于业务需求,很难做到统一,不得已自带设计体系;另一方面,后端数据模型没有统一,因此对接数据前后涉及到的前端状态管理、数据流转也未能统一,复用性大打折扣。
而躲在这些需求的背后,本质都是是同一套数据模型的渲染。
现实情况可能却是,ui 层面离领域模型很远,上图中举例到的知乎创作中心场景,产品经理可能某天突然想展示某些数据,突然想加入 「海盐计划」 4.0,「灯塔计划」,「致知计划」等明细,从 ui 上很容易把数据模型拆解的七零八碎,后继开发者很难从页面看出其背后的领域和数据模型。前端从此和产品、业务渐行渐远。由于我们对于领域知识理解严重匮乏,逐步沦为切图和填充数据字段的工具人,而真正能够进行统一抽象的数据模型,状态管理机制,副作用处理没有得倒应有的沉淀;后端同学面对着分散各自领域的存储表,也很难有端到端渲染层面的数据模型概念。
<hr/>Headless to resuce
我们先对 headless ui 下一个定义:
Headless 组件即无 ui 组件,表示仅提供 UI 元素和交互的数据状态逻辑、状态、流转处理和API,但不提供标记、样式或预构建实现(或另外实现)。
事实上,构建复杂 ui 最难的部分通常围绕状态,事件,副作用,数据的计算和管理。通过将标记,样式和实现细节先剔除,我们的逻辑和组件可以更模块化和重复使用。这样带来的好处是业务有极大的 ui 自定义空间,而对框架来说,只考虑逻辑可以让自己更轻松的覆盖更多场景,满足更多开发者不同的诉求。
Headless ui 是如何解决问题的呢?简单说,headless ui 组件开发者需要关注逻辑节点而非 UI 节,重点工作包括设计并实现 combinators/primitives, data-processing, state-management, business logic。这样一来,开发者从日常的「实现 markup, styles and implementation details」工作中解放出来,最终目标是提供更加 modular and reusable 的组件。
分工和职能上,根据团队可能会分化为基础能力前端研发和 UI 侧前端研发来说,对应:
- 基础能力前端研发只需要聚焦广义的 functional features,这里面包括了数据流设计等
- ui 侧前端研发具备完全的组件 appearance(any style) 控制权,更贴合业务需求
这样两种类型的前端开发者(业务 ui 敏感 & 模型抽象敏感),都有各自的主战场和技能发挥舞台。
Headless ui 和 hooks
真正理解 React hooks 的同学,可能会联想到 React Hooks 是 Headless ui 在 React 世界中得以实现的基石。从面试八股文中,我们就知道 hooks 的好处是可以自定义 hooks,将业务逻辑封装在自定义的 hooks 里,然后将状态和控制器导出给 ui 使用,大大减少了 ui 里的模板代码,并且有效分离了 ui 定义和业务定义、层次分明。(from Headless UI 和 hooks 的一点思考)
在 不败花丶:全新的 React 组件设计理念 Headless UI 文章中,作者认为「正是因为 React Hooks 的诞生,使 Headless ui 组件在技术上成为可能,这也是它为什么最近才开始流行的原因。」
我们不妨来看一个有趣的例子(headless-user-interface-components):

业务侧需求:在抛硬币实现猜正反的场景中,实现一个相关硬币组件(先不描述的太具体,这里只做一个例子)。
// 第一版实现
const CoinFlipV0 = () => Math.random() < 0.5 ? <div>Heads</div> : <div>Tails</div>;
// 实现样式
const CoinFlipV1 = () => Math.random() < 0.5
? (<div><img src={HeadsCoin} alt=&#34;Heads&#34; /></div>)
: (<div><img src={TailsCoin} alt=&#34;Tails&#34; /></div>);
- 业务需求变更 / 扩大 1:一个新场景需要在页面上使用 label 显示抛硬币结果。对应实现上,组件代码需要加入 showLabels 作为组件参数
export const CoinFlipV2 = ({ showLabels = false }) => Math.random() < 0.5
? (<div><img src={HeadsCoin} alt=&#34;Heads&#34; />
{showLabels && <span>Heads</span>}
</div>)
: (<div><img src={TailsCoin} alt=&#34;Tails&#34; />
{showLabels && <span>Tail</span>}
</div>);
- 业务需求变更 / 扩大 2:一个新场景需要设计有一个按钮,来实现执行「重新抛硬币」的能力。对应组件实现上,我们只好加入一个 showButton 作为组件参数。注意,这时候已经是该组件的第三版迭代了
export const CoinFlipV3 = ({ showLabels = false, , showButton = false }) => {
const [state, setState] = React.useState(flip());
const handleClick = () => setState(flip());
return (
<>
{showButton && <button onClick={handleClick}>Reflip</button>}
{state.flipResults < 0.5 ? (
<div>
<img src={HeadsCoin} alt=&#34;Heads&#34; />
{showLabels && <span>Heads</span>}
</div>
) : (
<div>
<img src={TailsCoin} alt=&#34;Tails&#34; />
{showLabels && <span>Tails</span>}
</div>
)}
</>
);
}
在 headless 的设计理念下,我们将组件的逻辑 logic 和样式 & UI 部分接耦(这里还不涉及到数据交互,更多的工程场景还会涉及到的相关 API 数据处理等)。
- 实现纯 logic 层组件,样式 & ui 部分通过渲染业务自定义的 children 的方式实现:
export const CoinFlipLogic = ({ children }) => {
const [state, setState] = React.useState(flip());
const handleClick = () => setState(flip());
return children({
rerun: handleClick,
isHeads: state.flipResults < 0.5,
});
};
注意看,我们的实现中只关注硬币正反面 state 以及相关重置状态按钮的响应事件,并通过 isHeads 和 rerun 两个「能力」提供给业务使用。
那么相关业务使用场景(严格来说这还不是最终的业务实现层,而是介于业务和基础实现的中间层,即 base 上层的 biz 能力)对应的代码如下:
export const CoinFlipV4 = ({ showLabels = false, showButton = false }) => (
<CoinFlipLogic>
{({ rerun, isHeads }) => (
<>
{showButton && <button onClick={rerun}>Reflip</button>}
{isHeads ? (
<div>
<img src={HeadsCoin} alt=&#34;Heads&#34; />
{showLabels && <span>Heads</span>}
</div>
) : (
<div>
<img src={TailsCoin} alt=&#34;Tails&#34; />
{showLabels && <span>Tails</span>}
</div>
)}
</>
)}
</CoinFlipLogic>
);
代码似乎变多了,但是逻辑更清晰了,分工更彻底了,对应业务能力更强了。
- 进一步优化,甚至我们可以将 isHeads 等逻辑拆的更干净,此时是一个 Probability 组件:
const run = () => ({
random: Math.random(),
});
export const Probability = ({ children, threshold }) => {
const [state, setState] = React.useState(run());
const handleChange = () => setState(run());
return children({
rerun: handleChange,
result: state.random < threshold,
});
};
使用方式:
export const RollDiceV1 = () => (
<Probability threshold={1 / 6}>
{({ rerun, result }) => (
<div>
<span onMouseOver={rerun}>Roll the dice!</span>
{result ? (
<div>Big winner!</div>
) : (
<div>You win some, you lose most.</div>
)}
</div>
)}
</Probability>
);
注意,这时候 interface 完全变更了(threshold, result),能够响应的事件也扩大了(这时候 rerun 的触发逻辑是鼠标划过事件)。
这个时候,(分离了 ui 和业务逻辑的)React hooks 该出场了,结合 React hooks,上面的这个例子可以得到非常天然的落地实现。如下:
export const useProbability = ({ threshold }) => {
const [state, setState] = React.useState(run());
const handleChange = () => setState(run());
return {
rerun: handleChange,
result: state.random < threshold,
};
};
export const RollDiceV2 = () => {
const { rerun, result } = useProbability({threshold: 1 / 6});
<div>
<span onMouseOver={rerun}>Roll the dice!</span>
{result ? <div>Big winner!</div> : <div>You win some, you lose most.</div>}
</div>;
};
Headless ui 和 React hooks 背后的架构思想变革
如果你理解了上面场景想要表达的 headless 概念,那么你可能对 React 全面推进 hooks feature 革命的背后逻辑也会更加清楚。
React hooks,headless ui 理念轰轰烈烈推进背后是 ui library 向 -> logic utility 的转型或分化,都是我们更优雅地前端组件级 biz, base 合理分层的武器。
而反观像 antd 这种 ui/component library 大而全的武器,毫无疑问能帮助我们迅速开发,完成从零到一的建设,也能支撑迅速迭代的需求。但总有一天,随着业务的发展,逻辑的膨胀,前端在组件的架构上总会出现明显的需要升级的暗号,这些暗号就藏在我们上述开发场景的页面截图背后,藏在重复代码的背后。
除此之外,更多需要考虑 headless ui 的一些表象:系统难以维护;系统变得更加脆弱,开发人员不知道在进行变更的时候会破坏其他功能,或者需要承担较多的心智负担来理解消化;要从一个技术栈转移到另一个技术栈时不得不对整个系统进行修改;当多个团队在同一个代码库中进行交付时,会造成代码库的混乱,我们难以保持应用处于最新状态,难以升级版本;在没有团队负责维护和改进的情况下,有些代码会被遗弃,团队之间没有明确的责任; 纵观社区上的大部分现存巨型组件库,往往都不先在内部抽象数据和逻辑模型,同一个组件库中,不同 ui 组件实现却完全异构。我们错误地执迷于描述组件可以做什么,从大的块里取东西;而不是描述自己长什么样,如何从小的组成大的。Modular and reusable composable 成为永远飘着空中难以落地的口号。
我们发现,antd 中 #issues/5910:对于重型组件的基于功能粒度的拆分能力 引起了广泛热议,甚至 antd 组件 #5910 很难和一个时间处理的 moment utility 剥离。
记得 @徐飞 讲个一个有趣的观察:
前端工程师当看待一个服务接口的时候,如果它提供了几十个参数,并且不分类,不分层,你大概率觉得那是一个很糟糕的东西,绝对不足以成为一个可复用体系的基础。但是当你看见一个几十个 props 也不分类不分组的前端组件,大部分却觉得组件化就是这样,它应该是很好的底层库。
headless ui 和 hooks 落地和开源社区方案
既然已经扯远这么多,那我们就继续聊一下 headless ui(或者在 React scope 下,hooks)的开源和落地情况吧。往大了说,headless ui 涉及到前端团队职能的分化,涉及到前端组件方案的颠覆。往小了说,落地上,我们可以找到一个垂直场景,借助开源社区已有能力的基础,实现 atomic headless ui(开源提供) -> biz headless ui(我们封装) 的设计。
目前社区 headless ui 开源方案主要是更 low level 组件的封装,工程上,我们可以基于良好封装的 low level headless ui 开源库,实现业务关联(往往是数据获取和处理)的 biz headless ui。这个过程,需要后端数据建模抽象,需要前端无缝对接。事实上,前后端一体的渲染层 <-适配层 <-模型层 <-存储层都需要默默提升。一个 LCDP(Low-Code Development Platform)的完成度,依赖每一层的进一步进展。更长远和更理想的这个话题我们姑且不表,先回到前端层面开源社区方案。
- headlessui.com: Completely unstyled, fully accessible UI components, designed to integrate beautifully with Tailwind CSS.
- React Table Library: Build and design powerful datagrid experiences while retaining 100% control over markup and styles.
- 典型的 lightweight table libraries(虽然他的作者认为是界于 heavyweight 和 lightweight 之间,但其实更偏向 lightweight)
- 兼容更现代化的 composition over configuration, customization, extensibility, and SSR 理念。
- 方便进行扩展,方便我们和后端模型相结合
- tanstack.com/table: Headless, type-safe, powerful utilities for complex workflows like Data Management, Data Visualization, Charts, Tables, and UI Components
- downshift-js.com:Primitives to build simple, flexible, WAI-ARI compliant React autocomplete/combobox or select dropdown components.
- radix-ui.com:Unstyled, accessible components for building high‑quality design systems and web apps in React.
- react-spectrum.adobe.com/react-aria:A library of React Hooks that provides accessible UI primitives for your design system.
- 抖音前端 semi.design:
- 国内大厂的一个实现,Semi 从未尝试约束用户,固化所谓的「统一规范」,而是在默认基础上,充分进行模块化解耦,并开放自定义能力,方便用户进行二次裁剪与定制,搭建适用于不同形态产品的前端资产
总结
React hooks 当然有必要分离 ui 和 logic,这是刻在 React hooks 基因里的要义,也是 headless ui 理念的落地基石。我们从一个问题中,以小见大,聊了前端分工,前端边界,前端趋势和发展,但这不是一篇「阿里味」的恢弘叙事长文,在开喷前,不妨静下心来读一下,尝试感受回答中的诚意。
最后,headless ui 不是孤立存在的:向前(卷设计、卷业务),我们可以通过 https://tailwindcss.com/ 这样的平台协助解决样式管理方案、定制样式或设计体系,做到系统对接,体系建设(常见于中小公司);向后(卷后端),我们可以通过统一数据建模,对接渲染抽象能力,更高效地实现前后端一体的 LCDP(Low-Code Development Platform) 页面搭建等需求。卷前端卷我们自己,在 headless ui 基础上,把数据层面(数据驱动)、adapter 适配做到极致,找到前端 ui 和逻辑资产沉淀的空间,我们的想象力、我们的目光本可以更长远。 |
|