React18, Reagent(v1.3)与Ant Design(V5)联合使用指南

Table of Contents

1. I. 引言

1.1. A. Reagent 和 Ant Design 简介

Reagent 是一个为 ClojureScript 设计的极简主义 React 包装库. 它允许开发者使用纯 ClojureScript 函数和数据结构(特别是 Hiccup 风格的语法)来高效地构建用户界面.

  1. Reagent 的核心优势在于其简洁性, 性能以及与 ClojureScript 不可变数据结构的天然契合, 这通常能带来比原生 React 更优的渲染性能.
  2. 截至最新版本(例如 1.3.0), Reagent 持续改进其与 React 最新特性的兼容性, 包括对 React 18 的实验性支持.
  3. Ant Design (Antd) 则是一个企业级的 UI 设计体系和基于 React 的 UI 组件库
  4. 它提供了大量预制, 高质量, 可定制的组件, 旨在帮助开发者快速构建美观, 一致且功能丰富的用户界面
  5. Ant Design 5.x 版本引入了重要的技术调整, 如采用 CSS-in-JS 方案(使用 @ant-design/cssinjs) 以支持动态主题和更好的性能, 并用 Day.js 替换了 Moment.js 以优化包大小.
  6. 其设计理念围绕"自然", "确定", "意义", "生长" 四个核心价值观展开, 旨在为用户和设计者提供高效愉悦的体验.

1.2. B. 两者结合的优势

将 Reagent 的函数式编程范式, 高效的状态管理(通过 reagent/atom) 与 Ant Design 丰富的组件生态系统和成熟的设计规范相结合, 可以为 ClojureScript 开发者带来显著的优势:

  • 开发效率提升: Antd 提供了大量开箱即用的组件, 减少了从头构建 UI 元素的需求 6. Reagent 的 Hiccup 语法比 JSX 更简洁, 且能利用 ClojureScript 的强大功能进行动态 UI 构建
  • 一致的设计语言: Antd 保证了应用在视觉和交互上的一致性, 特别适合企业级应用开发.
  • 高性能: Reagent 利用 ClojureScript 的不可变数据结构优化了 React 的渲染周期 Antd 5.x 的 CSS-in-JS 和按需加载特性也有助于提升应用性能
  • 强大的生态系统: ClojureScript 社区活跃, Reagent 本身成熟稳定. Antd 拥有庞大的用户基础和丰富的文档资源
  • 函数式与响应式编程: Reagent 的 atom 提供了简洁的响应式状态管理, 与 Antd 组件的受控模式能很好地结合

1.3. C. 本指南的目标和范围

本指南旨在为希望在 ClojureScript 项目中联合使用最新版 Reagent (重点关注 1.2.0 及 1.3.0 版本特性) 和 Ant Design 5.x 的开发者提供一份全面的使用模式, 注意事项和实践指南. 内容将涵盖环境搭建, 核心概念, 组件集成, 样式与主题定制, 高级主题以及最佳实践. 我们将特别关注两者之间的互操作性, 包括 Props 处理, 事件处理, 数据转换以及 React Hooks 的使用.

2. II. 搭建开发环境 (使用 shadow-cljs)

2.1. A. shadow-cljs 简介及其在 ClojureScript 项目中的作用

shadow-cljs 是一个为 ClojureScript 设计的构建工具, 它专注于简化编译流程, 提供与 JavaScript 生态系统(特别是 npm)的无缝集成, 并支持快速的开发迭代周期, 包括热重载和 REPL 支持. 对于 Reagent 和 Ant Design 的联合使用, shadow-cljs 是推荐的构建工具, 因为它能很好地处理 npm 依赖(如 Antd 及其图标库)和 JavaScript 互操作. shadow-cljs 由两部分组成: 一个 Clojure 库负责实际的编译工作, 以及一个 npm 包提供命令行界面.

2.2. B. 项目初始化和依赖配置

  • 安装 shadow-cljs: 可以通过 npm 或 yarn 将 shadow-cljs 添加到项目开发依赖中. 使用 npx create-cljs-project my-project 可以快速创建一个项目骨架.
  • shadow-cljs.edn 配置: 此文件是 shadow-cljs 项目的核心配置文件, 用于定义构建目标, 源路径, 依赖等. 一个基础的配置示例如下:
;; shadow-cljs.edn
{:source-paths ["src"] ;; ClojureScript 源代码目录
 :dependencies [[reagent "1.3.0"] ;; ClojureScript 依赖, 例如 Reagent
                ;; React 依赖由 npm 管理, shadow-cljs 会自动处理
               ]
 :dev-http {8080 "public"} ;; 开发服务器配置, 服务 public 目录下的静态文件
 :builds {:app {:target :browser ;; 构建目标为浏览器
                :output-dir "public/js" ;; JS 输出目录
                :asset-path "/js" ;; 编译后 JS 文件的公共路径
                :modules {:main {:init-fn my.app/init ;; 应用初始化函数
                                 :entries [my.app]}} ;; 入口命名空间
                :devtools {:watch-dir "public"} ;; 监控 public 目录以实现 CSS 热重载
                }}}

Reagent 1.1.0 版本开始, 项目需要自行声明对 React 的依赖, 可以通过 npm. shadow-cljs 项目通常通过 npm 管理 React 依赖. Reagent 1.3.0 的示例已迁移到使用 shadow-cljs.

  • package.json 配置: 用于管理 JavaScript 依赖, 如 React, ReactDOM 和 Ant Design 15.

    // package.json
    {
      "devDependencies": {
        "shadow-cljs": "^2.28.0" // 版本号仅为示例
      },
      "dependencies": {
        "react": "^18.3.1", // Reagent 1.2.0+ 支持 React 18
        "react-dom": "^18.3.1",
        "antd": "^5.19.0", // 使用 Ant Design 5.x 最新版
        "@ant-design/icons": "^5.3.7" // Antd 5.x 图标库
      }
    }
    

