没有前言,直入主题
Hooks
自从有了 hooks 就很少用 class 来定义组件了,只要恰当的处理好缓存,用一系列 hooks 来定义组件,又方便又简洁。
这里不铺开讲 hooks 的具体使用,最佳实践就是将业务代码和 UI 实现分离。以前工作的时候习惯将业务代码分离成几个方法放在类组件里,当时觉得挺好,但是渲染代码和业务代码混在一起之后,后期维护就头疼了。
hooks 的好处是可以自定义 hooks,将业务逻辑封装在自定义的 hooks 里,然后将状态和控制器导出给 UI 使用,大大减少了 UI 里的模板代码,并且有效分离了 UI 定义和业务定义、层次分明。
另外有一个初期不太明显的好处,那就是重用业务逻辑,即多个组件重用某个 hook;但是这一般发生在业务推进,发现重复代码的时候。
Headless UI
听说过 headless 浏览器,Headless UI?
现在做的比较成熟的是 TailwindCSS 团队出的 Headless UI,如果你用过 TailwindCSS,肯定对它海量的工具类很有印象——用声明式的方式来写 UI。
当然业界对它也是褒贬不一,缺点谈的比较多的是它在重复发明 CSS。既然已经有了 CSS,为啥还要再来一个 CSS 框架来在 className
里变相写 CSS,即使它只是比普通的 CSS 简短了一些。这里不具体讨论其优劣,合适自己团队就可以了。
Headless UI 相当于提供了 UI 的骨架,类似于原始的 HTML 且不带 style。用 TailwindCSS 提供的工具类来填充的外观,它只提供核心的组件,并暴露一系列状态控制器。
以前用过的 UI 库都是属于 opinionated 类型的,即它有自己的一套外观设计,例如 Bootstrap 时期出现大量千篇一律的网站,长得都一样,一眼就能看出来哪些是用 Bootstrap 堆出来的。现在有了 React、Vue 这样的框架,自己实现 UI 组件变得容易许多,所以设计师们也有更多的岗位机会了 xD。当然也并不是真的就很容易,CSS 的怪癖不少,重载 HTML 原生标签在不同浏览器上还有不同的痛点。
回到 Headless UI,它属于 unopinionated 类型,自身没有任何设计语言,有的只有基础组件和逻辑,没有定义好的外观。如果你的团队使用 Sketch 或者 Figma,采用标准 UX 设计流程:定义网站的基础色板、基础组件库,那么这套框架就是为了这样的需求和开发流程而生的。自定义 TailwindCSS 变量以符合设计的色板,padding 和 margin 都可以自定义,这样保证了网站整体样式的一致。
重复发明 HTML
就像 TailwindCSS 重复发明 CSS 一样,Headless UI 也有重复发明 HTML 的嫌疑,不同之处在于它出现在 React、Vue 时代,它是这些 UI 框架下的产物,相比原生的 HTML,它符合了现在的开发习惯
例子
以官方文档的 Menu 为例:
function MyDropdown() {
return (
<Menu>
<Menu.Button>More</Menu.Button>
<Menu.Items>
<Menu.Item>
{({ active }) => ( // 暴露 active 状态
<a
className={`${active && "bg-blue-500"}`}
href="/account-settings"
>
Account settings
</a>
)}
</Menu.Item>
<Menu.Item disabled>
<span className="opacity-75">Invite a friend (coming soon!)</span>
</Menu.Item>
</Menu.Items>
</Menu>
);
}
Chakra UI
Chakra UI 也是一个比较有意思的 UI 库,我觉得它和 TailwindCSS 很像,可以自定义色板以及 padding、margin 主题样式,它甚至自带了 light/dark 主题控制。
如果你看过它的文档,那么你可能觉得它是属于 opinionated 类型的,因为它有一套默认的 UI 样式;但是如果你看过 Box、Flex 这些组件后,大概又不确定了。它有色卡以及边距配置,有自己的样式,但同时它的组件设计却又提供了太多的灵活性,你会发现几乎每个组件都支持一些 CSS 属性:
<Box
color="gray.500"
fontWeight="semibold"
letterSpacing="wide"
fontSize="xs" // 字体大小,xs 通过全局配置设置
textTransform="uppercase"
ml="2" // margin-left 缩写,数值对应的具体 px 在全局配置
>
{property.beds} beds • {property.baths} baths
</Box>
又在重复...没有,没有重复发明,只是从在 css 文件里写 CSS 变成了写组件的属性,相当于从原生 react 的 style 属性里定义样式变成了直接在组件层级定义样式。当然这些样式在输出时不会跑到 HTML 元素的 style 属性上去,它们会被抽离到 style 标签里,并且元素也会打上一个混淆后的 className 指向自定义的样式。
RadioGroup
表单类的组件是比较难自定义的,为了自定义 radio 组,Chakra UI 提供了 useRadioGroup
以及 useRadio
这两个 hook。前者定义表单字段,后者用于获取样式,使用如下:
// 定义部分
const { getRootProps, getRadioProps } = useRadioGroup({
name: "type", // 字段 name
defaultValue: "react", // 字段默认值
onChange: console.log, // onChange 监听
})
// UI 注入
<RadioCard key={value} {...getRadioProps({ value: 'react' })}>
react
</RadioCard>
// RadioCard.tsx
const { getInputProps, getCheckboxProps } = useRadio(props) // 解析 getRadioProps 返回值
const input = getInputProps() // 获取 input 设置
const checkbox = getCheckboxProps() // 获取 checked 状态设置
return (
<Box as="label">
<input {...input} />
<Box
{...checkbox}
_checked={{ // 设置 checked 状态的样式
bg: "teal.600",
}}
_focus={{ // 设置 focus 样式
boxShadow: "outline",
}}
>
{props.children}
</Box>
</Box>
)
在父组件通过 useRadioGroup
定义整个 radio group 的属性,再在 radio 组件里使用 useRadio
解析 getRadioProps
传递的属性,再获取解析后的 checkbox 和 input 属性,分别赋予不同的组件上。input 对应 <input>
元素,checkbox 对应需要处理 checked
状态的元素
如果打印这两个属性,它们都是样式和 data attribute 的设置信息。例如 input 是把其变为一个宽高都是 1px 的盒子,设置 checked
属性,并且设置 position
为 absolute
让其不占位;checkbox 设置 data-focus
、data-hover
和 data-checked
,分别对应 hover、focus 和 checked 状态样式。
这里 _checked
属性类似于 Headless UI 里 Menu 暴露的 active
变量,让你定义 checked/active 状态下组件的样式。
前者定义表单字段,后者用于获取样式
这里的获取样式并不是指获取 Chakra UI 自带的样式,而是指既能满足组件正常工作,又不影响 UI 设计的工具样式
从 RadioGroup 到 Headless
自定义 RadioGroup 时,框架只提供了两个 hook,样式——你来定,这才是 headless 的核心,他定义了组件的状态迁移逻辑,你只需要接收它返回的属性,并且根据其变换组件样式就可以了。
类似 radio,Chakra UI 也提供了自定义 checkbox 的 hook。这两个组件自定义的方式相比其他实在是风格迥异,不由让人浮想联翩,也算是这个 UI 框架对 Headless 的尝试了。
开源案例
React-Table
React-Table 是一个表格库,在简介里它也标榜了自己是一个 Headless UI 库——不帮你渲染元素,只提供数据处理逻辑。API 基于 hook
visx
visx,airbnb 的图表库,没有皮肤,适合需要高度定制图表的情况。基于 D3,只是 API 并不是基于 hook
后记
没有聊很深,因为这只是“一点”思考。Headless UI 是业务代码和 UI 分离的更进一步,你也可以认为它只是前端代码分层的一个别名。或许这也是未来前端库一个趋势,专注于业务和逻辑,UI 更加 dumb。最终前端不再关注设计,只负责数据处理和状态转换,让 UI 去定义 UI。
完