<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
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框架邏輯的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45