前端唯一的护城河?结合 AI 将字节组件库 Headless 化后的感想~

以下是我个人将团队正在使用的字节和蚂蚁的组件库 Arco/Ant Design, 结合 AI 将源码重写为无样式组件(Headless 化)的一些感触。简单结论是: AI 还是差点意思。

在目前的 Web 前端开发或独立开发圈子里,如果你还没听过 Headless(逻辑与 UI 解耦)组件库,那你可能正在错过 AI 时代最前沿的开发范式。

在海外,这类方案早已席卷社区。无论是大名鼎鼎的 MUI 旗下的 Base UI,还是狂揽 114k+ Star、彻底改变开发习惯的 shadcn/ui,都在传达一个信号:UI 的归 UI,逻辑的归逻辑。

💡 什么是 Headless? 简单来说,组件只负责驱动交互核心逻辑,但不强绑定任何视觉样式。开发者可以像捏泥人一样,在不破坏"大脑"的前提下,随心所欲地定制"皮肤"。

个人说明

首先说明一下,我自己潜心研究国内外组件库源码大概有两年时间了,例如国外的 chakra-ui, shadcn ui, floating-ui 组件库, 国内的 ant-design、字节的 arco-design, 腾讯的 Tdesign 组件库的源码。(偶尔发现源码 BUG 提的 PR 也合并了)。

也做过两个组件库的编写,改造过程中一个很深的感触就是 AI 从 0 到 1 造 Ant Design/Arco Design 级别的组件几乎不可行(你可以立马试试),甚至可以说是写的是垃圾代码, 必须有一个资深以上开发本身就了解这些组件的架构和核心技术细节的前提下,AI 才能在辅助层面极大的提高开发效率。

简单总结就是: AI 的产出代码质量好坏完全取决于开发者本身的技术水平。

虽然很多营销号宣称"AI 无敌",只要几句话就能生成一切,但客观地说这些营销号的作者大多数并没有拿出很多自己开发经历的经验和开源出来的作品作为证据。

本文主要案例来自从源码层面改造字节的组件库 Arco Design 的表单组件 Form(改造了很多组件,这篇文中着重拿 Form 表单举例)。欢迎访问网站

