Headless UI
- Published on
一、什么是Headless
Headless 一词最初就是来源于 Headless Computer(无头计算机)也就是 Headless System(无头系统),百科的介绍为:
无头系统(Headless System)是指已配置为无须显示器(即“头”)、键盘和鼠标操作的计算机系统或设备。无头系统通常通过网络连接控制,但也有部分无头系统的设备需要通过RS-232串行连接进行设备的管理。服务器通常采用无头模式以降低运作成本。
常见的无头系统一种是服务器,一种是路由器。后来Headless一词有了更多的组合,如 Headless Browser、Headless CMS, Headless BI 等等,headless也更像是一种设计模式的表述。下面以Headless CMS为例来从侧面展示一些Headless所代表的含义。
传统的CMS系统,如WordPress,它们包括前端和后端的完整堆栈。这意味着它们不仅需要管理内容,还需要管理如何显示这些内容。然而,Headless CMS只关注内容管理,而把展示的工作交给了前端应用程序。因此,它是“无头的”,即没有包含前端展示的功能。这使得整个架构有了更高的可扩展性,内容不仅可以出现在浏览器上同时还可以在APP或是可穿戴设备上显示。
这也就是前后端分离成为软件开发主流模式的开始。当今的headless CMS除了给开发人员提供的API,也是一个集中管理和分发内容的平台。而具体到前端开发领域这种模式仍然可以进一步被应用。
二、什么是 Headless UI
2019年React v16.8 的发布使Hooks 成为 React 生态系统中的一个正式特性,提供了在函数组件中使用 state 和其他 React 特性的能力,使得组件的使用变得更加强大和灵活。而vue在2020年发布的vue3中也支持了组合式API, 之前逻辑重用的mixins也因此不再被推荐。而这些UI框架的改进使得Headless UI组件库的产生有了先决条件。
Headless UI 全称是 Headless User Interface (无头用户界面),是一种前端开发的方法论,其核心思想是将 用户界面(UI)的逻辑和交互行为 与 视觉表现(CSS 样式) 分离开来。
具体来说,Headless UI 的组件通常是纯粹的 JavaScript(或其他编程语言)组件,它们包含了一些交互逻辑和状态管理,但没有任何与视觉样式相关的代码。
三、为什么要使用 Headless UI
如今我们经常使用的element ui或者ant组件库,它们提供了组件功能和样式上的强大集成,开发的效率也因此有了大幅度提升。当然这也是有两面性的,一个常常面临的开发场景就是设计师期望修改这些组件的样式,而这也就变得十分的麻烦。我们不得不频繁使用deep使新的样式生效,同时还要小心翼翼避免影响到其他的地方。另一方面对于基础组件的交互逻辑的修改却是比较少的,功能的扩展也是简单的。
那么完全自己写呢?使用UI框架在表面层实现自定义组件通常并不困难。但大多数时候,这些自定义组件的实现往往会忽略UI组件行为的一些非常重要的方面。这包括诸如焦点、模糊状态感知、键盘导航和遵循WAI-ARIA设计原则等行为。根据W3C规范正确实现是比较困难的,并且可能会显著减慢产品开发速度。而Headless UI库的目的在实现上面组件行为的同时给你代码的所有权和控制权,让你可以决定组件是如何构建和样式化。
四、组件库
最开始出现的就是名为headlessUI的组件库,但是headlessUI提供的组件并不多。之后就是组件更为完善的radix UI组件库,其中已经囊括了日常经常使用的大部分组件,而且也同样提供了默认的组件样式,即使不进行修改也是可用性很高的。
然后着重介绍的就是集radix UI之所长的shadcn-ui。在JavaScript Rising Stars公布的2023 年 JavaScript 项目榜单中,shadcn-ui 获得了整体推荐的第一名。
shadcn-ui 与其他流行库的区别在于它不是可下载的 NPM 包。相反,可以通过终端命令集成shadcn-ui 组件,该命令安装底层依赖项并将组件源代码直接复制到代码库中以进行进一步修改。
它提供了如下的功能:
- 主题和主题编辑器:可以使用图形界面来创建自定义主题。编辑器会生成包含自定义样式定义的代码片段,只需将其复制粘贴到程序中即可使用。
- 深色模式:支持 Next.js 和 Vite 应用的暗黑模式
- cli工具:自动配置项目,与框架集成、生成配置文件及添加组件等。
- 组件库:包含40+的基础组件库。
可以通过下面的方式来向项目中添加新的组件:
1、在初始化项目之后运行命令
npx shadcn-vue@latest init
2、根据命令行问题来配置components.json文件,其中有一些主题颜色和css的选项
Would you like to use TypeScript (recommended)? no / yes
Which style would you like to use? › Default
Which color would you like to use as base color? › Slate
Where is your global CSS file? › › src/index.css
Do you want to use CSS variables for colors? › no / yes
Where is your tailwind.config.js located? › tailwind.config.js
Configure the import alias for components: › @/components
Configure the import alias for utils: › @/lib/utils
Are you using React Server Components? › no / yes (no)
3、然后开始添加需要使用的组件
npx shadcn-vue@latest add button
4、button组件的代码将生成到项目中
<script setup>
import { Primitive } from "radix-vue";
import { buttonVariants } from ".";
import { cn } from "@/lib/utils";
const props = defineProps({
variant: { type: null, required: false },
size: { type: null, required: false },
class: { type: null, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false, default: "button" },
});
</script>
<template>
<Primitive
:as="as"
:as-child="asChild"
:class="cn(buttonVariants({ variant, size }), props.class)"
>
<slot />
</Primitive>
</template>
import { cva } from "class-variance-authority";
export { default as Button } from "./Button.vue";
export const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
xs: "h-7 rounded px-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
组件中使用了cn函数,它结合了clsx和tailwind-merge两个库的能力,以管理css类。clsx允许通过className串联来有条件地应用样式返回类字符串,而tailwind-merge则浅合并了类字符串,以确保没有样式冲突。
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs) {
return twMerge(clsx(inputs));
}
import clsx from 'clsx';
// or
import { clsx } from 'clsx';
// Strings (variadic)
clsx('foo', true && 'bar', 'baz');
//=> 'foo bar baz'
// Objects
clsx({ foo:true, bar:false, baz:isTrue() });
//=> 'foo baz'
// Objects (variadic)
clsx({ foo:true }, { bar:false }, null, { '--foobar':'hello' });
//=> 'foo --foobar'
// Arrays
clsx(['foo', 0, false, 'bar']);
//=> 'foo bar'
// Arrays (variadic)
clsx(['foo'], ['', 0, false, 'bar'], [['baz', [['hello'], 'there']]]);
//=> 'foo bar baz hello there'
// Kitchen sink (with nesting)
clsx('foo', [1 && 'bar', { baz:false, bat:null }, ['hello', ['world']]], 'cya');
//=> 'foo bar hello world cya'
通过分析Button组件的实现,我们可以看到一些与SOLID原则相关的模式:
单一职责原则(SRP):Button组件似乎有一个单一的职责,即根据提供的变体呈现不同样式的按钮。它将样式管理委托给buttonVariants对象。
开放/封闭原则(OCP):代码似乎遵循开放/封闭原则,允许添加新变体而不需要修改现有代码。新变体可以很容易地添加到buttonVariants定义中的变体对象中。
依赖倒置原则(DIP):Badge组件及其样式被分开定义。Button组件依赖于buttonVariants对象来获取样式信息。这种分离允许灵活性和更容易的维护。
一致性和可重用性:代码通过使用cva实用函数来管理基于变体的应用样式,从而促进了一致性。这种一致性可以使开发人员更容易理解和使用组件。此外,Button组件是可重用的,并且可以轻松地集成到应用程序的不同部分。
关注点分离:样式和渲染的关注点被分离。buttonVariants对象处理样式逻辑,而Button组件负责渲染和应用样式。