安装依赖: npm installyarn install.

2.3. C. Ant Design CSS 和资源处理

Ant Design 5.x 采用 CSS-in-JS 技术, 不再提供整体的 CSS 文件(如 antd/dist/antd.css 已废弃).

  • 基础重置样式: 如果需要重置一些基础样式, 可以引入 antd/dist/reset.css . 最直接的方法是在 public/index.html 文件中通过 <link> 标签引入:

    <head>
      <link rel="stylesheet" href="/node_modules/antd/dist/reset.css" />
      <link rel="stylesheet" href="/js/main.css" />
    </head>
    

    shadow-cljs 本身不直接编译或捆绑 CSS 文件, 它依赖外部工具或直接 HTML 链接来处理 CSS. 对于来自 node_modules 的 CSS 文件, 如 Antd 的 reset.css, 一种常见做法是将其复制到 shadow-cljs 开发服务器可访问的公共目录(例如 public/css), 或者在 index.html 中直接通过相对路径(相对于 node_modules, 如果服务器配置允许访问)或绝对路径(如果已复制到公共服务目录)引用. syn-antd 项目的文档也提到需要在 HTML 中引用相应的 Antd CSS 文件 18. Refine 框架的迁移指南建议, 如果之前使用 @pankod/refine-antd/dist/styles.min.css, 迁移到 Antd 5 时应改为引入 @pankod/refine-antd/dist/reset.css. 这进一步印证了 reset.css 的重要性.

  • 组件级 CSS-in-JS: Antd 5 组件会自动处理自身的样式注入 这意味着通常不需要为每个组件单独引入 CSS.
  • CSS 热重载: shadow-cljs 的开发工具支持 CSS 热重载. 如果样式表被包含在页面中并且在文件系统上被修改, shadow-cljs 可以自动重新加载它. 确保 :devtools {:watch-dir "public"} (或相应的静态资源目录) 配置在 shadow-cljs.edn 中.

3. III. 核心概念回顾

3.1. A. Reagent 核心

3.1.1. Hiccup

Reagent 使用 Hiccup, 一种将 HTML 结构表示为 ClojureScript 向量(vector)的 DSL.

  • 基本结构: [:tag-keyword {:attributes "map"} child1 child2...].
  • 示例: [:div {:class "container"} [:h1 "Hello"][:p "World"]].
  • Reagent 对 Hiccup 进行了一些扩展, 例如 . 用于 class, # 用于 id 的简写(尽管更推荐在属性 map 中明确指定), 以及 > 用于快速嵌套.
  • nil 子节点会被忽略, 便于条件渲染: [:div (when visible? [:p "Visible"])].

3.1.2. Components

