泰裤辣 🚀 原�实现一个无头组件比传统组件简�辣么多��

2,766 阅读14分钟

作者:易师傅 �github

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

�言

承接上文,我们已�知�了无头组件库 Headless UI 是什么,以�有什么样的作用,包括怎么去实现一个最基本 Headless UI 无头组件库框架;

如果你还�知�也没关系,看看《Headless UI》这个�费的专�就行了;

正所谓å??而论é?“ã€?夸夸其谈ã€?纸上谈兵的人比比皆是,我们è¦?å?šçš„ä¸?ä»…ä»…è¦?能论其é?“ã€?夸其谈,也è¦?行胜于言,更è¦?实战,真正å?šåˆ°çŸ¥è¡Œå?ˆä¸€ã€‚

那么接下�,就�到了第二篇的敲代�实战环节 ~

一�分�对比

1)分�传统 UI 组件库的 Popover 组件需�什么?

我相信大家多多少少都使用过 Ant-Design�Element�Vant等等传统 UI 组件库中的其中一�,而且几乎都使用过了其中的 Popover 组件 功能,那么我们分�一下他们的共�点:

1.1 传统 UI 组件库的 Popover 组件基本使用:

1. Element:

image.png

2. Ant-Design:

image.png

1.2 其它更多功能:

  • 触å?‘æ–¹å¼?:点击ã€?è?šç„¦ã€?悬浮等等
  • ä½?置显示
  • 自定义内容

我相信这是大部分 Popover 组件所需的功能点,如果你能全部实现,那基本就算是一个�格的组件了。

我们�以根�此�抽离其中的样�,�实现一个仅需�交互逻辑的无头 Popover 组件;

2)分�一个无头组件库 Popover 组件需�什么?

其实,一个�有交互逻辑的无头 Popover 组件与传统组件的功能是大差�差的,一些基本使用等等都是必须�具备的;

�是在样�这�我们�需�下更多的功夫;

那么我们实现的功能点就主�包括以下:

  • 基本使用
  • 触å?‘æ–¹å¼?
  • ä½?置显示
  • 自定义内容

分�完之�,那我们开始实战 ~

二�实战之「基本使用�

因为我们在上一篇文章中已��建好了基本项目框架的架�,所以我们基于此�进行开���

1)创建

# �到 package/vue 目录下
cd package/vue

# 新建 Popover 目录
mkdir src/Popover

2)实现基本 PopoverRoot 根组件

2.1. 基本�置

新建 PopoverRoot.vue 文件


<script lang="ts">
import type { Ref } from 'vue'
import { createContext } from '@yi-ui/shared'

// 暴露的三个�数
export interface PopoverRootProps {
  /**
   * 打开状�,当它最�被渲染时。当您�需�控制其打开状�时使用。
   */
  defaultOpen?: boolean
  /**
   * 控制当�组件的打开状�
   */
  open?: boolean
  /**
   * popover的模�。当设置为true时,将�用与外部元素的交互,并且�有弹出�的内容对�幕阅读器��。
   *
   * @defaultValue false
   */
  modal?: boolean
}

// 暴露的事件
export type PopoverRootEmits = {
  /**
   * 打开状�事件回调
   */
  'update:open': [value: boolean]
}

// 组件上下文传递时所需的�数
export interface PopoverRootContext {
  triggerElement: Ref<HTMLElement | undefined>
  contentId: string
  open: Ref<boolean>
  modal: Ref<boolean>
  onOpenChange(value: boolean): void
  onOpenToggle(): void
  hasCustomAnchor: Ref<boolean>
}

// 传递组件上下文
export const [injectPopoverRootContext, providePopoverRootContext]
  = createContext<PopoverRootContext>('PopoverRoot')
</script>

这段代�很简,我相信大家基本都看得懂,�是其中有一个 createContext函数 的出处需�解释一下;

createContext函数是一个写在 yi-ui/shared �包中的公共方法;

主�作用就是让整个组件上下文建立�系,方便整个父�组件的�调;

2.2 核心代�

�规矩,先看代�

<script setup lang="ts">
import { ref, toRefs } from 'vue'
import { useVModel } from '@vueuse/core'
import { PopperRoot } from '../Popper'

// 设置组件的默认属性值
const props = withDefaults(defineProps<PopoverRootProps>(), {
  defaultOpen: false,
  open: undefined,
  modal: false,
})
// 事件
const emit = defineEmits<PopoverRootEmits>()
const { modal } = toRefs(props)

