实战开始 🚀 在 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 无头组件库等其它部分:

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

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

相关推荐
excel3 小时前
为什么需要构建工具(Webpack / Vite 的本质)
前端
lang201509283 小时前
Java SAX 流式解析全解:从原理到 EasyExcel 实战
java·前端·javascript
Rain5093 小时前
2.4. PostgreSQL 数据库连接与实战指南
前端·数据库·人工智能·后端·postgresql·数据分析
console.log('npc')3 小时前
Codex 桌面端接入 Headroom 压缩代理完整教程
前端·vscode
独泪了无痕4 小时前
Vue集成uuid生成唯一标识实践指南
前端·vue.js
yuanyxh12 小时前
Mac 软件推荐
前端·javascript·程序员
万少12 小时前
AtomCode开发微信小程序《谁去呀》 全流程
前端·javascript·后端
某人辛木12 小时前
Web自动化测试
前端·python·pycharm·pytest
Kagol13 小时前
Superpowers GSD gstack AgentSkills深度测评
前端·人工智能
excel14 小时前
JavaScript 字符串与模板字面量:从表象到本质理解
前端