实战开始 🚀 在 React 和 Vue3 中使用 Headless UI 无头组件库

作者:易师傅github

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

前言

在上篇文章中,咱们偏重点介绍了 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 项目

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

bash 复制代码
pnpm install @headlessui/vue

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

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


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

在 Select.vue 代码中:

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,有的只是简单的交互逻辑;

咱们看下具体效果:

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

是不是 So Easy;

是的,就是 So Easy;

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

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

用 Tailwind css 实现

1. 安装 Tailwind 与初始化

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

npx tailwindcss init -p

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


2. 添加 Tailwind 样式

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. 最终呈现效果:

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

用 scss/less/css 使用

1. 安装与初始化

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

2. 具体实现代码:

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. 最终呈现效果:

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

用 CSS in JS 使用

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

具体代码如下:

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 项目:

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

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

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

新增 Tooltip.tsx 并且修改:

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.具体实现效果如下:

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

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

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

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

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

用 Tailwind css 实现

1. 安装 Tailwind 与初始化

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

npx tailwindcss init -p

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

bash 复制代码
pnpm install classnames

其它与上面几乎一致


2. 添加 Tailwind 样式

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. 最终呈现效果:

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

用 scss/less/css 使用

1.安装与初始化

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

2. 具体实现代码:

TooltipCss.css 代码

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 代码

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. 最终呈现效果:

上述 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

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

具体代码如下:

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 无头组件库等其它部分:

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

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

相关推荐
清灵xmf13 分钟前
深入解析 JavaScript 事件委托
前端·javascript·html·事件委托
小妖别跑42 分钟前
PDA(程序派生地址,Program Derived Address),为什么有这个地址,而不是直接指定地址
前端·智能合约
growdu_real44 分钟前
pandoc自定义过滤器
vue.js
2301_796982141 小时前
网页打开时,下载的文件text/html/重定向类型有什么作用?
前端·html
重生之我在20年代敲代码1 小时前
HTML讲解(二)head部分
前端·笔记·html·web app
天下无贼!1 小时前
2024年最新版TypeScript学习笔记——泛型、接口、枚举、自定义类型等知识点
前端·javascript·vue.js·笔记·学习·typescript·html
计算机学姐2 小时前
基于SpringBoot+Vue的篮球馆会员信息管理系统
java·vue.js·spring boot·后端·mysql·spring·mybatis
小白小白从不日白2 小时前
react 高阶组件
前端·javascript·react.js
程序员大金2 小时前
基于SpringBoot+Vue+MySQL的智能物流管理系统
java·javascript·vue.js·spring boot·后端·mysql·mybatis
Mingyueyixi2 小时前
Flutter Spacer引发的The ParentDataWidget Expanded(flex: 1) 惨案
前端·flutter