在 React 和 Vue3 中使用 Headless UI 无头组件库
/ 15 min read
Table of Contents
前言
在上篇文章中,咱们偏重点介绍了 Headless UI 的概念与优缺点,我相信很多同学已经对 Headless UI 有了最基本的认知;
什么?你不知道?那估计是掘金的推荐算法还没有意识到问题的严重性,赶快移驾至《在 2023 年屌爆了一整年的 shadcn/ui 用的 Headless UI 到底是何方神圣?》查阅即可。
回到正文,咱们这篇文章来着重的讲解一下,Headless UI 实战部分,让你无论是在 Vue3 中还是 React 中,都能用的游刃有余;
一、Headless UI 的概念和优势
Headless UI全称是 Headless User Interface (无头用户界面),是一种前端开发的方法论(亦或者是一种设计模式),其核心思想是将 用户界面(UI)的逻辑 和 交互行为 与 视觉表现(CSS 样式) 分离开来;
因为我上一篇文章已经详细的介绍了其概念与优势,所以这里不做赘述;
如果还有同学不懂其概率与优势的,可以移驾至上一篇文章中《传送门》,详细了解;
二、在 Vue3 中使用 Headless UI
安装与使用
1. 快速创建一个 vue3 项目
# npm 7+, extra double-dash is needed:npm create vite@latest my-vue-app -- --template vue
# yarnyarn create vite my-vue-app --template vue
# pnpmpnpm create vite my-vue-app --template vue
# bunbun create vite my-vue-app --template vue因为市面上 Headless UI 无头组件库较多,为了方便大家上手,咱们主要都以 Tailwind Labs 团队开源的 headlessui 无头组件库为基本依赖;
2. 安装 @headlessui/vue
pnpm install @headlessui/vue根据官网所示,一共提供了 10 个无头组件,咱们以其中具有代表性的 Listbox (Select) 为例;
实现一个高度自定义且符合 UI 设计师的一个 Select 组件
3. 先实现最基本样式组件
在 Select.vue 代码中:
<template> <Listbox v-model="selectedRegion"> <ListboxButton>{{ selectedRegion?.name || '请选择' }}</ListboxButton> <ListboxOptions> <ListboxOption v-for="item in regions" :key="item.id" :value="item" :disabled="item.unavailable" > {{ item.name }} </ListboxOption> </ListboxOptions> </Listbox></template>
<script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue'
const regions = [ { id: 1, name: '北京', unavailable: false }, { id: 2, name: '上海', unavailable: false }, { id: 3, name: '广州', unavailable: false }, { id: 4, name: '深圳', unavailable: true }, { id: 5, name: '香港', unavailable: false }, { id: 5, name: '澳门', unavailable: false }, ] const selectedRegion = ref()</script>代码其实很简单,渲染的样式的完全就是浏览器自带的,没有 UI,有的只是简单的交互逻辑;
咱们看下具体效果:
到这里,无头组件库 headlessui 的基本安装与使用就已经完成;
是不是 So Easy;
是的,就是 So Easy;
那么,接下来我们就要开始自定义的按照设计稿给的样式来处理咯;
因为 CSS 样式实现也是多种方式,所以咱们雨露均沾,都一一的讲解一下。
用 Tailwind css 实现
1. 安装 Tailwind 与初始化
pnpm install -D tailwindcss@latest postcss@latest autoprefixer@latest
npx tailwindcss init -p再详细的不是本文核心,就不做拓展讲解了
2. 添加 Tailwind 样式
<template> <Listbox v-model="selectedRegion"> <ListboxButton class="w-[230px] h-[44px] text-[#999] outline-[#fff] flex items-center justify-between text-16 text-left bg-[#fff] px-[20px] rounded-[4px] border-[1px] border-solid border-[#e6e6e6]"> {{ selectedRegion?.name || '请选择' }}
<i class="block w-[16px] h-[16px] bg-[url(~/assets/pull.png)] bg-no-repeat bg-cover"></i> </ListboxButton> <ListboxOptions class="w-[230px] text-16 text-left bg-[#fff] rounded-[4px] border-[1px] shadow-[0px_3px_16px_2px_#e6e6e6]"> <ListboxOption v-for="item in regions" :key="item.id" :value="item" :disabled="item.unavailable" as="template" v-slot="{ active, selected }" > <li :class="{ 'bg-[#f7f8fa] text-[#006aff]': active, 'bg-white text-[#333333]': !active, }" class="h-[44px] leading-[44px] pl-[20px] cursor-pointer" > <CheckIcon v-show="selected" />
{{ item.name }} </li> </ListboxOption> </ListboxOptions> </Listbox></template>
<script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue'
const regions = [ { id: 1, name: '北京', unavailable: false }, { id: 2, name: '上海', unavailable: false }, { id: 3, name: '广州', unavailable: false }, { id: 4, name: '深圳', unavailable: false }, { id: 5, name: '香港', unavailable: false }, { id: 5, name: '澳门', unavailable: false }, ] const selectedRegion = ref()</script>3. 最终呈现效果:
上述 Tailwind 样式例子的源码地址:链接
用 scss/less/css 使用
1. 安装与初始化
可自行搜索了解安装,scss 与 less 安装教程现在都烂大街了;
2. 具体实现代码:
<template> <Listbox v-model="selectedRegion"> <ListboxButton class="box-button"> {{ selectedRegion?.name || '请选择' }}
<i class="box-button-icon"></i> </ListboxButton> <ListboxOptions class="list"> <ListboxOption v-for="item in regions" :key="item.id" :value="item" :disabled="item.unavailable" as="template" v-slot="{ active, selected }" > <li :class="{ 'bg-[#f7f8fa] text-[#006aff]': active, 'bg-white text-[#333333]': !active, }" class="list-item" > {{ item.name }} </li> </ListboxOption> </ListboxOptions> </Listbox></template>
<script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue'
const regions = [ { id: 1, name: '北京', unavailable: false }, { id: 2, name: '上海', unavailable: false }, { id: 3, name: '广州', unavailable: false }, { id: 4, name: '深圳', unavailable: false }, { id: 5, name: '香港', unavailable: false }, { id: 5, name: '澳门', unavailable: false }, ] const selectedRegion = ref()</script>
<style scoped>
.box-button { width: 230px; height: 44px; color: #999; font-size: 16px; outline: #fff; display: flex; align-items: center; justify-content: space-between; text-align: center; background-color: #fff; padding: 0 20px; border-radius: 4px; border: 1px solid #e6e6e6;}
.box-button-icon { display: block; width: 16px; height: 16px; background: url(~/assets/pull.png); background-repeat: no-repeat; background-size: cover;}
.list { width: 230px; font-size: 16px; text-align: left; background-color: #fff; border-radius: 4px; border: 1px solid #e6e6e6; box-shadow: 0 3px 16px 2px #e6e6e6;}
.list-item { height: 44px; line-height: 44px; padding-left: 20px; cursor: pointer;}</style>3. 最终呈现效果:
上述scss/less/css样式例子的源码地址:链接
用 CSS in JS 使用
在 Vue 3 中,可以通过多种方式使用 CSS in JS。其中一种方法是使用 <style> 组件的特殊 v-bind 语法来动态绑定样式对象。
具体代码如下:
<template> <ListboxButton :style="boxButton"> {{ selectedRegion?.name || '请选择' }} <i :style="boxButtonIcon"></i> </ListboxButton>
// do something</template>
<script setup>import { reactive } from 'vue';
const boxButton = reactive({ width: '230px', height: '44px', color: '#999', fontSize: '16px', outline: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'space-between', textAlign: 'center', backgroundColor: '#fff', padding: '0 20px', borderRadius: '4px', border: '1px solid #e6e6e6',
})
const boxButtonIcon = reactive({ display: 'block', width: '16px', height: '16px', background: 'url(~/assets/pull.png)', backgroundRepeat: 'no-repeat', backgroundSize: 'cover', })
const list = reactive({ width: '230px', fontSize: '16px', textAlign: 'left', backgroundColor: '#fff', borderRadius: '4px', border: '1px solid #e6e6e6', boxShadow: '0 3px 16px 2px #e6e6e6', })
const listItem = reactive({ height: '44px', lineHeight: '44px', paddingLeft: '20px', cursor: 'pointer', })</script>
<style scoped>/* 这里可以编写其他的 CSS 规则 */</style>最终呈现效果也是与上面一致。
上述CSS in JS样式例子的源码地址:链接
到这里咱们已经会在 Vue3 项目中使用 Headless UI 组件库了,但是值得注意的是,上面只是使用了无头组件库headlessui来举例说明,开源仓库现不仅只有这一个,例如 radix-vue 也是一个不错的选择,当然还有许多,这里就不赘述了,大家可自行了解;
三、在 React 中使用 Headless UI
安装与使用
1. 快速创建一个 React 项目:
# npm 7+, extra double-dash is needed:npm create vite@latest my-vue-app -- --template vue
# yarnyarn create vite my-vue-app --template vue
# pnpmpnpm create vite my-vue-app --template vue
# bunbun create vite my-vue-app --template vue由于 React 的无头组件库甚多,且在 2023 年屌爆了一整年的 shadcn/ui 就是基于 radix-ui 无头组件库来实现,所以咱们以 radix-ui 作为基本依赖;
2. 安装 radix-ui:
我们以实现一个 tooltip 组件为例,来实现一个自定义样式的组件
因为 radix-ui 每个组件都要单独安装,所以咱们单独安装 @radix-ui/react-tooltip
pnpm install @radix-ui/react-tooltip3. 实现最基本样式组件:
新增 Tooltip.tsx 并且修改:
import * as Tooltip from '@radix-ui/react-tooltip';import { InfoCircledIcon } from '@radix-ui/react-icons';
const TooltipDemo = () => { return ( <Tooltip.Provider> <Tooltip.Root> <Tooltip.Trigger asChild> <button> <InfoCircledIcon /> </button> </Tooltip.Trigger> <Tooltip.Portal> <Tooltip.Content sideOffset={5}> 解释说明文案 <Tooltip.Arrow /> </Tooltip.Content> </Tooltip.Portal> </Tooltip.Root> </Tooltip.Provider> );};
export default TooltipDemo;4.具体实现效果如下:
因为没有写样式,所以都是浏览器默认自带的样式
Tip:其中的
@radix-ui/react-icons是使用到radix-ui提供的icon,所以大家可自行选择,是否使用,如需使用,自行pnpm install @radix-ui/react-icons下载即可。
到这里 radix-ui 的基本使用,就结束了,其实也是很简单;
但是很明显,咱们想要的是更加完美的一个 Tppltip 组件,所以咱们必须得再加以点缀(Style),实现属于自己的组件。
上述基本组件例子的源码地址:链接
用 Tailwind css 实现
1. 安装 Tailwind 与初始化
pnpm install -D tailwindcss@latest postcss@latest autoprefixer@latest
npx tailwindcss init -p使用 React 的同学,应该都知道,需要单独的安装一个 classnames 插件
pnpm install classnames其它与上面几乎一致
2. 添加 Tailwind 样式
import * as Tooltip from '@radix-ui/react-tooltip';import { InfoCircledIcon } from '@radix-ui/react-icons';
const TooltipDemo = () => { return ( <Tooltip.Provider> <Tooltip.Root delayDuration={0}> <Tooltip.Trigger asChild> <button className="text-violet11 shadow-blackA4 hover:bg-violet3 inline-flex h-[35px] w-[35px] items-center justify-center rounded-full bg-white outline-none hover:shadow-[0_2px_10px]"> <InfoCircledIcon /> </button> </Tooltip.Trigger> <Tooltip.Portal> <Tooltip.Content className="bg-[#000] text-white p-2 rounded-md text-xs" sideOffset={5}> 这是一段鼠标悬浮后的解释说明文案 <Tooltip.Arrow className="text-[#000]" /> </Tooltip.Content> </Tooltip.Portal> </Tooltip.Root> </Tooltip.Provider> );};
export default TooltipDemo;3. 最终呈现效果:
上述 Tailwind 样式源码地址:链接
用 scss/less/css 使用
1.安装与初始化
scss 与 less 的安装与使用不做赘述
2. 具体实现代码:
TooltipCss.css 代码
.IconButton { border-radius: 50%; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; height: 35px; width: 35px;}.IconButton:hover { box-shadow: 0 2px 10px #d9d9d9;}
.TooltipContent { background-color: #000; color: #fff; padding: 2px 6px; border-radius: 4px; font-size: 13px;}
.TooltipArrow { color: #000;}TooltipCss.tsx 代码
const TooltipDemo = () => { return ( <Tooltip.Provider> <Tooltip.Root delayDuration={0}> <Tooltip.Trigger asChild> <button className="IconButton"> <InfoCircledIcon /> </button> </Tooltip.Trigger> <Tooltip.Portal> <Tooltip.Content className="TooltipContent" sideOffset={5}> 这是一段鼠标悬浮后的解释说明文案 <Tooltip.Arrow className="TooltipArrow" /> </Tooltip.Content> </Tooltip.Portal> </Tooltip.Root> </Tooltip.Provider> );};3. 最终呈现效果:
上述 scss/less/css 样式例子的源码地址:链接
用 CSS in JS 使用
在 React 中使用 CSS in JS,一般有多种方式:
内联样式(Inline Styles):直接在JSX元素上应用样式对象。使用styled-components库:创建可以像组件一样使用的样式化组件。使用emotion或radium库:这些库提供了类似styled-components的功能,同时也可以进行样式组合和优化。使用CSS模块:将CSS提取为模块,可以避免CSS选择器冲突。使用 @stitches/react 库
当然,这里不可能全部讲解到,主要用到比较常见的Radium 库方式来进行举例
安装 Radium
pnpm i -D radium @types/radium具体代码如下:
import * as Tooltip from '@radix-ui/react-tooltip';import { InfoCircledIcon } from '@radix-ui/react-icons';import Radium from 'radium';
const TooltipDemo = () => { return ( <Tooltip.Provider> <Tooltip.Root delayDuration={0}> <Tooltip.Trigger asChild> <button style={IconButtonStyles}> <InfoCircledIcon /> </button> </Tooltip.Trigger> <Tooltip.Portal> <Tooltip.Content style={TooltipContentStyles} sideOffset={5}> 这是一段鼠标悬浮后的解释说明文案 <Tooltip.Arrow style={TooltipArrowStyles}/> </Tooltip.Content> </Tooltip.Portal> </Tooltip.Root> </Tooltip.Provider> );};
const IconButtonStyles = { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 35, height: 35, borderRadius: '50%', outline: 'none', '&:hover': { boxShadow: '0 2px 10px #d9d9d9', },}
const TooltipContentStyles = { backgroundColor: '#000', color: 'white', padding: '2px 6px', borderRadius: '4px', fontSize: '13px',}
const TooltipArrowStyles = { color: '#000',}
export default Radium(TooltipDemo);最终实现的效果与上面也几乎一致
四、比较 React 和 Vue 中 Headless UI 的异同
根据上述的实际使用,我们会发现其实无论是在 React 或 Vue 中,使用的 Headless UI 组件库,其实大同小异,都是要自定义样式、而且自定义样式的写法也几乎一致。
可能最大的差一点,也就只有 React 和 Vue 编码方式语法糖的差异了,这个就得考验大家的基本功底了。
还有较大的差异点,就是第三方无头组件库的使用方式不同,这个主要取决于第三方组件库了。
五、其它 Headless UI 库
就目前市面上的,所有开源的无头组件库,几乎大部分都只支持 React,这个就不做解释了,懂的都懂。
作者在这里就简要的收集了一些市面上的无头组件;
适合 React
- headlessui:链接
- Radix UI:链接
- Reach UI:链接
- downshift:链接
- Base UI:链接
- headless-datepicker:链接
- ark:链接
- ariakit:链接
- …等等…
- 这里主要列举一些具有代表性的库
适合 Vue
如果还有比较好一点组件库,也欢迎大家补充 ~
总结
这篇文章主要给大家介绍了 Headless UI 在项目中的实战部分,如果有帮到你,那就来个一键三连吧,感谢;
下面咱们将开始如何动手实现一个 Headless UI 无头组件库等其它部分:
感谢大家的支持,码字实在不易,其中如若有错误,望指出,如果您觉得文章不错,记得 点赞关注加收藏 哦 ~
关注我,带您一起搞前端 ~