// vueuse 的一个��绑定 hook
// vue 3.4+ �以使用 defineModel:https://cn.vuejs.org/api/sfc-script-setup.html#definemodel 
const open = useVModel(props, 'open', emit, {
  defaultValue: props.defaultOpen,
  passive: (props.open === undefined) as false,
}) as Ref<boolean>

const triggerElement = ref<HTMLElement>()
const hasCustomAnchor = ref(false)

// 暴露给�组件的属性与方法
providePopoverRootContext({
  contentId: '',
  modal,
  open,
  onOpenChange: (value) => {
    open.value = value
  },
  onOpenToggle: () => {
    open.value = !open.value
  },
  triggerElement,
  hasCustomAnchor,
})
</script>

<template>
  <PopperRoot>
    <slot />
  </PopperRoot>
</template>

其实整个代�也是很简�的,大家几乎都能看的懂,语义化是很明显的;

其中�能有疑问的地方,应该就�有 PopperRoot 了;

PopperRoot 是一个基于 @floating-ui/vue 库实现的基本组件;

主è¦?作用是为浮动元素æ??供锚点定ä½?,并且将其ä½?置定ä½?在当å‰?å?‚考元素æ—?边的库;

几乎�大多数的组件库都有使用到类似的库,这里就�展开讲了,大家明白就行了 ~

到这里这个最基本的 PopoverRoot 根组件就实现完毕了,是ä¸?是很简å?•å‘¢ï¼Œå¯¹çš„,就是这么简å?•ï¼Œå¦‚果你还没看懂,建议多看几é??哦~

2.3 导出

新建 index.ts

export {
  default as PopoverRoot,
  type PopoverRootProps,
  type PopoverRootEmits,
} from './PopoverRoot.vue'

到这基本就完�了;

那么既然开�完毕了,那么咱们下一步就是开始去调试了

2.4 playground 调试

基本使用:

进入目录:

cd playground/vue3

新增 Popover.vue 组件:

<template>
    <PopoverRoot :default-open="true">
        <div>Your popover content here</div>
    </PopoverRoot>
</template>

<script setup lang="ts">
import { PopoverRoot } from '@yi-ui/vue';

</script>

<style scoped>
</style>

渲染效果:

image.png


结��数使用:

<template>
    <PopoverRoot v-model:open="toggleState" >
        <!-- PopoverTrigger -->
        <button @click="handleVisible">点击显示/��</button>
        
        <!-- PopoverContent -->
        <ul v-if="toggleState">
            <li>popover content: 11111</li>
            <li>popover content: 22222</li>
            <li>popover content: 33333</li>
            <li>popover content: 44444</li>
            <li>popover content: 55555</li>
        </ul>
    </PopoverRoot>
</template>

<script setup lang="ts">
import { PopoverRoot } from '@yi-ui/vue';
import { ref } from 'vue';

const toggleState = ref(false);

const handleVisible = () => {
    toggleState.value = !toggleState.value;
};

</script>

我们能看到点击按钮会显示与��:

7.gif

其实代�中的实现与现在的 PopoverRoot 根组件 关�性�是很大;

所以接下�我们��的就是把这个 PopoverTrigger 组件 和 PopoverContent 组件 嵌入到 PopoverRoot 根组件中去,这样使用的时候�会事�功�;

3)实现 PopoverTrigger �组件

3.1 功能介�

PopoverTrigger �组件的功能其实很简�,那就是切�弹出 Popover 的一个触�器功能,默认情况下,它将 PopoverContent �组件(也就是主�的内容部分)定�在当�触�器上。

3.2 核心代�

<script lang="ts">
import type { PrimitiveProps } from '@/Primitive'
import { useForwardExpose } from '@yi-ui/shared'

export interface PopoverTriggerProps extends PrimitiveProps {}
</script>

<script setup lang="ts">
import { onMounted } from 'vue'
import { injectPopoverRootContext } from './PopoverRoot.vue'
import { Primitive } from '@/Primitive'
import { PopperAnchor } from '@/Popper'

const props = withDefaults(defineProps<PopoverTriggerProps>(), {
  as: 'button',
})

const rootContext = injectPopoverRootContext()

const { forwardRef, currentElement: triggerElement } = useForwardExpose()

onMounted(() => {
  rootContext.triggerElement.value = triggerElement.value
})
</script>