(https://meow.frontlight.tech/zh/form) 查看改造后的 Form 组件具体功能和 API

好了,我们继续之前的 headless 的概念往下说。

现状:国内 B 端的"硬核"与 "枷锁"

虽然 Headless 组件库如此流行,但在国内 B 端市场,情况有些特殊:以 Ant Design 为代表的大厂系组件库(如字节的 Arco Design / Semi Design、腾讯的 TDesign 等)稳坐头把交椅。

坦白说,它们处理极其复杂业务场景的能力(联动、嵌套、动态校验等),确实远超国外的 Headless 库。这也是为什么国内开发者至今仍难以割舍这些组件库的原因。

但在全面拥抱 AI 编程的今天,这些传统的组件库无论是样式覆盖,还是通过修改 CSS 变量来定制样式,对 AI 来说都存在"适配性断层"------因为 AI 最擅长的是从零生成样式,而不是在已有的层层样式嵌套中搞魔改。

那么,一个硬核的问题摆在了我们面前:

我们能否既保留 Ant Design 极其强大的逻辑处理能力,又让它彻底"Headless 化" ------ 也就是把 100% 的视觉控制权交还给开发者?让 DOM 结构的控制权交给开发者?

答案是肯定的,但问题就在于改造的难度非常大。为什么呢,我们来举一个实战翻车案例:

案例分析:一个"暗藏杀机"的 validate 需求

我举个典型的例子。在重构 Form 组件的校验方法时,我们需要一个支持多种调用方式的 validate 函数:

  • 回调函数模式:this.form.validate((errors, values) => { ... })
  • Promise 模式:this.form.validate().then(values => { ... })
  • Async/Await 模式:const values = await this.form.validate()

为了实现这个功能,在 Arco Design 源码中是这样做的,首先它用一个高阶函数 promisify 来对原始校验逻辑进行包装。

javascript 复制代码
// 核心逻辑简述
public validate = promisify((...args) => {
// 省略复杂的校验逻辑...
const promises = controlItems.map(x => x.validateField());

Promise.all(promises).then(result => {
    // 逻辑处理...
    if (Object.keys(errors).length) {
      callback?.(errors, cloneDeep(values));
    } else {
      callback?.(null, cloneDeep(values));
    }
  });
});

而 promisify 的内部实现,最关键的一行是判断用户当前的调用意图的 if 语句:

javascript 复制代码
function promisify(fn) {
  return function (...args) {
      // 关键判断:如果最后一个参数是函数,说明用户在用回调模式
      if (typeof args[args.length - 1] === 'function') {
        return fn.apply(this, args); // 直接执行,不返回 Promise
      } else {
        // 否则,返回一个全新的 Promise 实例
        return new Promise((resolve, reject) => {
          // ...将原始函数 Promise 化
        });
      }
    };
}

而在重构这部分代码的时候,我希望优化 this.validate 内部逻辑,并去掉 promisify, 用来减少代码量,但遗憾的是这么一个小小的需求, AI 会在这里"集体翻车"。

我测试了目前市面上最顶尖的模型:Claude 4.7 (Opus), GPT-5.4, Gemini 3.1 Pro。

在我没有明确提示的情况下,没有一个模型能自动识别并改造出兼容 this.validate 多种用法的代码。它们要么粗暴地全部转化为 Promise,要么干脆破坏了原有的回调链路。(核心是一定要判断 this.validate() 中最后一个参数是否是回调,如果是就不要包装为 Promise, 否则只保留之前版本回调的用法)。

只有在我明确指出错误, 并且加入限定的提示词后,它们才会像"挤牙膏"一样修正。

很多类似体验让我深切感受到之前提到的结论: 如果使用者本身技术不够扎实,在面对这种复杂基础设施代码时,AI 写出的代码,很难察觉到它异常的地方。

同时对我们打工人来说有个非常矛盾的使用 AI 的问题浮现出来:

对于我们打工人来说,AI 犯错,顶多是生成了一段 Bad Code;但人犯错,可能你人就被裁了或者影响你的绩效。

传统组件库在 AI 时代的局限性

说到这里,可能有同学可能会问:传统的组件库如 Ant DesignAI 也能帮我改样式啊,为什么非要搞 Headless

这里我们要纠正一个认知偏差:AI 最强大的能力是"生成",而它最痛苦的能力是"魔改"。

Ant Design 目前的 UI 方案对 AI 并不友好

目前国内类似 Antd 的组件库仍然是以改变 CSS 变量的方式定制 UI, 这就带来两个核心问题。

  • 一个组件能定义的 CSS 变量有限,并不能随心所以修改 UI
  • 另一个是组件内部 DOM 结构固定,外部是无法修改的 这就导致 AI 并不知道 Antd 组件源码的 DOM 结构。并且修改的 CSS 变量也是基于语义化,而不是真的知道源码 DOM 结构上下文而做出精确修改。

所以传统组件库像是一个高度集成的"黑盒"。这是对 AI 非常非常不友好的。

而在 Headless 模式下,所有的 DOM 结构都是你(或者 AI)亲手写出来的。 AI 对它生成的代码拥有 100% 的上下文掌控力。它不需要去猜测黑盒里的逻辑,它只需要根据你的描述,在逻辑钩子外结合任何 CSS 框架,例如 Tailwind CSSUnoCSS, Sass 等等。

现实中类似的样式历史债务比比皆是

我相信大多数做过复杂 B 端项目的团队都遇到过一个问题,就是 UI 有自己定制化的样式,然后你不得不去覆盖 Ant Design 的样式, 我直接给你看飞书文档中(他们前期部分团队应该用的 Ant Design),这样国内的顶级团队依然有这样的问题。

以下是飞书问卷页面,我随手截图的内容,这种覆盖的样式如下:

而这只是一个按钮的 CSS 样式,团队自定义后的样式覆盖情况(覆盖了起码有 4 次)。连字节核心产品都这样,你就可想而知这是一个多么普遍的问题了。

核心问题就在于多团队合作的时候,在这种提供完整 UI 的组件库里是很难避免样式覆盖问题的(以为 CSS 类已经写死在组件里了)。即使现在使用 CSS 变量来控制 UI 样式。

Headless 组件的优势举例

我相信绝大多数前端有过后台管理系统的经验,都会用过 Ant Design 的 Form 组件,其中 Ant Design 的 Form 组件在布局中最大的问题在于,其本身将固定的栅格布局系统嵌入其中,代码如下(注意 labelCol={{ span: 8 }}wrapperCol={{ span: 16 }})

javascript 复制代码
import React from 'react';
import { Button, Form, Input } from 'antd';

const App = () => (
  <Form
    name="basic"
    labelCol={{ span: 8 }}
    wrapperCol={{ span: 16 }}
    onFinish={(values) => console.log('Success:', values)}
  >
    <Form.Item
      label="Username"
      name="username"
      rules={[{ required: true, message: 'Please input your username!' }]}
    >
      <Input />
    </Form.Item>

    <Form.Item label={null}>
      <Button type="primary" htmlType="submit">Submit</Button>
    </Form.Item>
  </Form>
);

大家看上面的代码,核心就在于 labelCol={{ span: 8 }}wrapperCol={{ span: 16 }}。这看似简单的配置,其实是开发者噩梦的开始:

  • Ant Design 的栅格是基于将屏幕划分为 24 份。所以 span: 8,意味着是整个当前宽度的 8 / 24
  • 但我们开发中,几乎没有遇到 UI 是根据这种栅格布局给我们开发设计稿的,基本都是 px 为单位的设计稿,或者响应式布局的设计稿。

这就造成了如果我们想精确到例如 Form 表单中,Label 和具体的例如 Input 左右间距 24px 是比较麻烦的。

再例如如果你想在 LabelInput 之间插入一个自定义图标,或者想改变 LabelHTML 标签属性。对不起,如果组件库没给 API,你进不去。这种"黑盒"结构导致你即便有再强的 CSS、Html 功底,也只能在外面干瞪眼。

但即使 Headless 组件这么好用,为什么国内复杂的业务前端开发基本还是离不开 Ant Design,Arco Design 这类组件库呢?

为什么 shadcn/ui(headless 组件库) 很香,但国内 B 端却离不开 Ant Design?

最近两年,shadcn/ui 在开发者圈子里简直是"神"一般的存在。它那种把代码直接复制到项目里的逻辑,给了开发者前所未有的掌控感。但如果你拿着 shadcn/ui 去接一个国内大厂的 B 端外包或者内部系统,大概率你会写代码写到崩溃。

为什么?因为国内 B 端业务的复杂度,和国外那种追求极简的 SaaS 工具完全不是一个量级的。

"功能密度"的降维打击

举个例子,Select 组件,我除去单纯样式相关的参数,Ant Design 提供约 40+ 个左右的功能参数,而 shadcn/ui 除去单纯样式相关的参数,提供约 10+ 个左右的功能参数。

而这其中的功能参数的差值,使用者想要实现一样的功能,不仅仅要写 UI 样式,还需要补齐对应的逻辑功能。

所以也就出现了一些基于这些 headless 组件库封装的上层组件库(感觉他们的意义不大,还不如直接用 Arco Design, Ant Design 呢)。

"业务确定性"胜过"样式自由"

国内的研发节奏只有两个字:。之前在滴滴做研发的时候,当时是做一个对外售卖的财务软件,就那种按分钟计时的排期(时间紧),你又害怕出 bug 直接影响绩效(质量要求高,功能要求多)。当时唯一的选择就是 Arco DesignAnt Design 这种大而全的组件库。

你说样式自定义的问题?那就随缘处理了,反正强行样式覆盖嘛😭。

所以,最完美的解法是:抽离 Arco/Antd 历经千万级 DAU 业务考验的内部逻辑,套上类似 shadcn/ui 的极致自由度。 我们来看看改造后的 Form 具体长什么样:

我们拿之前的 Arco Design 和 Ant Design 的 Form 使用案例来说,只是改造之前的用法:

javascript 复制代码
import React from 'react';
import { Button, Form, Input } from 'antd';

const App = () => (
  <Form
    name="basic"
    labelCol={{ span: 8 }}
    wrapperCol={{ span: 16 }}
    onFinish={(values) => console.log('Success:', values)}
  >
    <Form.Item
      label="Username"
      name="username"
      rules={[{ required: true, message: 'Please input your username!' }]}
    >
      <Input />
    </Form.Item>

    <Form.Item label={null}>
      <Button type="primary" htmlType="submit">Submit</Button>
    </Form.Item>
  </Form>
);

改造后的用法如下, 使用方法跟 React-Form-Hook 非常类似:

javascript 复制代码
import { Form } from'@meow-kit/web-react';
import { Button, Checkbox, Input } from'antd';

function App() {
const [form] = Form.useForm();
const errors = Form.useFormErrors(form);

const onFinish = (values) => {
    console.log('Success:', values);
  };

const onFinishFailed = (errorInfo) => {
    console.log('Failed:', errorInfo);
  };

return (
    <Form
      name="basic"
      form={form}
      initialValues={{ remember: true }}
      onSubmit={onFinish}
      onSubmitFailed={onFinishFailed}
      autoComplete="off"
      className="w-full"
    >
      <FormControl
        label="Username"
        field="username"
        errors={errors}
        getValueFromEvent={(e) => e.target.value}
        rules={[{ required: true, message: 'Please input your username!' }]}
      >
        <Input className="w-full" placeholder="Enter your username" />
      </FormControl>

      <Button type="primary" htmlType="submit" className="ml-22">
        Submit
      </Button>
    </Form>
  );
};

const FormControl = ({
  label,
  field,
  rules,
  errors,
  children,
  className = '',
  layout = 'horizontal', // 'vertical' | 'horizontal' 
  labelWidth = 'w-18',
  ...itemProps
}) => {
// get current field error message
const errorMessage = errors?.[field]?.message;
// check if field is required, used to render red star icon
const isRequired = rules?.some((rule) => rule.required);

const isHorizontal = layout === 'horizontal';

return (
    <div
      className={`flex relative ${
        isHorizontal ? 'flex-row items-start' : 'flex-col'
      } ${className}`}
    >
      {/* Label 区域 */}
      {label && (
        <label
          className={`
            text-sm font-medium text-gray-700
            ${
              isHorizontal
                ? `shrink-0 text-right mr-4 pt-1.5 ${labelWidth}` // 横向:固定宽度、靠右对齐、向下偏移以对齐输入框内文字
                : 'mb-1.5 block' // 纵向:底部留白、独占一行
            }
          `}
        >
          {isRequired && <span className="mr-1 text-red-500">*</span>}
          {label}
        </label>
      )}

      {/* Core control wrapper area: occupy remaining space */}
      <div className="flex-1 min-w-0 flex flex-col">
        <Form.Item field={field} rules={rules} {...itemProps}>
          {children}
        </Form.Item>

        {/* Unified Error message UI */}
        <div
          className={`mt-1 min-h-[20px] text-xs text-red-500 transition-all duration-300 ${
            errorMessage
              ? 'translate-y-0 opacity-100'
              : '-translate-y-1 opacity-0'
          }`}
        >
          {errorMessage}
        </div>
      </div>
    </div>
  );
};

其中 FormControl 组件是豆包 AI 写的(证明语义化后,简单的模型也能很好的完成任务),是我们封装的 UI样式,这个是交给用户的(我把Form` 中所有跟样式有关的源码全部抽离),然后使用 AI 来自定义封装的,基本上可以任意让 AI 折腾,DOM 结构和 CSS 随便自定义。

所以这是一个很好的将 Ant Design/Arco Design Headless 化的案例。欢迎大家一起讨论~

源码地址:github仓库(https://github.com/lio-mengxiang/meow-ui)

相关推荐
winlife_1 小时前
AI 怎么验证 Unity PlayMode 行为:截图 + 输入模拟的完整闭环
人工智能·unity·游戏引擎·ai编程·claude·playmode
冴羽yayujs1 小时前
前端周报:Remix 3、Node 26 与 Chrome 148
前端
问心无愧05131 小时前
ctf show web 入门39
android·前端·笔记
卷无止境1 小时前
Alpine.js入门笔记
前端
@王先生11 小时前
【K8S-ETCD初始化三节点集群】
前端·chrome·k8s·etcd·集群
千里马学框架1 小时前
WMS/AMS深入WindowState如何正确找到自己在层级结构树中位置进行挂载
android·wms·ai编程·性能·系统开发·车载开发·framework工程师
LinDaiDai_霖呆呆1 小时前
做 Agent 开发入门必懂的 10 个 Agent 核心概念
前端·agent·ai编程
CocoaKier2 小时前
发现豆包有一个趣又实用的功能,文章链接转播客
ai编程
原则猫2 小时前
await 到底在等待什么
前端