实战开始 🚀 在 React 和 Vue3 中使用 Head

作者:易师傅github

声明:本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

前言

在上篇文章中,咱们偏重点介绍了 Headless UI 的概念与优缺点,我相信很多同学已经对 Headless UI 有了最基本的认知;

什么?你不知道?那估计是掘金的推荐算法还没有意识到问题的严重性,赶快移驾至《在 2023 年屌爆了一整年的 shadcn/ui 用的 Headless UI 到底是何方神圣?》查阅即可。

回到正文,咱们这篇文章来着重的讲解一下,Headless UI 实战部分,让你无论是在 Vue3 中还是 React 中,都能用的游刃有余;

a4cb5ea7980714696892ee4e579b3b33.gif

一、Headless UI 的概念和优势

Headless UI 全称是 Headless User Interface (无头用户界面),是一种前端开发的方法论(亦或者是一种设计模式),其核心思想是将 用户界面(UI)的逻辑交互行为视觉表现(CSS 样式) 分离开来;

因为我上一篇文章已经详细的介绍了其概念与优势,所以这里不做赘述;

如果还有同学不懂其概率与优势的,可以移驾至上一篇文章中《传送门》,详细了解;

二、在 Vue3 中使用 Headless UI

安装与使用

1. 快速创建一个 vue3 项目

1
2
3
4
5
6
7
8
9
10
11
bash复制代码# npm 7+, extra double-dash is needed:
npm create vite@latest my-vue-app -- --template vue

# yarn
yarn create vite my-vue-app --template vue

# pnpm
pnpm create vite my-vue-app --template vue

# bun
bun create vite my-vue-app --template vue

因为市面上 Headless UI 无头组件库较多,为了方便大家上手,咱们主要都以 Tailwind Labs 团队开源的 headlessui 无头组件库为基本依赖;


2. 安装 @headlessui/vue

1
bash复制代码pnpm install @headlessui/vue

根据官网所示,一共提供了 10 个无头组件,咱们以其中具有代表性的 Listbox (Select) 为例;

实现一个高度自定义符合 UI 设计师的一个 Select 组件


3. 先实现最基本样式组件

在 Select.vue 代码中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
ts复制代码<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,有的只是简单的交互逻辑;

咱们看下具体效果:

1.gif

到这里,无头组件库 headlessui 的基本安装与使用就已经完成;

是不是 So Easy;

是的,就是 So Easy;

那么,接下来我们就要开始自定义的按照设计稿给的样式来处理咯;

因为 CSS 样式实现也是多种方式,所以咱们雨露均沾,都一一的讲解一下。

用 Tailwind css 实现

1. 安装 Tailwind 与初始化

1
2
3
bash复制代码pnpm install -D tailwindcss@latest postcss@latest autoprefixer@latest

npx tailwindcss init -p

再详细的不是本文核心,就不做拓展讲解了


2. 添加 Tailwind 样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
ts复制代码<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. 最终呈现效果:

2.gif

上述 Tailwind 样式例子的源码地址:链接

用 scss/less/css 使用

1. 安装与初始化

可自行搜索了解安装,scss 与 less 安装教程现在都烂大街了;

2. 具体实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
ts复制代码<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. 最终呈现效果:

2.gif

上述scss/less/css样式例子的源码地址:链接

用 CSS in JS 使用

Vue 3 中,可以通过多种方式使用 CSS in JS。其中一种方法是使用<style>组件的特殊 v-bind语法来动态绑定样式对象。

具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
ts复制代码<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 项目:

1
2
3
4
5
6
7
8
9
10
11
bash复制代码# npm 7+, extra double-dash is needed:
npm create vite@latest my-vue-app -- --template vue

# yarn
yarn create vite my-vue-app --template vue

# pnpm
pnpm create vite my-vue-app --template vue

# bun
bun 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

1
bash复制代码pnpm install @radix-ui/react-tooltip

3. 实现最基本样式组件:

新增 Tooltip.tsx 并且修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ts复制代码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.具体实现效果如下:

因为没有写样式,所以都是浏览器默认自带的样式

3.gif

Tip:其中的 @radix-ui/react-icons 是使用到 radix-ui 提供的 icon,所以大家可自行选择,是否使用,如需使用,自行 pnpm install @radix-ui/react-icons 下载即可。

到这里 radix-ui 的基本使用,就结束了,其实也是很简单;

但是很明显,咱们想要的是更加完美的一个 Tppltip 组件,所以咱们必须得再加以点缀(Style),实现属于自己的组件。

上述基本组件例子的源码地址:链接

用 Tailwind css 实现

1. 安装 Tailwind 与初始化

1
2
3
bash复制代码pnpm install -D tailwindcss@latest postcss@latest autoprefixer@latest

npx tailwindcss init -p

使用 React 的同学,应该都知道,需要单独的安装一个 classnames 插件

1
bash复制代码pnpm install classnames

其它与上面几乎一致


2. 添加 Tailwind 样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
tsx复制代码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. 最终呈现效果:

5.gif

上述 Tailwind 样式源码地址:链接

用 scss/less/css 使用

1.安装与初始化

scss 与 less 的安装与使用不做赘述

2. 具体实现代码:

TooltipCss.css 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ts复制代码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. 最终呈现效果:

5.gif

上述 scss/less/css 样式例子的源码地址:链接

用 CSS in JS 使用

在 React 中使用 CSS in JS,一般有多种方式:

  1. 内联样式(Inline Styles):直接在JSX元素上应用样式对象。
  2. 使用styled-components库:创建可以像组件一样使用的样式化组件。
  3. 使用emotion或radium库:这些库提供了类似styled-components的功能,同时也可以进行样式组合和优化。
  4. 使用CSS模块:将CSS提取为模块,可以避免CSS选择器冲突。
  5. 使用 @stitches/react 库

当然,这里不可能全部讲解到,主要用到比较常见的Radium 库方式来进行举例

安装 Radium

1
bash复制代码pnpm i -D radium @types/radium

具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
tsx复制代码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

适合 Vue

  • headlessui:链接
  • radix-vue:链接
  • ark:链接
  • vue 的话目前暂时只找到这几个,欢迎大家补充

如果还有比较好一点组件库,也欢迎大家补充 ~

总结

这篇文章主要给大家介绍了 Headless UI 在项目中的实战部分,如果有帮到你,那就来个一键三连吧,感谢;

下面咱们将开始如何动手实现一个 Headless UI 无头组件库等其它部分:

Headless UI (1).png

感谢大家的支持,码字实在不易,其中如若有错误,望指出,如果您觉得文章不错,记得 点赞关注加收藏 哦 ~

关注我,带您一起搞前端 ~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%