<template>
  <component
    :is="rootContext.hasCustomAnchor.value ? Primitive : PopperAnchor"
    as-child
  >
    <Primitive
      :ref="forwardRef"
      :type="as === 'button' ? 'button' : undefined"
      aria-haspopup="dialog"
      :aria-expanded="rootContext.open.value"
      :aria-controls="rootContext.contentId"
      :data-state="rootContext.open.value ? 'open' : 'closed'"
      :as="as"
      :as-child="props.asChild"
      @click="rootContext.onOpenToggle"
    >
      <slot />
    </Primitive>
  </component>
</template>

其实它的代�就�需�这么多,我们简�解释一下其中的核心函数:

useForwardExpose:使其�以处�自己的 Ref;

Primitive:一个通用的基本元素组件;

PopperAnchor:瞄点定�元素;

3.3 导出

export {
  default as PopoverTrigger,
  type PopoverTriggerProps,
} from './PopoverTrigger.vue'

3.4 playground 调试

编辑 Popover.vue 组件:

<script setup lang="ts">
import { PopoverRoot, PopoverTrigger } from '@yi-ui/vue'
import { ref } from 'vue'

const toggleState = ref(false)

// function handleVisible() {
//   toggleState.value = !toggleState.value
// }
</script>

<template>
  <PopoverRoot v-model:open="toggleState">
    <!-- PopoverTrigger -->
    <!-- <button @click="handleVisible">点击显示/��</button> -->
    <PopoverTrigger>
      点击显示/��
    </PopoverTrigger>

    <!-- PopoverContent -->
    <ul v-if="toggleState">
      <li>popover content: 11111</li>
      <li>popover content: 22222</li>
      <li>popover content: 33333</li>
      <li>popover content: 44444</li>
      <li>popover content: 55555</li>
    </ul>
  </PopoverRoot>
</template>

根�与上� PopoverRoot根组件 调试的对比,我们很明显的少了 handleVisible 函数,这是因为我们已�在 PopoverTrigger �组件的 onOpenToggle 中实现了。

7.gif

好了,到这里我们已�有了触�器 PopoverTrigger �组件 ,接下�就是�实现 PopoverContent �组件主�内容了。

4)实现 PopoverContent �组件

4.1 功能介�

PopoverContent �组件 主�就是在 PopoverTrigger �组件 触�器在弹出窗�时的组件内容。

4.2 核心代�

�规矩,先看代�

<script lang="ts">
export interface PopoverContentProps {
  forceMount?: boolean
}
</script>

<script setup lang="ts">
import { injectPopoverRootContext } from './PopoverRoot.vue'
import { useForwardExpose } from '@yi-ui/shared'
import { Presence } from '@/Presence'

const rootContext = injectPopoverRootContext()

const { forwardRef } = useForwardExpose()

</script>

<template>
  <Presence :ref="forwardRef" :present="rootContext.open.value">
    <slot />
  </Presence>
</template>

其实一看,PopoverContent �组件 实现起�是�是贼简�;

是的,的确贼简�;

我相信上�的代�,大家也就对 Presence 会比较陌生,咱们简�介�下:

主�作用就是有�件的显示和��当�的组件,就类似于 vue 的 v-if 功能。

4.3 导出

新建 index.ts

export {
  default as PopoverContent,
  type PopoverContentProps,
} from './PopoverContent.vue'

4.4 playground 调试

编辑 Popover.vue 组件:

<script setup lang="ts">
import { PopoverRoot, PopoverTrigger, PopoverContent } from '@yi-ui/vue'

</script>

<template>
  <PopoverRoot>
    <PopoverTrigger>
      点击显示/��
    </PopoverTrigger>

    <PopoverContent>
      <ul>
        <li>popover content: 11111</li>
        <li>popover content: 22222</li>
        <li>popover content: 33333</li>
        <li>popover content: 44444</li>
        <li>popover content: 55555</li>
      </ul>
    </PopoverContent>
  </PopoverRoot>
</template>

根�与上� PopoverRoot根组件 调试的对比,我们�少了 toggleState 数�,这是因为我们已�用 rootContext.open �进行关�了。

7.gif

到这里我们一个最最最基本的 Popover 组件 就已�实现了,但是我们从上�分�得�的,一个完整的 Popover 组件 离�开四个大的功能:

  • 基本使用
  • 触å?‘æ–¹å¼?
  • ä½?置显示
  • 自定义内容