在 Reagent 中, 组件通常是返回 Hiccup 的普通 ClojureScript 函数.

  • Form-1 (简单组件): (defn my-component [params][:div "Hello " params]).
  • Form-2 (带局部状态或初始化逻辑的组件): 函数返回一个渲染函数, 外部函数用于一次性设置, 内部函数用于多次渲染.

    (defn timer-component [] ;; Original example was incomplete, this is a common Reagent pattern.
      (let [seconds-elapsed (r/atom 0)] ;; 局部状态
        (fn [] ;; 渲染函数
          (js/setTimeout #(swap! seconds-elapsed inc) 1000)
          ;; Original snippet was missing the Hiccup to render
          ;; For example: [:div "Seconds: " @seconds-elapsed]
          ;; Sticking to the original provided code structure:
          (js/setTimeout #(swap! seconds-elapsed inc) 1000))))
          ;; The number 1 seems to be a reference, not part of the code.
    
  • Form-3 (需要生命周期方法的组件): 使用 reagent.core/create-class 定义, 或通过元数据附加到函数组件上.

    (def component-with-lifecycle
      (with-meta plain-component
        {:component-did-mount (fn [this] (js/console.log "Mounted!"))}))
        ;; The number 2 seems to be a reference.
    

3.1.3. React 18 支持与 reagent.dom.client

Reagent 1.2.0 引入了对 React 18 的实验性支持. 新的 reagent.dom.client 命名空间提供了 React 18 createRoot API 及其相关辅助函数. 而传统的 reagent.dom/render 继续使用旧的 ReactDOM.render, 允许项目继续使用 React 17 的模式. 这意味着如果想利用 React 18 的并发特性, 需要主动使用 reagent.dom.client/create-rootreagent.dom.client/render.

;; 使用 React 18 createRoot API
(ns my.app
  (:require [reagent.dom.client :as rdomc]
            [reagent.core :as r]))

(defn app-root []
  ;; Example content
  [:div "App Root Content"])

(defn init! []
  (let [el (js/document.getElementById "app") ;; Example target element
        root (rdomc/create-root el)] ;; Create root
    (rdomc/render root [app-root]))) ;; Render the component

3.1.4. State Management (Atoms)

Reagent 的核心状态管理机制是 reagent.core/atom (通常称为 ratom).

  • ratom 行为类似 Clojure 的 atom, 但 Reagent 会追踪对其的解引用 (deref@).
  • 任何解引用了 ratom 的组件在其值改变时会自动重新渲染.
  • 示例:

    (def click-count (r/atom 0))
    
    (defn counting-component []
      [:div {:on-click #(swap! click-count inc)}
       "Clicked " @click-count " times."])
       ;; The number 2 seems to be a reference.
    
  • cursor 允许创建指向 ratom 内部某个路径的 "视图", 当 cursor 指向的数据变化时, 仅依赖该 cursor 的组件会更新, 有助于性能优化.

3.1.5. Interop with React

Reagent 构建于 React 之上, 并提供与 React 的互操作机制.

  • reagent.core/adapt-react-class 或其简写 :> 用于在 Hiccup 中使用 React 组件.
  • reagent.core/reactify-component 用于将 Reagent 组件转换为 React 组件, 以便传递给需要 React 组件作为参数的库.
  • reagent.core/as-element 用于将 Hiccup 形式转换为 React 元素, 常用于需要 React 元素作为 prop 的场景.

3.1.6. React Hooks

Reagent 1.0 之后, 通过 :f> 语法糖或配置编译器选项, 可以创建函数式 React 组件, 从而直接使用 React Hooks (如 useState, useEffect 等). 这是与现代 React 生态(包括 Antd 5.x 的许多内部实现)集成的关键.

(ns my.app
  (:require ["react" :as react] ;; 引入 react 库
            [reagent.core :as r]))

(defn hook-example []
  (let [[count setCount] (react/useState 0)]
    [:div
     [:p "Count: " count]
     [:button {:on-click #(setCount (inc count))} "Increment"]]))

(defn app []
  [:f> hook-example]) ;; :f> 表示这是一个函数式组件, 可以使用 hooks

3.1.7. Reagent 1.3.0 的重要变更

  • Breaking Change: :dangerouslySetInnerHTML 的使用方式改变. 现在必须使用 reagent.core/unsafe-html 来标记那些将直接插入 DOM 的 HTML 字符串, 否则 Reagent 会忽略该值. 这是出于安全考虑.

    ;; 旧方法 (Reagent < 1.3.0)
    ;; [:div {:dangerouslySetInnerHTML {:__html "<p>Raw HTML</p>"}}]
    ;; 
    ;; [:div {:innerHTML "<p>Raw HTML</p>"}] ;; Depending on context
    
    ;; 新方法 (Reagent >= 1.3.0)
    (require '[reagent.core :as r])
    [:div {:dangerouslySetInnerHTML {:__html (r/unsafe-html "<p>Raw HTML</p>")}}]
    

    如果 Antd 组件的某些属性间接导致了此类 HTML 的渲染, 并且该 HTML 内容来源于 Reagent, 开发者需要注意此变化.

  • 示例已更新为使用 shadow-cljs.
  • 扩展了 reaction, make-reaction, run! 的文档字符串.
  • 暴露了 clj-kondo 库配置, 使得 reagent.core/with-let 的行为与 clojure.core/let 一致. with-let 宏用于在组件挂载时仅评估一次绑定, 并提供一个 finally 块用于组件卸载时的清理, 是管理组件内部状态和生命周期的有效方式, 可替代部分 useEffect 的场景.

3.2. B. Ant Design 5.x 核心

3.2.1. 设计哲学

  • 自然 (Natural): 追求自然的交互, 降低用户认知成本.
  • 确定 (Certain): 提供高确定性, 低协作熵的界面, 无论是对设计者还是用户.
  • 意义 (Meaningful): 以用户为中心, 创造有意义的人机交互, 助力用户达成目标.
  • 生长 (Growing): 设计具有发展眼光的产品, 促进人机共同成长. 这些设计价值观指导着 Antd 组件库的开发和迭代, 确保了其在企业级应用中的适用性和一致性.

3.2.2. 组件概览

Antd 提供了丰富的组件集, 涵盖通用, 布局, 导航, 数据录入, 数据展示, 反馈等多个方面. 例如: Button, Icon, Grid, Layout, Space, Menu, Dropdown, Form, Input, Select, Table, Modal, Message, Notification 等.

3.2.3. CSS-in-JS

Antd 5.x 的一个核心技术调整是全面采用 CSS-in-JS, 底层使用 @ant-design/cssinjs.

  • 优势: 更好地支持动态主题, 按需加载, 避免全局样式污染, 提升性能(组件级别 CSS-in-JS).
  • 变更: 移除了 Less 文件和 Less 变量的导出; 不再打包 CSS 文件(如 antd/dist/antd.css); babel-plugin-import 不再被支持, 因为 CSS-in-JS 天然支持按需引入.
  • 如果需要重置基础样式, 可以引入 antd/dist/reset.css.
  • 对于不想污染全局样式但又希望原生元素符合 Antd 规范的情况, 可以在最外层使用 App 组件.

3.2.4. Day.js 替换 Moment.js

出于性能和包大小的考虑, Antd 5.x 使用 Day.js 替换了内置的 Moment.js. 如果项目中依赖日期处理, 需要注意此变更并相应调整.

3.2.5. API 变更

  • 弹层类组件的 visible 属性统一为 open (例如 Modal, Drawer, Dropdown, Tooltip).
  • 弹层类组件的类名 API 统一为 popupClassName.
  • 移除了 LocaleProvider, 应使用 <ConfigProvider locale={...} /> .
  • Comment 组件移至 @ant-design/compatible, PageHeader 组件移至 @ant-design/pro-components.
  • BackTop 组件被废弃, 功能合并到 FloatButton.

这些核心概念的理解对于有效结合 Reagent 和 Ant Design 至关重要. 开发者需要意识到, 虽然 Reagent 提供了 ClojureScript 的便利性, 但在与 Antd 这样的 React 组件库交互时 , 本质上还是在与 React 组件打交道, 因此 React 的 props 传递, 事件处理, 生命周期(或 Hooks)等概念依然适用, 只是通过 Reagent 的语法和机制进行表达.

4. IV. Reagent 与 Ant Design 5.x 集成模式与实践

在 ClojureScript 项目中集成 Reagent 和 Ant Design 5.x, 主要依赖于 Reagent 提供的 React 互操作能力. 核心是理解如何将 Ant Design 的 React 组件引入到 Reagent 的 Hiccup 结构中, 并正确处理属性传递, 事件回调和数据转换.

4.1. A. 引入和使用 Ant Design 组件

  • NPM 依赖安装: 首先, 确保已在 package.json 中添加 antd@ant-design/icons (针对 Antd 5.x) 的依赖, 并已运行 npm installyarn install.

    "dependencies": {
      "react": "^18.3.1",
      "react-dom": "^18.3.1",
      "antd": "^5.x.x",
      "@ant-design/icons": "^5.x.x"
    }
    
  • 在 ClojureScript 中 require Ant Design 组件: 使用 shadow-cljs 时, 可以直接 require npm 包中的模块. Ant Design 的组件可以从主包 antd 中按需引入.

    (ns my.app
      (:require ["antd" :as antd] ; 引入 antd 主模块
                ["@ant-design/icons" :as icons] ; 引入图标库
                [reagent.core :as r]))
    

    shadow-cljs 会处理这些 JavaScript 模块的解析和打包 14. Antd 5.x 默认支持 ES模块和 tree shaking, 这意味着只引入使用到的组件代码, 有助于减小打包体积. shadow-cljs 也支持对 JS 依赖的 tree shaking.

  • 使用 :> 语法糖进行组件互操作: Reagent 的 :> 语法糖 (等同于 reagent.core/adapt-react-class) 是在 Hiccup 中使用 React 组件的主要方式.

    (ns my.app
      (:require ["antd" :as antd]
                [reagent.core :as r]))
    (defn my-antd-button []
      [:> antd/Button "Click Me!"] ;; Example, original was empty
     )
    

    这里, antd/Button 是从 antd 主模块中解构出来的 Button 组件.

  • Props 传递 (kebab-case vs camelCase): 当通过 :> 使用 React 组件时, 传递给组件的 props map 中的关键字通常应与 React 组件期望的 prop 名称一致. Ant Design 组件的 props 通常是 camelCase 形式(如 onClick, dataSource, initialValues). Reagent 对标准 HTML 属性和事件处理函数(如 :on-click) 有内置的 kebab-case 到 camelCase 的转换.

    然而, 对于第三方 React 组件(如 Antd)的自定义 props, 最安全的方式是直接使用 React 组件所期望的 camelCase 形式的关键字, 例如 :dataSource:open. Reagent 的属性处理机制通常能正确地将这些关键字属性传递给底层的 React 组件.

    例如, Antd Button 的 onClick prop 在 Reagent 中通常写作 :on-click, Reagent 会处理转换. 但对于像 modalProps 这样的复杂 prop, 或者当不确定时, 使用与 JS 端完全一致的 camelCase 关键字(如 :modalProps) 或字符串键(并配合 clj->js) 是更稳妥的做法.

    reagent.core/create-class 的文档提到, React 组件方法的 map键 可以是 kebab-case 或 camelCase, 这表明 Reagent 内部有一定的转换逻辑. Reagent 官方互操作文档的 FlipMove 示例中, :duration:easing 关键字直接映射到 JS props.

    总的来说, 对于 Antd 组件, 使用其文档中指定的 prop 名称(通常是 camelCase)作为 ClojureScript map 中的关键字通常是有效的.

  • 数据转换 (clj->jsjs->clj): Ant Design 组件的许多 props (如 Table 的 dataSourcecolumns, Menu 的 items, Form 的 initialValuesrules) 需要 JavaScript 对象或数组. 在 ClojureScript 中, 需要使用 clj->js 将 ClojureScript 的 map 和 vector 转换为相应的 JavaScript 数据结构.

    反之, 从 Ant Design 组件的回调函数中接收到的数据 (如事件对象, 表单值) 是 JavaScript 对象/数组, 可能需要使用 js->clj (通常配合 :keywordize-keys true) 转换回 ClojureScript 数据结构以便于处理. 一个常见的误区是 #js 标签不是递归的, 而 clj->js 是递归转换的, 这对于嵌套数据结构至关重要.

    此数据转换过程是确保 ClojureScript 的数据世界与 Ant Design (作为 JavaScript React 库) 的数据世界正确对接的基石. 若忽略了必要的转换, Ant Design 组件可能无法正确解析数据, 导致渲染错误或行为异常. 例如, Table 组件若未收到正确格式的 JavaScript 数组作为 dataSource, 将无法展示数据.

4.2. B. 具体组件使用示例

以下示例将展示如何在 Reagent 中使用一些常用的 Ant Design 5.x 组件.

4.2.1. Button 和 Icon

  • 基础 Button: [:> antd/Button "Click Me"]
  • 带 Icon 的 Button: 首先, 安装并引入 @ant-design/icons.

    (ns my.app
      (:require ["antd" :as antd]
                ["@ant-design/icons" :as icons] ; 引入图标库, 如 icons/SearchOutlined
                [reagent.core :as r]))
    
    (defn button-with-icon []
      [:> antd/Button {:icon [:> icons/SearchOutlined]} "Search"])
    
    (defn another-button-with-icon []
      [:> antd/Button {:icon [:> icons/PlusCircleOutlined] :type "primary"} " Create New Record"])
    
  • 图标的 Tree Shaking: Ant Design 和 @ant-design/icons 本身支持 ES 模块的 tree shaking. shadow-cljs 在构建 release 版本时也会尝试进行 tree shaking.

    为了最大化 tree shaking 的效果, 从而减小最终包体积, 推荐从 @ant-design/icons 中具体导入所需的图标, 而不是导入整个库.

    例如, 在 JavaScript 中会写 import { SmileOutlined } from '@ant-design/icons';. 在 ClojureScript 中, 通过 (:require ["@ant-design/icons" :as icons]) 然后使用 [:> icons/SmileOutlined] 的方式, 依赖于 shadow-cljsicons 这个 JavaScript 模块别名的 tree shaking 能力.

    这通常是有效的. 如果遇到包体积问题, 可以考虑更细粒度的引入, 例如 (:require ["@ant-design/icons/es/icons/SmileOutlined$default" :as SmileOutlined]) (具体路径需查证 antd 结构), 这样引入的是图标的默认导出.

4.2.2. Modal: 状态管理 (r/atom), 回调 (onOk, onCancel)

使用 Reagent atom 控制 Modal 的可见性. Antd 5.x 中 Modal 的可见性属性是 open.

(ns my.app
  (:require [reagent.core :as r]
            ["antd" :as antd]))

(def modal-visible (r/atom false))

(defn handle-ok [e]
  (js/console.log "OK clicked")
  (reset! modal-visible false))

(defn handle-cancel [e]
  (js/console.log "Cancel clicked")
  (reset! modal-visible false))

(defn my-modal-component []
  [:div
   [:> antd/Button {:on-click #(reset! modal-visible true)} "Open Modal"]
   [:> antd/Modal {:title "My Modal Title"
                   :open @modal-visible
                   :onOk handle-ok
                   :onCancel handle-cancel}
    [:p "Modal Content Line 1"]
    [:p "Modal Content Line 2"]]])

此模式借鉴了 Reagent 中常见的状态管理方式 并适配了 Antd 5.x Modal 的 API.

4.2.3. Forms

  • 受控 Antd Input 与 Reagent Atoms: 使用 r/atom 管理输入框的值, 通过 :value:onChange props 实现双向绑定.

    (ns my.app
      (:require [reagent.core :as r]
                ["antd" :as antd]))
    (def name-val (r/atom ""))
    
    (defn my-input []
      [:> antd/Input
       {:placeholder "Enter name"
        :value @name-val
        :onChange #(reset! name-val (.. % -target -value))}])
    

这是 Reagent 中处理表单输入的基础模式, 也适用于 Antd 的 Input 组件.

  • 使用 Form.useForm Hook 实现高级表单 (在 Reagent :f> 组件中): Antd 5.x 的 Form 组件推荐使用 Form.useForm() hook 来进行表单状态管理, 校验和提交. 要在 Reagent 中使用 React Hooks, 组件需要以 :f> 形式定义.

    (ns my.app
      (:require ["react" :as react] ; 必须引入 react 才能使用 hooks
                ["antd" :as antd]
                [reagent.core :as r] ; Ensure reagent.core is aliased
                [cljs.core :refer [clj->js js->clj]]))
    
    (defn login-form-comp []
      ;; 直接在函数式组件内部调用 useForm
      (let [[form-instance] (antd/Form.useForm) ; form-instance 在组件重渲染间保持稳定
            handle-finish (fn [values]
                            (js/console.log "Success:" (js->clj values :keywordize-keys true)))]
        [:> antd/Form {:form form-instance ; 将 form 实例传递给 Form 组件
                       :name "login"
                       :initialValues (clj->js {:remember true}) ; 初始值需 clj->js
                       :onFinish handle-finish} ; 提交回调
         [:> antd/Form.Item {:label "Username"
                             :name "username" ; 字段名
                             :rules (clj->js [{:required true ; 校验规则需 clj->js
                                              :message "Please input your username!"}])}
          [:> antd/Input]]
         [:> antd/Form.Item {:label "Password"
                             :name "password"
                             :rules (clj->js [{:required true
                                              :message "Please input your password!"}])}
          [:> antd/Input.Password]]
         [:> antd/Form.Item
          [:> antd/Button {:type "primary" :htmlType "submit"}
           "Log in"]]]))
    
    (defn app []
      [:f> login-form-comp]) ; 使用 :f> 使得 login-form-comp 可以使用 hooks
    

Form.useForm 返回一个包含 form 实例的数组, 因此使用 (let [[form-instance] (antd/Form.useForm)]...) 进行解构. 这个 form-instance 是稳定且可以在多次渲染中使用的.

此模式是 Reagent 应用现代 React 表单库(如 Antd Form)的关键, 它允许 ClojureScript 开发者利用 Antd 强大的表单功能, 同时保持 Reagent 的函数式风格.

  • 校验规则 (rules): 在 Form.Item:rules prop 中传递校验规则数组. 这些规则对象需要通过 clj->js 转换为 JavaScript 数组和对象. 例如: :rules (clj->js [{:required true :message "此字段为必填项"} {:min 5 :message "至少需要5个字符"}]).

4.2.4. Table

  • 数据绑定 (dataSource, columnsclj->js): Table 的 :dataSource prop 需要一个 JavaScript 对象数组, 每个对象代表一行数据且必须有唯一的 key 属性. :columns prop 也需要一个 JavaScript 对象数组, 定义列的结构和行为. 这两者都需从 ClojureScript 数据结构通过 (clj->js...) 转换.
  • 使用 r/as-element 进行列的自定义渲染: 列定义中的 render 函数接收 (text, record, index) 三个参数, 其中 text 是当前单元格的值, record 是当前行的数据对象 (JS 对象), index 是行索引 64. 此函数应返回一个 React 节点. 如果想在单元格中渲染 Reagent 组件或 Hiccup, 必须使用 reagent.core/as-element 将其转换为 React 元素.

    (ns my.app
      (:require [reagent.core :as r]
                ["antd" :as antd]
                ["@ant-design/icons" :as icons]
                [cljs.core :refer [clj->js js->clj]]))
    
    ;; 一个简单的 Reagent 组件作为单元格渲染器
    (defn action-buttons [record-clj] ; record-clj 是转换后的 ClojureScript map
      [:<> ;; Use a fragment or a span
       [:> antd/Button {:type "link"
                        :on-click #(.log js/console "Edit" record-clj)}
        "Edit"]
       [:> antd/Button {:type "link" :danger true
                        :on-click #(.log js/console "Delete" record-clj)}
        "Delete"]]) ;; Original had "Delete"]]", assuming typo corrected
    
    (def table-columns
      (clj->js ; 整个列定义数组需要转换
       [{:title "Name" :dataIndex "name" :key "name"}
        {:title "Age" :dataIndex "age" :key "age"}
        {:title "Address" :dataIndex "address" :key "address"}
        {:title "Action"
         :key "action"
         :render (fn [text record index] ; record 是 JS 对象
                   (r/as-element [action-buttons (js->clj record :keywordize-keys true)]))}]))
                   ;; Original had "]))" which seems like an extra paren
    
    (def table-data
      (clj->js ; dataSource 需要是 JS 对象数组
       [{:key "1" :name "John Brown" :age 32 :address "New York No. 1 Lake Park"}
        {:key "2" :name "Jim Green" :age 42 :address "London No. 1 Lake Park"}
        {:key "3" :name "Joe Black" :age 32 :address "Sidney No. 1 Lake Park"}]))
    
    (defn my-table []
      [:> antd/Table {:dataSource table-data :columns table-columns}])
    

这个 (r/as-element...) 的使用是至关重要的. Antd 的 Table 组件(或其他任何接受渲染函数的 React 组件)期望 render 函数返回一个 React 可以直接处理的节点. Reagent 组件本身返回的是 Hiccup 数据结构, 或者它就是一个函数. r/as-element 充当了桥梁, 将 Reagent 的世界(Hiccup)转换到 React 的世界(React 元素). 不使用它, Antd Table 将无法正确渲染自定义的 Reagent 内容.

4.2.5. Menu 和 Navigation

Antd 5.x 的 Menu 组件改用 items prop 来定义菜单项, 取代了之前直接嵌套 Menu.ItemMenu.SubMenu 的方式 items prop 需要一个特定结构的 JavaScript 对象数组.

(ns my.app
  (:require [reagent.core :as r]
            ["antd" :as antd]
            ["@ant-design/icons" :as icons]
            [cljs.core :refer [clj->js js->clj]]))

(def menu-items
  (clj->js
   [{:key "mail"
     :icon (r/as-element [:> icons/MailOutlined]) ; 图标也需要是 React 元素
     :label "Navigation One"}
    {:key "app"
     :icon (r/as-element [:> icons/AppstoreOutlined])
     :label "Navigation Two"}
    {:key "sub1"
     :label "Navigation Three - Submenu"
     :icon (r/as-element [:> icons/SettingOutlined]) ;; Original had empty (r/as-element)
     :children [{:type "group" ; 分组类型
                 :label "Item 1 Group"
                 :children [{:key "setting:1" :label "Option 1"}
                            {:key "setting:2" :label "Option 2"}]}
                {:type "group"
                 :label "Item 2 Group"
                 :children [{:key "setting:3" :label "Option 3"}
                            {:key "setting:4" :label "Option 4"}]}]}]))

(defn my-menu []
  [:> antd/Menu {:mode "inline"
                 :theme "dark"
                 :items menu-items ; 传递转换后的 items
                 :onClick (fn [info]
                            (let [key (.-key info)
                                  keyPath (js->clj (.-keyPath info))]
                              (js/console.log "Clicked menu item:" key keyPath)))}])

注意, 如果菜单项的 iconlabel 是复杂的 Reagent 组件, 它们也需要通过 r/as-element 转换. Antizer 库的示例 60 可能展示的是旧版 Antd Menu API, 使用 Antd 5.x 时务必参考其最新文档关于 items prop 的用法.

4.3. C. 在 Reagent :f> 组件中使用 React Hooks (例如 useEffect, useState) 与 Antd 交互

Reagent 的 :f> 语法是使用 React Hooks 的前提 4. 这使得可以在 Reagent 组件中利用如 useState, useEffect, useContext 等标准 React Hooks 25. 这对于管理与 Antd 组件相关的局部状态或副作用非常有用, 特别是当 Antd 组件本身也依赖 Hooks 时(例如 Antd 5.x 的 message.useMessage 72 或 Form.useForm).

(ns my.app
  (:require ["react" :as react] ; 引入 react
            ["antd" :as antd]
            [reagent.core :as r]
            [cljs.core :refer [js->clj]])) ; Added js->clj

(defn data-fetcher-component [{:keys [item-id]}]
  (let [[loading setLoading] (react/useState true)
        [error setError] (react/useState nil)
        [data setData] (react/useState nil)] ; Corrected from original for valid let binding

    (react/useEffect
     (fn [] ; effect 函数
       (setLoading true)
       (-> (js/fetch (str "https://api.example.com/items/" item-id))
           (.then #(if (.-ok %) (.json %) (js/Promise.reject (.-statusText %))))
           (.then (fn [fetched-data]
                    (setData (js->clj fetched-data :keywordize-keys true))
                    (setLoading false)))
           (.catch (fn [err]
                     (setError (str err))
                     (setLoading false))))
       ;; 清理函数 (可选)
       (fn [] (.log js/console (str "Cleaning up effect for item-id: " item-id))))
     #js [item-id]) ; 依赖数组, 当 item-id 变化时重新执行 effect

    (cond
      loading [:> antd/Spin {:tip "Loading..."}] ; Changed to Spin for loading state
      error [:> antd/Alert {:message "Error" :description error :type "error"}]
      data [:div
            [:h3 "Fetched Data:"]
            [:pre (pr-str data)]]
      :else [:p "No data"])))

(defn app []
  [:f> data-fetcher-component {:item-id "123"}])

这种模式使得 Reagent 应用能够充分利用 React 生态中的现代实践. Antd 5.x 自身广泛使用 Hooks, 例如 Form.useFormmessage.useMessage. 为了与这些 API 无缝集成, 并在 ClojureScript 中保持一致的开发体验, Reagent 的 :f> 组件成为了不可或缺的工具. 它不仅是技术上的桥梁, 更是理念上的融合, 使得 Reagent 应用在处理复杂状态和副作用时, 能更贴近主流 React 开发范式.

4.4. 表格 4: Ant Design 组件集成速查表

为了方便开发者快速查阅 Reagent 与 Ant Design 组件集成的常用模式, 下表总结了部分核心组件的用法:

Antd 组件 Reagent 互操作语法 关键 Props (CLJS 关键字) clj->js 是否必须 (针对 Props) 注意事项
Button [:> antd/Button ... ] :type, :onClick, :icon, :loading, :disabled, :danger 简单 Props 通常不需要 Icon 可以是 [:> icons/SomeIcon]
Modal [:> antd/Modal ... ] :open, :title, :onOk, :onCancel, :footer :footer (若为复杂节点) 使用 r/atom 控制 :open 状态.
Form [:> antd/Form ... ] :form (form-instance), :onFinish, :initialValues :initialValues, rules :f> 组件中使用 Form.useForm 获取 form-instance.
Form.Item [:> antd/Form.Item ... ] :name, :label, :rules :rules 必须是 Form 的子组件.
Input [:> antd/Input ... ] :value, :onChange, :placeholder, :disabled 简单 Props 通常不需要 若使用 r/atom 实现受控输入, 需管理 :value:onChange.
Table [:> antd/Table ... ] :dataSource, :columns, :rowSelection, :pagination :dataSource, :columns columns 中的 render 函数若返回 Reagent 组件/Hiccup, 需用 (r/as-element...) 包装.
Menu [:> antd/Menu ... ] :items, :mode, :theme, :onClick, :selectedKeys :items Antd 5.x 中 :items 是一个对象数组.
Icon (from @ant-design/icons) [:> icons/SomeIcon] :style, :spin, :twoToneColor 简单 Props 通常不需要 引入方式如 (:require ["@ant-design/icons" :as icons]) 或引入特定图标.
ConfigProvider [:> antd/ConfigProvider ... ] :theme, :locale :theme, :locale (若复杂) 包裹应用顶层以实现全局主题和国际化配置.

此表格为开发者提供了一个关于如何在 Reagent 项目中集成最常用 Antd 组件的快速参考, 涵盖了基本语法, 关键属性以及数据转换的需求, 旨在加速开发进程并减少常见错误.

5. V. Reagent-Antd 项目中的样式与主题定制

Ant Design 5.x 引入了 CSS-in-JS 作为其主要的样式方案, 这为主题定制和样式管理带来了新的模式.

5.1. A. 理解 Ant Design 的 CSS-in-JS (@ant-design/cssinjs)

Ant Design 5.x 使用 @ant-design/cssinjs 库来动态生成和注入组件样式. 这意味着组件的样式是在运行时通过 JavaScript 创建的, 而不是依赖预编译的 CSS 文件.

  • 优点:
    • 动态主题: 可以轻松实现主题的动态切换和细粒度定制.
    • 按需加载: 只会加载实际使用到的组件的样式, 减少了不必要的 CSS 体积.
    • 作用域隔离: 组件样式具有局部作用域, 减少了全局样式冲突的风险.
    • 性能: 组件级别的 CSS-in-JS 解决方案有助于提升渲染性能.
  • 覆盖样式:
    • 通过 ConfigProvidertheme 属性进行主题级别的定制(详见 V.C).
    • 使用 @ant-design/cssinjs 提供的 StyleProvider 进行更高级的控制, 例如调整 hashPriority 来管理 CSS 选择器的特异性, 或使用 transformers 73. Antd 5 默认使用 :where 选择器来降低其样式的特异性, 使得用户自定义样式更容易覆盖 73.
    • 直接针对 Antd 生成的类名进行覆盖(可以通过浏览器开发者工具查看)虽然可行, 但可能因版本更新而变得脆弱.

5.2. B. 全局样式: antd/dist/reset.cssStyleProvider

5.2.1. antd/dist/reset.css

为了保证跨浏览器的一致性和基础样式的重置, Antd 5.x 仍然提供了 reset.css 文件 8. 如第二部分所述, 应在项目的 index.html 中引入此文件 18.

<link rel="stylesheet" href="path/to/your/node_modules/antd/dist/reset.css" />

或者, 更常见的做法是将此文件复制到 shadow-cljs 开发服务器可访问的公共目录(例如项目的 public/css/ 目录), 然后在 index.html 中引用.

5.2.2. StyleProvider

@ant-design/cssinjs 中的 StyleProvider 组件允许对 CSS-in-JS 的行为进行配置.

  • hashPriority: 可以设置为 "high" 来增加 Antd 生成样式的特异性(默认是 "low", 使用 :where), 这在需要兼容不支持 :where 的旧浏览器或解决特异性冲突时有用.
  • transformers: 可以用于在样式注入前对其进行转换, 例如 legacyLogicalPropertiesTransformer 用于兼容旧浏览器的逻辑属性, 或 px2remTransformer 用于将 px 单位转换为 rem 单位.
  • container: 在 Shadow DOM 场景下, 可以指定样式注入的容器 73.
  • layer: Antd 5.17.0+ 支持配置 @layer 来统一降低 CSS 优先级, 便于用户覆盖样式 73.

在 Reagent 中使用 StyleProvider:

(ns my.app
  (:require ["@ant-design/cssinjs" :as cssinjs] ; For StyleProvider
            ["antd" :as antd]
            [reagent.core :as r]))

(defn my-app-with-style-provider []
  [:> cssinjs/StyleProvider {:hashPriority "high"}
   ;; Your application components go here
   [:div "My App Content"]]
  )

5.3. C. 使用 ConfigProvider 进行高级主题定制

ConfigProvider 是 Ant Design 中用于全局配置(包括主题和国际化)的核心组件.

5.3.1. theme Prop

ConfigProvider:theme prop 接受一个主题对象, 用于定制应用的整体外观和感觉.

  • 主题对象结构: 该主题对象主要包含两个顶级键:
    • token: 一个包含全局设计令牌 (design tokens) 的 map. 这些令牌控制着基础样式, 如主色调 (:colorPrimary), 圆角大小 (:borderRadiusLG), 字体 (:fontFamily) 等.
    • components: 一个 map, 允许对特定组件的样式进行覆盖. 键是组件名 (如 :Button, :Modal), 值是该组件的特定令牌配置.
  • 在 Reagent 中使用并进行 clj->js 转换: 在 ClojureScript 中定义主题时, 通常会使用 ClojureScript 的 map 结构. 由于 Antd 的 ConfigProvider~期望一个 JavaScript 对象作为 ~theme prop 的值, 因此必须使用 clj->js 进行递归转换.
(ns my.app
  (:require [reagent.core :as r]
            ["antd" :as antd]
            [cljs.core :refer [clj->js]]))

(def my-custom-theme
  (clj->js ; 必须进行转换
   {:token {:colorPrimary "#52c41a" ; 修改主色调
            :borderRadiusLG 12     ; 修改大圆角
            :fontFamily "Georgia, serif"}
    :components {:Button {:colorPrimary "#1890ff" ; 单独覆盖 Button 的主色调
                          :fontWeight "bold"
                          :controlHeightLG 48} ; 大号按钮高度
                 :Modal {:titleFontSize 22
                         :headerBg "#f0f2f5"}}}))

(defn app-with-custom-theme []
  [:> antd/ConfigProvider {:theme my-custom-theme}
   [:div
    [:> antd/Button {:type "primary"} "Custom Themed Button"]
    ;;... 其他组件将继承或使用 ConfigProvider 定义的主题...
    ]])

这种通过 ConfigProviderclj->js 进行主题定制的方式, 是 Reagent 应用充分利用 Antd 5.x 强大主题能力的标准途径. 忘记 clj->js 的转换会导致主题配置无效, 因为 Antd 无法理解 ClojureScript 的原生数据结构. 这突显了在进行 JavaScript 互操作时, 数据结构转换的重要性, 尤其是在处理如主题这样复杂的嵌套配置对象时.

6. VI. 高级主题与最佳实践

6.1. A. 性能考量

6.1.1. Reagent 的渲染机制

  • Reagent 本身因其基于 ClojureScript 不可变数据结构的高效比较, 默认情况下性能良好 2. 组件仅在其依赖的数据(ratom, props)发生变化时才重新渲染.
  • 对于动态生成的组件列表, 务必为每个列表项提供唯一的 :key prop, 这有助于 React 进行高效的 DOM diff 和更新.
  • 在 props 中传递匿名函数时需谨慎, 因为每次父组件渲染时都可能创建新的函数实例, 导致子组件不必要的重渲染. 在 :f> 组件中, 可以使用 react/useCallback 来记忆化回调函数, 确保其引用稳定性.

6.1.2. Antd Tree Shaking 与 shadow-cljs

  • Ant Design 5.x 及其图标库 @ant-design/icons 被设计为支持 tree shaking, 这意味着现代构建工具(如 Webpack, shadow-cljs 底层也可能利用类似机制处理 JS 模块)可以移除未使用的代码, 从而减小最终应用的包体积.
  • shadow-cljs 对 npm 依赖也支持 tree shaking 19. 在构建生产版本时 (通常是 shadow-cljs release app), shadow-cljs 会尝试优化 JavaScript 依赖.
  • 为了获得最佳的 tree shaking 效果, 推荐从 antd@ant-design/icons 中按需导入具体组件或图标, 而不是导入整个库. 例如, 在 JavaScript 中 import { Button } from 'antd';. 在 ClojureScript 中, 当使用 ["antd" :refer [Avatar Button DatePicker Input Layout Menu Modal Pagination Space Table Tag Typography]] 时, shadow-cljs 通常能够有效地进行 tree shaking. 如果遇到问题, 可以考虑更细粒度的导入, 例如 (:require ["antd/es/button$default" :as AntButton]) (路径可能因 antd 内部结构而异, 需查证). syn-antd 项目的初衷之一就是为了解决早期 Antd 版本与 ClojureScript tree shaking 的问题, 但 Antd 5.x 本身在这方面已有很大改进.

Author: 青岛红创

Created: 2025-11-02 Sun 10:43