关于javascript:精读Headless-组件用法与原理

29次阅读

共计 4493 个字符,预计需要花费 12 分钟才能阅读完成。

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 切换是否为手动按 EnterSpace 键:

<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 后,TabTab.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;
}

所有子组件如 TabTab.PanelTab.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 内已知该 TabindexselectedIndex,那么给用户提供一个组合变量 selected 就可能比别离提供这两个变量更不便。

总结

咱们总结一下 Headless 的设计与应用思路。

作为框架作者,首先要剖析这个组件的业务性能,并形象出应该拆分为哪些 UI 模块,并利用 RenderProps 将这些 UI 模块以 UI 无关形式提供,并精心设计每个 UI 模块提供的状态。

作为使用者,理解这些组件别离反对哪些模块,各模块提供了哪些状态,并依据这些状态实现对应的 UI 组件,响应这些状态的变动。因为最简单的状态逻辑曾经被框架内置,所以对于 UI 状态多样的业务甚至能够每个组件重写一遍 UI 款式,对于款式稳固的场景,业务也能够依照 Headless + UI 作为整体封装出蕴含 UI 的组件,提供给各业务场景调用。

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

如果你想参加探讨,请 点击这里,每周都有新的主题,周末或周一公布。前端精读 – 帮你筛选靠谱的内容。

关注 前端精读微信公众号

<img width=200 src=”https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg”>

版权申明:自在转载 - 非商用 - 非衍生 - 放弃署名(创意共享 3.0 许可证)

正文完
 0