显然我们已�实现了基本使用和触�方�功能,那么接下就是�置显示和自定义内容了

三�实战之「�置显示�

1)�考&分�

根�我们上�实现的基本使用,咱们�考一下,�置显示我们应该在哪个组件中实现呢?

很明显还是在 PopoverContent �组件 中,毕竟咱们现实主�内容就是它;

在上�,咱们讲过了 @floating-ui/vue 库是干嘛的了,之所以用到它,还有一个就是,它能自定义�置;

接下�咱们就基于 @floating-ui/vue 库�实现� ~

2)@floating-ui/vue 的简�实用

2.1 安装

pnpm install @floating-ui/vue

2.2 基本使用

<template>
    <button ref="reference">�考元素</button>
    <ul ref="floating" :style="floatingStyles">
        <li>111浮动元素内容</li>
        <li>222浮动元素内容</li>
        <li>333浮动元素内容</li>
        <li>444浮动元素内容</li>
        <li>555浮动元素内容</li>
    </ul>
</template>

<script lang="ts" setup>
import { ref } from 'vue';
import { useFloating } from '@floating-ui/vue';

// 用于定�的�考(或锚点)元素
const reference = ref(null);
// 用于浮动的元素
const floating = ref(null);
// 用于控制浮动元素的样�
const { floatingStyles } = useFloating(reference, floating);
</script>

渲染效果如下:

image.png

默认情况下,浮动元素内容将定�在�考元素的底部中心,所以看到效果是这样的。 那么接下�,就是�更改�置的进阶使用了。

2.3 进阶使用

<template>
    <div class="content">
        <div>
            <button ref="reference">�考元素�下</button>
            <ul ref="floating" :style="floatingStyles">
                <li>浮动元素内容</li>
                <li>浮动元素内容</li>
                <li>浮动元素内容</li>
                <li>浮动元素内容</li>
                <li>浮动元素内容</li>
            </ul>
        </div>
        <div>
            <button ref="referenceTop">�考元素�上</button>
            <ul ref="floatingTop" :style="floatingTopStyles">
                <li>浮动元素内容</li>
                <li>浮动元素内容</li>
                <li>浮动元素内容</li>
                <li>浮动元素内容</li>
                <li>浮动元素内容</li>
            </ul>
        </div>
        <div>
            <button ref="referenceLeft">�考元素�左</button>
            <ul ref="floatingLeft" :style="floatingLeftStyles">
                <li>浮动元素内容</li>
                <li>浮动元素内容</li>
                <li>浮动元素内容</li>
                <li>浮动元素内容</li>
                <li>浮动元素内容</li>
            </ul>
        </div>
        <div>
            <button ref="referenceRight">�考元素��</button>
            <ul ref="floatingRight" :style="floatingRightStyles">
                <li>浮动元素内容</li>
                <li>浮动元素内容</li>
                <li>浮动元素内容</li>
                <li>浮动元素内容</li>
                <li>浮动元素内容</li>
            </ul>
        </div>
    </div>
</template>

<script lang="ts" setup>
import { ref } from 'vue';
import { useFloating, shift, flip, offset } from '@floating-ui/vue';

// 用于定�的�考(或锚点)元素
const reference = ref(null);
const referenceTop = ref(null);
const referenceLeft = ref(null);
const referenceRight = ref(null);
// 用于浮动的元素
const floating = ref(null);
const floatingTop = ref(null);
const floatingLeft = ref(null);
const floatingRight = ref(null);
// 中间件
const middleware = [shift(), flip(), offset(10)];
// 用于控制浮动元素的样�
const { floatingStyles } = useFloating(reference, floating, {
    // 指定�始化浮动�置
    placement: "bottom",
    middleware,
});
const { floatingStyles: floatingTopStyles } = useFloating(referenceTop, floatingTop, {
    // 指定�始化浮动�置
    placement: "top",
    middleware,
});
const { floatingStyles: floatingLeftStyles } = useFloating(referenceLeft, floatingLeft, {
    // 指定�始化浮动�置
    placement: "left",
    middleware,
});
const { floatingStyles: floatingRightStyles } = useFloating(referenceRight, floatingRight, {
    // 指定�始化浮动�置
    placement: "right",
    middleware,
});

</script>


