前端唯一的護城河?結合 AI 將字節元件庫 Headless 化後的感想~

以下是我個人將團隊正在使用的字節和螞蟻的元件庫 `Arco/Ant Design`,結合 AI 將原始碼重寫為無樣式元件(Headless 化)的一些感觸。簡單結論是:AI 還是差點意思。

在目前的 Web 前端開發或獨立開發圈子裡,如果你還沒聽過 Headless(邏輯與 UI 解耦)元件庫,那你可能正在錯過 AI 時代最前沿的開發範式。

在海外,這類方案早已席捲社群。無論是大名鼎鼎的 MUI 旗下的 Base UI,還是狂攬 114k+ Star、徹底改變開發習慣的 shadcn/ui,都在傳達一個訊號:UI 的歸 UI,邏輯的歸邏輯。

💡 什麼是 Headless?簡單來說,元件只負責驅動互動核心邏輯,但不強綁定任何視覺樣式。開發者可以像捏黏土人一樣,在不破壞「大腦」的前提下,隨心所欲地自訂「皮膚」。

個人說明

首先說明一下,我自己潛心研究國內外元件庫原始碼大概有兩年時間了,例如國外的 chakra-uishadcn uifloating-ui 元件庫,國內的 ant-design、字節的 arco-design、騰訊的 Tdesign 元件庫原始碼。(偶爾發現原始碼 BUG 提的 PR 也合併了)。

也做過兩個元件庫的編寫,改造過程中一個很深的感觸就是 AI 從 0 到 1 造 Ant Design/Arco Design 級別的元件幾乎不可行(你可以立馬試試),甚至可以說寫出來的是垃圾程式碼,必須在資深以上開發本身就了解這些元件的架構和核心技術細節的前提下,AI 才能在輔助層面大幅提高開發效率。

簡單總結就是:AI 的產出程式碼品質好壞完全取決於開發者本身的技術水平。

雖然很多行銷號宣稱「AI 無敵」,只要幾句話就能生成一切,但客觀地說這些行銷號的作者大多並沒有拿出很多自己開發經歷的經驗和開源作品作為證據。

本文主要案例來自從原始碼層面改造字節的元件庫 Arco Design 的表單元件 Form(改造了很多元件,這篇文中著重拿 Form 表單舉例)。歡迎造訪網站

(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 体验AI代码助手 代码解读复制代码// 核心邏輯簡述
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 体验AI代码助手 代码解读复制代码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% 的上下文掌控力。它不需要去猜測黑盒裡的邏輯,它只需要根據你的描述,在邏輯 Hook 外結合任何 CSS 框架,例如 Tailwind CSSUnoCSSSass 等等。

現實中類似的樣式歷史債務比比皆是

我相信大多數做過複雜 B 端專案的團隊都遇到過一個問題,就是 UI 有自己客製化的樣式,然後你不得不去覆蓋 Ant Design 的樣式,我直接給你看飛書文件中(他們前期部分團隊應該用的 Ant Design),這樣國內的頂級團隊依然有這樣的問題。

以下是飛書問卷頁面,我隨手截圖的內容,這種覆蓋的樣式如下:

image.png

而這只是一個按鈕的 CSS 樣式,團隊自訂後的樣式覆蓋情況(覆蓋了起碼有 4 次)。連字節核心產品都這樣,你就可想而知這是一個多麼普遍的問題了。

核心問題就在於多團隊合作的時候,在這種提供完整 UI 的元件庫裡是很難避免樣式覆蓋問題的(以為 CSS 類已經寫死在元件裡了)。即使現在使用 CSS 變數來控制 UI 樣式。

Headless 元件的優勢舉例

我相信絕大多數前端有過後台管理系統的經驗,都會用過 Ant Design 的 Form 元件,其中 Ant Design 的 Form 元件在佈局中最大的問題在於,其本身將固定的格線佈局系統嵌入其中,程式碼如下(注意 labelCol={{ span: 8 }}wrapperCol={{ span: 16 }}):

javascript 体验AI代码助手 代码解读复制代码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 DesignAnt Design 呢)。

「業務確定性」勝過「樣式自由」

國內的研發節奏只有兩個字:。之前在滴滴做研發的時候,當時是做一個對外售賣的財務軟體,就那種按分鐘計時的排程(時間緊),你又害怕出 bug 直接影響績效(品質要求高,功能要求多)。當時唯一的選擇就是 Arco DesignAnt Design 這種大而全的元件庫。

你說樣式自訂的問題?那就隨緣處理了,反正強行樣式覆蓋嘛😭。

所以,最完美的解法是:抽離 Arco/Antd 歷經千萬級 DAU 業務考驗的內部邏輯,套上類似 shadcn/ui 的極致自由度。我們來看看改造後的 Form 具體長什麼樣:

我們拿之前的 Arco Design 和 Ant Design 的 Form 使用案例來說,只是改造之前的用法:

javascript 体验AI代码助手 代码解读复制代码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 Hook Form 非常類似:

javascript 体验AI代码助手 代码解读复制代码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 倉庫(github.com/lio-mengxia…)


原文出處:https://juejin.cn/post/7638577060101161000


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝12   💬4   ❤️1
464
🥈
alicec
📝1   ❤️2
87
#4
我愛JS
💬1  
3
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
📢 贊助商廣告 · 我要刊登