无UI 组件Headless框架逻辑原理用法示例详解

目录
  • 概述
  • 精读
  • 总结

概述

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 的组件,提供给各业务场景调用。

讨论地址是:精读《Headless 组件用法与原理》· Issue #444 · dt-fe/weekly

以上就是无UI 组件Headless框架逻辑原理用法示例详解的详细内容,更多关于无UI 组件Headless框架逻辑的资料请关注我们其它相关文章!

赞 (0)

相关推荐

  • 解决selenium+Headless Chrome实现不弹出浏览器自动化登录的问题

    目前由于phantomjs已经不维护了,而新版的Chrome(59+)推出了Headless模式,对爬虫来说尤其是定时任务的爬虫截屏之类的是一大好事. 不过按照网络上的一些方法来写的话,会报下面的错误: 后来经过分析,他们运行python是在mac或者linux下进行的,win下由于高版本的chromedriver只能通过路径进行指定,所以会出现这类找不到驱动程序的错误. 经过比对常识网络上的各种代码,后来得出了win下可顺畅执行的driver的写法如下: from selenium impor

  • 在Java SE上使用Headless模式的超级指南

    这篇文章介绍怎样在标准Java(Java SE,也称作J2SE)平台上用Headless模式. Headless模式是在缺少显示屏.键盘或者鼠标时的系统配置.听起来不可思议,但事实上你可以在这中模式下完成不同的操作,甚至是用图形数据也可以. 哪里才能用到此模式呢?想想你的应用不停的生成一张图片,比如,当用户每次登陆系统是都要生成一张认证图片.当创建图片时,你得应用既不需要显示器也不需要键盘.让我们假设一下,现在你的应用有个主架构或者专有服务器,但这个服务没有显示器,键盘或者鼠标.理想的决定是用环

  • selenium设置浏览器为headless无头模式(Chrome和Firefox)

    新版本的selenium已经明确警告将不支持PhantomJS,建议使用headless的Chrome或FireFox. 两者使用方式非常类似,基本步骤为: 下载驱动 创建选项,设定headless 创建WebDriver,指定驱动位置和选项 对URL发起请求,获得结果,进行解析 Chrome 驱动的下载路径为:https://chromedriver.storage.googleapis.com/index.html 接下来创建选项并设定headless: options = webdrive

  • selenium+headless chrome爬虫的实现示例

    python爬虫写起来非常快,虽然也可以用java,但是没有python来的简洁迅速 selenium在前面总结过,是一个自动化测试库.headless chrome是无界面的浏览器模式,和PHANTOMJS类似.但是PHANTOMJS往往会出现莫名的错误,而且速度没有headless chrome快 from selenium.webdriver.chrome.options import Options global DRIVER chrome_options = Options() chr

  • Python使用selenium + headless chrome获取网页内容的方法示例

    使用python写爬虫时,优选selenium,由于PhantomJS因内部原因已经停止更新,最新版的selenium已经使用headless chrome替换掉了PhantomJS,所以建议将selenium更新到最新版,使用selenium + headless chrome 准备工作: 安装chrome.chrome driver.selenium 一.安装chrome 配置yum下载源,在目录/etc/yum.repos.d/下新建文件google-chrome.repo > cd /e

  • node puppeteer(headless chrome)实现网站登录

    puppeteer简介 puppeteer是Chrome团队开发的一个node库,可以通过api来控制浏览器的行为,比如点击,跳转,刷新,在控制台执行js脚本等等.有了这个神器,写个爬虫,自动签到,网页截图,生成pdf,自动化测试什么的,都不在话下. puppeteer的简单例子 代码来自官网: const puppeteer = require('puppeteer'); (async () => { const browser = await puppeteer.launch();//打开浏

  • 无UI 组件Headless框架逻辑原理用法示例详解

    目录 概述 精读 总结 概述 Headless 组件即无 UI 组件,框架仅提供逻辑,UI 交给业务实现.这样带来的好处是业务有极大的 UI 自定义空间,而对框架来说,只考虑逻辑可以让自己更轻松的覆盖更多场景,满足更多开发者不同的诉求. 我们以 headlessui-tabs 为例看看它的用法,并读一读 源码. headless tabs 最简单的用法如下: import { Tab } from "@headlessui/react"; function MyTabs() { ret

  • epoll封装reactor原理剖析示例详解

    目录 reactor是什么? reactor模型三个重要组件与流程分析 组件 流程 将epoll封装成reactor事件驱动 封装每一个连接sockfd变成ntyevent 封装epfd和ntyevent变成ntyreactor 封装读.写.接收连接等事件对应的操作变成callback 给每个客户端的ntyevent设置属性 将ntyevent加入到epoll中由内核监听 将ntyevent从epoll中去除 读事件回调函数 写事件回调函数 接受新连接事件回调函数 reactor运行 react

  • Vue extends 属性的用法示例详解

    目录 引言 App.vue Son.vue HelloWorld.vue 小结 引言 最近在看抖音——<小山与 bug>,看到一个很神奇的 Vue 继承组件的方法,后来专门去翻了 element 和 iview 的源码,发现这个属性的用法好像在这些框架里还没有用到过,怀着试一试的态度,我就自己搭建了个测试项目,发现其实还是挺好用的,甚至有望代替目前我们前端框架业务代码混入的底层实现.话不多说,直接上代码: App.vue <template> <div> <Son

  • java中Servlet监听器的工作原理及示例详解

    监听器就是一个实现特定接口的普通java程序,这个程序专门用于监听另一个java对象的方法调用或属性改变,当被监听对象发生上述事件后,监听器某个方法将立即被执行. 监听器原理 监听原理 1.存在事件源 2.提供监听器 3.为事件源注册监听器 4.操作事件源,产生事件对象,将事件对象传递给监听器,并且执行监听器相应监听方法 监听器典型案例:监听window窗口的事件监听器 例如:swing开发首先制造Frame**窗体**,窗体本身也是一个显示空间,对窗体提供监听器,监听窗体方法调用或者属性改变:

  • Go语言基础类型及常量用法示例详解

    目录 基础类型 概述 按类别有以下几种数据类型 数值类型 派生类型 变量 概述 单个变量声明 多个变量声明 基础类型 概述 在 Go 编程语言中,数据类型用于声明函数和变量.数据类型的出现时为了把数据分成所需要用大数据的时候才需要申请大内存,这样可以充分的列用内存. 按类别有以下几种数据类型 数值类型 布尔型 bool:布尔型的值只可以是常量 true 或者 false,默认值为 false. 字符串类型 string:编码统一为 UTF-8 编码标识 Unicode 文本,默认值为空字符串.

  • Vue响应式原理的示例详解

    Vue 最独特的特性之一,是非侵入式的响应系统.数据模型仅仅是普通的 JavaScript 对象.而当你修改它们时,视图会进行更新.聊到 Vue 响应式实现原理,众多开发者都知道实现的关键在于利用 Object.defineProperty , 但具体又是如何实现的呢,今天我们来一探究竟. 为了通俗易懂,我们还是从一个小的示例开始: <body> <div id="app"> {{ message }} </div> <script> v

  • JavaScript事件的委托(代理)的用法示例详解

    目录 简介 示例:事件委托 写法1:事件委托 写法2:每个子元素都绑定事件 示例:新增元素 写法1:事件委托 写法2:每个子元素都绑定事件 简介 说明 本文用示例介绍JavaScript中的事件(Event)的委托(代理)的用法. 事件委托简介 事件委托,也叫事件代理,是JavaScript中绑定事件的一种常用技巧.就是将原本需要绑定在子元素的响应事件委托给父元素或更外层元素,让外层元素担当事件监听的职务. 事件代理的原理是DOM元素的事件冒泡. 事件委托的优点 1.节省内存,减少事件的绑定 原

  • Go底层channel实现原理及示例详解

    目录 概念: 使用场景: 底层数据结构: 操作: 创建 发送 接收 关闭 案例分析: 概念: Go中的channel 是一个队列,遵循先进先出的原则,负责协程之间的通信(Go 语言提倡不要通过共享内存来通信,而要通过通信来实现内存共享,CSP(Communicating Sequential Process)并发模型,就是通过 goroutine 和 channel 来实现的) 使用场景: 停止信号监听 定时任务 生产方和消费方解耦 控制并发数 底层数据结构: 通过var声明或者make函数创建

  • Java中枚举类的用法示例详解

    目录 1.引入枚举类 2.实现枚举类 3.枚举类的使用注意事项 4.枚举的常用方法 5.enum细节 1.引入枚举类 Java 枚举是一个特殊的类,一般表示一组常量,比如一年的 4 个季节,一个年的 12 个月份,一个星期的 7 天,方向有东南西北等. Java 枚举类使用 enum 关键字来定义,各个常量使用逗号 , 来分割. 示例: enum Color { RED, GREEN, BLUE; } 2.实现枚举类 接下来我们来看一个一个简单的DEMO示例: /** * java枚举 */ p

  • python中前缀运算符 *和 **的用法示例详解

    这篇主要探讨 ** 和 * 前缀运算符,**在变量之前使用的*and **运算符. 一个星(*):表示接收的参数作为元组来处理 两个星(**):表示接收的参数作为字典来处理 简单示例: >>> numbers = [2, 1, 3, 4, 7] >>> more_numbers = [*numbers, 11, 18] >>> print(*more_numbers, sep=', ') 2, 1, 3, 4, 7, 11, 18 用途: 使用 * 和

随机推荐