<style scoped>
.content {
    display: flex;
    width: 100vw;
    height: 1000px;
    padding-top: 300px;
}
.content > div {
    width: 20%;
    height: 500px;
}
ul, li {
    list-style: none;
    margin: 0;
    padding: 0;
}

ul {
    border: 1px solid #ccc;
    text-align: center;
}
</style>

咱们直接看渲染结果:

image.png

到这里,咱们已�熟悉了@floating-ui/vue 的基本使用了,当然还有超多��的API 使用,这里就�赘述了,感兴趣的�学�以自行了解官方文档:传�门。

那么接下�就是如何代入到我们的无头组件库中去了。

3)�装 Popper 共用组件

如果直接按照上�的使用,我想这�是一个�格的组件库,因为咱们考�的还有很多,比如代�解耦�代��用等等。

所以咱们需�根�上��简��装一个 Popper 共用组件。

该组件包括:

  • PopperRoot: 根组件
  • PopperContent:主è¦?内容组件
  • PopperArrow:箭头
  • PopperAnchor:触å?‘器

其中核心的 PopperContent 代�(抽�其中部分,主�为了实现�置显示):

<script lang="ts">
import type {
  Placement,
} from '@floating-ui/vue'
import { createContext, useForwardExpose } from '@yi-ui/shared'
import type {
  Align,
  Side,
} from './utils'

export const PopperContentPropsDefaultValue = {
  side: 'bottom' as Side,
  sideOffset: 0,
  align: 'center' as Align,
  alignOffset: 0,
}

export interface PopperContentProps {
  /**
   * ä½?ç½®
   * 
   * @defaultValue "top"
   */
  side?: Side

  /**
   * �离触�器的�离
   *
   * @defaultValue 0
   */
  sideOffset?: number

  /**
   * 对�方�相对于触�器
   *
   * @defaultValue "center"
   */
  align?: Align

  /**
   * å??移é‡?
   *
   * @defaultValue 0
   */
  alignOffset?: number
}

export interface PopperContentContext {}

export const [injectPopperContentContext, providePopperContentContext]
  = createContext<PopperContentContext>('PopperContent')
</script>

<script setup lang="ts">
import { computed, ref } from 'vue'
import {
  useFloating,
} from '@floating-ui/vue'
import { injectPopperRootContext } from './PopperRoot.vue'
import {
  Primitive,
} from '../Primitive'

const props = withDefaults(defineProps<PopperContentProps>(), {
  ...PopperContentPropsDefaultValue,
})
const rootContext = injectPopperRootContext()

const { forwardRef } = useForwardExpose()

const floatingRef = ref<HTMLElement>()

const desiredPlacement = computed(
  () =>
    (props.side
    + (props.align !== 'center' ? `-${props.align}` : '')) as Placement,
)


const { floatingStyles } = useFloating(
  rootContext.anchor,
  floatingRef,
  {
    strategy: 'fixed',
    placement: desiredPlacement,
  },
)

</script>

<template>
  <div
    ref="floatingRef"
    :style="{
      ...floatingStyles,
    }"
  >
    <Primitive
      :ref="forwardRef"
    >
      <slot />
    </Primitive>
  </div>
</template>

我相信通过上�的 @floating-ui/vue 的简�实用,大家都能看的懂其中的代�;

还有更多的细节和相关组件这里就��赘述介�了,当然一个完整的 Popper 共用组件 远�止于此,感兴趣的�学�以看看�代�:传�门

4)完善 PopoverContent �组件

根�上�的已�实现了的 PopoverContent �组件 �完善

<script lang="ts">
import type { PopperContentProps } from '@/Popper'
import type { PrimitiveProps } from '@/Primitive'

export interface PopoverContentProps extends PopperContentProps, PrimitiveProps {
  forceMount?: boolean
}
</script>

<script setup lang="ts">
import { injectPopoverRootContext } from './PopoverRoot.vue'
import { useForwardExpose, useForwardPropsEmits } from '@yi-ui/shared'
import { PopperContent } from '@/Popper'
import { Presence } from '@/Presence'

const props = defineProps<PopperContentProps>()
const emits = defineEmits()

const rootContext = injectPopoverRootContext()

const forwarded = useForwardPropsEmits(props, emits)
const { forwardRef } = useForwardExpose()

</script>

<template>
  <Presence :ref="forwardRef" :present="rootContext.open.value">
    <PopperContent
      v-bind="forwarded"
      :ref="forwardRef"
    >
      <slot />
    </PopperContent>
  </Presence>
</template>

是的,这样就实现了,基本的�置了

5)playground 调�

5.1 vue 项目中调试

<script setup lang="ts">
import { PopoverContent, PopoverRoot, PopoverTrigger } from '@yi-ui/vue'
</script>

<template>
  <PopoverRoot>
    <PopoverTrigger>
      点击显示/��左边的内容
    </PopoverTrigger>

    <PopoverContent side="left">
      <ul>
        <li>popover content: 11111</li>
        <li>popover content: 22222</li>
        <li>popover content: 33333</li>
        <li>popover content: 44444</li>
        <li>popover content: 55555</li>
      </ul>
    </PopoverContent>
  </PopoverRoot>
  <PopoverRoot>
    <PopoverTrigger>
      点击显示/���边的内容
    </PopoverTrigger>

    <PopoverContent side="right">
      <ul>
        <li>popover content: 11111</li>
        <li>popover content: 22222</li>
        <li>popover content: 33333</li>
        <li>popover content: 44444</li>
        <li>popover content: 55555</li>
      </ul>
    </PopoverContent>
  </PopoverRoot>
</template>

5.2 渲染效果

image.png

四�实战之「自定义内容�

自定义内容,其实在上�就已�实现了,�是没有具体的 demo 展示,咱们根�上�实现了的�自定义一下内容

1)demo 展示:

<script setup lang="ts">
import { PopoverContent, PopoverRoot, PopoverTrigger } from '@yi-ui/vue'
</script>

<template>
  <PopoverRoot>
    <PopoverTrigger>
      切�县市
    </PopoverTrigger>

    <PopoverContent>
      <ul>
        <li>北京市</li>
        <li>上海市</li>
        <li>广州市</li>
        <li>深圳市</li>
      </ul>
    </PopoverContent>
  </PopoverRoot>
</template>

<style scoped>
ul, li {
    list-style: none;
    margin: 0;
    padding: 0;
}

ul {
    border: 1px solid #ccc;
    text-align: center;
    width: 100px;
    background: #ffffff;
    border-radius: 4px;
    box-shadow: 0px 3px 16px 2px rgba(0,0,0,0.04), 0px 7px 14px 1px rgba(0,0,0,0.08), 0px 5px 5px -3px rgba(0,0,0,0.08); 
}

ul li {
  height: 40px;
  line-height: 40px;
  font-size: 14px;
  cursor: pointer;
  color: #333333;
}
li:hover {
  background: #f2f8ff;
  color: #006aff;
}
</style>

2)渲染效果:

image.png

五��结

如果你有实现过一个传统组件库,我相信这个无头组件库的实现方�肯定会让你眼�一亮;

因为它的确少了很多东西,就�独拿 style 样��说,你�仅�把一些样�抽离出�,还�写��共用的样�,怎么�心怎么�;

�说了,说多了都是泪~


当然我们一个完整的 Popover 无头组件 ��仅�于此,为了更好的�出其功能的自定义,还有许多组件与功能�装,这里就�是一两篇文章能讲解的清楚的,大家��知�其基本实现方�与概念��。


原本想在这篇文章中把文档和�元测试都讲解了的,但是写��现越写越多,内容越�越多,毕竟 4 �多字已�够看了。

还未完善的有:

  • 文档撰写
  • å?•å…ƒæµ‹è¯•
  • 支æŒ? Nuxt 调试
  • 打包构建

所以�了一个功能分割��,这篇以核心代�为主;

下一篇�讲解文档编写和�元测试的讲解,敬请期待~

总结

当�主�实现了一个最基本的 Popover 无头组件,包括其中的一些核心知识点,我相信无论你是��一个无头组件库还是传统组件库这都会对�益匪浅的。

Headless UI 往期相关文章:

  1. 在 2023 年屌爆了一整年的 shadcn/ui 用的 Headless UI 到底是何方神圣?
  2. 实战开始 🚀 在 React 和 Vue3 中使用 Headless UI 无头组件库
  3. 无头组件库既然这么� 🔥 那么我们自己手动实现一个�完�所谓的 KPI �

Headless UI (1).png

如果想跟我一起讨论技术�水摸鱼, 欢迎加入�端学习群�

如果扫�人数满了,�以扫�添加个人 vx 拉你:JeddyGong

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

关注我,带您一起��端 ~