1. 项目概述:一个现代、高效的ClojureScript前端框架

如果你和我一样,在ClojureScript生态里摸爬滚打了好些年,从最初的惊喜到后来面对复杂前端状态管理时的头疼,那么看到 bookedsolidtech/reagent 这个项目时,你大概会和我有同样的感觉:一种久违的清爽感。这不仅仅是一个普通的Reagent组件库,它更像是一个经过深思熟虑的、面向现代Web应用开发的 前端框架解决方案 。它基于Reagent这个ClojureScript世界最受欢迎的React包装器,但提供了一套更完整、更结构化的开发范式。

简单来说, reagent (这里指 bookedsolidtech/reagent 这个特定发行版或增强集合)旨在解决我们在使用原生Reagent开发中大型应用时遇到的典型痛点:状态管理分散、组件间通信复杂、副作用处理不够优雅、以及缺乏一套公认的最佳实践。它没有试图推翻Reagent的哲学,而是选择在其坚实的基础上,通过提供一系列精心设计的工具、模式和约定,让开发者能够更高效、更自信地构建可维护的前端应用。它特别适合那些已经认可ClojureScript函数式、不可变数据流魅力,但希望项目结构能更清晰、团队协作能更顺畅的开发者。

2. 核心设计理念与架构拆解

2.1 拥抱“单一数据源”与可预测的状态流

Reagent的核心是 ratom (响应式原子),它非常轻量且强大。但在大型应用中,如果每个组件都随意创建和管理自己的 ratom ,状态很快就会变得支离破碎,数据流难以追踪。 bookedsolidtech/reagent 框架的一个核心设计理念,就是强烈倡导并简化 单一数据源 单向数据流 的实现。

它通常提供一个顶层的 app-state 原子,或者一个更结构化的状态容器(可能基于 re-frame db 概念或自研的store模式)。所有应用状态都集中存储于此。组件不再直接修改遥远的兄弟组件的状态,而是通过 定义良好的事件(Events)或命令(Commands) 来触发状态变更。这些变更处理器(Handler)是纯函数,接收当前状态和一个载荷(payload),返回新的状态。这种模式带来了巨大的好处:状态变更变得可预测、可追溯、易于测试。你可以清晰地回答“这个按钮点击后,整个应用状态究竟发生了什么变化?”。

注意 :这并不意味着完全禁止组件本地状态。对于纯粹的UI状态(如一个下拉菜单是否展开),使用本地 ratom 是完全合理且高效的。框架提供的是管理 应用级状态 的更好方式。

2.2 组件模型:函数即组件,但更有组织

Reagent信奉“函数即组件”,一个返回Hiccup向量的函数就是一个组件。 bookedsolidtech/reagent 继承了这一思想,但通过约定和工具使其更具组织性。

首先,它鼓励将组件分为 展示组件 容器组件 。展示组件是“纯”的,它们只关心如何渲染,通过参数(props)接收数据和回调函数,内部没有状态,也不直接触发副作用。容器组件则负责“连接”状态和展示组件:它们订阅(subscribe)全局状态中的特定部分,并将状态片段和事件派发(dispatch)函数作为props传递给展示组件。这种分离极大地提升了组件的可复用性和可测试性。

其次,框架可能会提供一套标准的组件生命周期钩子接入方式,或者对Reagent的 reaction (用于从 ratom 派生计算值)进行更友好的封装,使得创建依赖于动态状态的组件逻辑变得更简洁。

2.3 副作用管理的标准化

副作用(如HTTP请求、访问浏览器本地存储、设置定时器)是前端开发无法回避的部分。在普通的Reagent中,副作用常常被随意地放在事件处理器或组件生命周期函数中,这会导致代码难以理解和测试。

bookedsolidtech/reagent 框架通常会引入一个明确的 副作用管理层 。借鉴 re-frame effects coeffects 概念,或者采用更简单的 command-handler 模式,它将副作用的描述与执行分离开来。事件处理器不再直接执行 fetch ,而是返回一个描述“需要发起一个获取用户数据的HTTP请求”的效果描述。然后,一个独立的 效果处理器 会解释并执行这个描述。这样做的好处是:

  1. 可测试性 :事件处理器是纯函数,只返回数据描述,极易测试。
  2. 可替换性 :你可以为测试环境提供一个模拟的效果处理器,而不需要修改业务逻辑。
  3. 可追溯性 :所有发起的副作用都在一个中心点被记录和管理。

2.4 路由与模块化的深度集成

现代单页应用离不开路由。 bookedsolidtech/reagent 框架通常会与某个ClojureScript路由库(如 bidi reitit )进行深度集成,或者提供自己的路由解决方案。关键不在于路由库本身,而在于框架如何将路由状态融入统一的状态管理,以及如何优雅地组织与路由对应的视图模块。

它可能提供一种机制,使得路由变化能自动触发特定的事件,从而加载对应模块的数据和组件。同时,它鼓励按功能或路由进行代码分割,利用ClojureScript的 require 动态加载能力,实现应用的懒加载,优化首屏性能。

3. 从零开始:构建一个基于此框架的待办事项应用

理论说得再多,不如动手实践。让我们通过构建一个经典的待办事项(TodoMVC)应用,来具体感受 bookedsolidtech/reagent 框架的威力。假设我们使用的是一个类似 re-frame 但更轻量或定制化的框架结构。

3.1 项目初始化与依赖配置

首先,你需要一个标准的ClojureScript项目环境。使用 lein (Leiningen)或 deps.edn (Clojure CLI工具)创建项目。在 project.clj deps.edn 中,你需要引入核心依赖:

;; project.clj 示例
(defproject my-todo-app "0.1.0-SNAPSHOT"
  :dependencies [[org.clojure/clojure "1.11.1"]
                 [org.clojure/clojurescript "1.11.60"]
                 [reagent "1.2.0"] ; Reagent核心
                 ;; 假设 bookedsolidtech/reagent 提供的是一个包含状态管理工具的包
                 ;; 这里可能需要一个具体的坐标,例如:
                 ;; [bookedsolidtech/reagent-core "0.1.0"]
                 ;; 为演示,我们假设其模式,并可能需要其他辅助库
                 [cljs-http "0.1.46"] ; HTTP客户端(用于副作用示例)
                 [com.andrewmcveigh/cljs-time "0.5.2"]] ; 时间处理
  :plugins [[lein-cljsbuild "1.1.8"]
            [lein-figwheel "0.5.20"]] ; 用于热重载开发
  :cljsbuild {:builds [{:id "dev"
                        :source-paths ["src"]
                        :figwheel true
                        :compiler {:main my-todo-app.core
                                   :asset-path "js/out"
                                   :output-to "resources/public/js/main.js"
                                   :output-dir "resources/public/js/out"
                                   :optimizations :none
                                   :source-map true}}]})

关键点在于,你需要明确 bookedsolidtech/reagent 具体提供了哪些 jar 包。它可能是一个元包(umbrella package),也可能是一系列独立库的集合(如 reagent-store reagent-router 等)。你需要根据其文档,引入正确的依赖。

3.2 定义应用状态(Store)与事件(Event)

src/my_todo_app/core.cljs 中,我们首先定义应用的状态结构和变更事件。

(ns my-todo-app.core
  (:require [reagent.core :as r]
            ;; 假设框架提供了 `create-store` 和 `reg-event` 等函数
            ;; [bookedsolidtech.reagent.store :as store]
            ;; 为演示,我们模拟一个简单的实现
            ))

;; 1. 定义状态结构
(def initial-state
  {:todos []          ;; 列表,每个todo是 {:id ... :text ... :done? ...}
   :filter :all       ;; :all, :active, :completed
   :next-id 1})       ;; 用于生成唯一ID

;; 2. 创建响应式应用状态存储 (Store)
;; (def app-store (store/create-store initial-state))
;; 模拟:我们用一个普通的 ratom 来模拟 store 的核心
(def app-state (r/atom initial-state))

;; 3. 定义事件类型和事件处理器
;; 框架通常会提供 `reg-event` 宏来关联事件关键字和处理器函数
;; 这里我们模拟一个简单的事件系统
(def event-handlers (atom {}))

(defn reg-event [event-key handler-fn]
  (swap! event-handlers assoc event-key handler-fn))

(defn dispatch [event-key payload]
  (if-let [handler (get @event-handlers event-key)]
    (swap! app-state #(handler % payload))
    (js/console.error "No handler registered for event:" event-key)))

;; 4. 注册具体的事件
;; 添加待办事项
(reg-event :add-todo
  (fn [state {:keys [text]}]
    (update state :todos conj
            {:id (:next-id state)
             :text text
             :done? false})))

;; 切换待办事项完成状态
(reg-event :toggle-todo
  (fn [state {:keys [id]}]
    (update state :todos
            (fn [todos]
              (mapv #(if (= (:id %) id)
                       (update % :done? not)
                       %)
                    todos)))))

;; 删除待办事项
(reg-event :delete-todo
  (fn [state {:keys [id]}]
    (update state :todos
            (fn [todos]
              (filterv #(not= (:id %) id) todos)))))

;; 更改过滤条件
(reg-event :set-filter
  (fn [state {:keys [filter]}]
    (assoc state :filter filter)))

这个模拟展示了框架的核心:一个中心化的状态原子和一套基于事件的状态更新机制。真实框架中的 reg-event dispatch 会更加健壮,可能支持中间件(用于日志、异步操作等)。

3.3 构建展示组件与容器组件

接下来,我们创建组件。首先是最基础的待办事项单项组件,它是一个纯展示组件。

;; src/my_todo_app/components/todo_item.cljs
(ns my-todo-app.components.todo-item
  (:require [reagent.core :as r]))

(defn todo-item [{:keys [id text done?] :as todo} on-toggle on-delete]
  [:li {:class (when done? "completed")}
   [:div.view
    [:input.toggle
     {:type "checkbox"
      :checked done?
      :on-change #(on-toggle id)}]
    [:label text]
    [:button.destroy
     {:on-click #(on-delete id)}]]])

注意,这个组件只接收数据( todo )和回调函数( on-toggle , on-delete ),它自己不知道这些回调具体做什么,也不知道状态存储在哪里。这使其非常易于测试和复用。

然后,我们创建主要的应用组件,它是一个容器组件,负责连接状态和展示。

;; src/my_todo_app/views/main_panel.cljs
(ns my-todo-app.views.main-panel
  (:require [reagent.core :as r]
            [my-todo-app.core :as core] ; 导入 dispatch 函数
            [my-todo-app.components.todo-item :refer [todo-item]]
            [my-todo-app.components.todo-footer :refer [todo-footer]] ; 假设有页脚组件
            [my-todo-app.subscriptions :as subs])) ; 导入订阅函数

(defn main-panel []
  (let [todos @(subs/filtered-todos) ; 订阅过滤后的待办列表
        filter-type @(subs/current-filter) ; 订阅当前过滤类型
        active-count @(subs/active-todo-count)] ; 订阅活跃待办数
    [:div
     [:section.todoapp
      [:header.header
       [:h1 "todos"]
       [:input.new-todo
        {:placeholder "What needs to be done?"
         :auto-focus true
         :on-key-down (fn [e]
                        (when (= (.-key e) "Enter")
                          (let [val (-> e .-target .-value .trim)]
                            (when-not (empty? val)
                              (core/dispatch :add-todo {:text val})
                              (-> e .-target (set! ""))))))}]]
      [:section.main
       [:input#toggle-all.toggle-all {:type "checkbox"}]
       [:label {:for "toggle-all"} "Mark all as complete"]
       [:ul.todo-list
        (for [todo todos]
          ^{:key (:id todo)} ; React列表渲染需要key
          [todo-item todo
           #(core/dispatch :toggle-todo {:id %})
           #(core/dispatch :delete-todo {:id %})])]]
      [todo-footer filter-type active-count]]]))

在这个容器组件中,我们使用了 subs/filtered-todos 订阅(Subscription) 。订阅是框架提供的另一个核心概念,它允许组件声明式地依赖状态的某一部分。当这部分状态变化时,组件会自动重新渲染。

3.4 实现订阅(Subscriptions)

订阅是派生状态(Derived State)的声明方式。它们通常是纯函数,从全局状态中计算并返回一个值。框架会高效地缓存这些计算结果,只在依赖的状态变化时才重新计算。

;; src/my_todo_app/subscriptions.cljs
(ns my-todo-app.subscriptions
  (:require [reagent.core :as r]
            [my-todo-app.core :as core]))

;; 框架通常会提供 `reg-sub` 宏来创建订阅
;; 这里我们模拟一个简单的订阅系统
(def subscriptions (atom {}))

(defn reg-sub [sub-key computation-fn]
  (swap! subscriptions assoc sub-key computation-fn))

(defn subscribe [sub-key]
  (let [computation (get @subscriptions sub-key)
        state (r/cursor core/app-state [])] ; 获取整个状态
    ;; 创建一个 reaction,当 computation 的结果变化时,触发重新计算
    (r/reaction (computation @state))))

;; 注册具体的订阅
;; 所有待办事项
(reg-sub :all-todos
  (fn [state] (:todos state)))

;; 当前过滤条件
(reg-sub :current-filter
  (fn [state] (:filter state)))

;; 活跃的待办事项数量
(reg-sub :active-todo-count
  (fn [state]
    (count (filter (comp not :done?) (:todos state)))))

;; 根据过滤条件筛选后的待办事项
(reg-sub :filtered-todos
  (fn [_]
    ;; 订阅可以依赖其他订阅!
    (let [all-todos (subscribe :all-todos)
          current-filter (subscribe :current-filter)]
      (r/reaction
        (let [todos @all-todos
              filt @current-filter]
          (case filt
            :active (filter (comp not :done?) todos)
            :completed (filter :done? todos)
            :all todos))))))

;; 提供便捷函数给组件使用
(defn filtered-todos [] (subscribe :filtered-todos))
(defn current-filter [] (subscribe :current-filter))
(defn active-todo-count [] (subscribe :active-todo-count))

通过订阅系统,组件 main-panel 不再直接读取 app-state 的原始结构,而是通过声明式的订阅函数获取已经过计算和筛选的数据。这实现了数据层与视图层的解耦,也使得复杂的派生状态逻辑可以被集中管理和复用。

3.5 处理异步副作用:加载远程待办事项

现在,让我们引入一个常见的副作用:从服务器加载初始待办事项列表。这展示了框架如何处理异步操作。

首先,我们扩展事件系统以支持异步事件(或效果)。一个常见的模式是,事件处理器可以返回一个“效果描述”,而不是直接修改状态。

;; 在 core.cljs 中扩展
(reg-event :load-todos-request
  (fn [state _]
    ;; 请求开始时,可以设置一个加载状态
    (assoc state :loading? true)))

(reg-event :load-todos-success
  (fn [state {:keys [todos]}]
    (-> state
        (assoc :loading? false)
        (assoc :todos todos)
        (assoc :next-id (inc (apply max 0 (map :id todos))))))) ; 更新下一个ID

(reg-event :load-todos-failure
  (fn [state {:keys [error]}]
    (-> state
        (assoc :loading? false)
        (assoc :error error))))

;; 框架的“效果处理器”会监听类似 `:http-get` 的效果
;; 我们模拟一个处理异步事件的“效果处理器”中间件
(defn async-event-middleware [handler]
  (fn [state event]
    (let [result (handler state event)]
      (if (and (vector? result) (= (first result) :async-effect))
        ;; 如果是异步效果描述,则执行它,并立即返回更新后的状态(通常是设置加载状态)
        (let [[_ effect-fn] result
              updated-state (assoc state :loading? true)] ; 立即更新状态表示开始加载
          ;; 在下一个事件循环或通过框架的机制执行副作用
          (js/setTimeout #(effect-fn) 0)
          updated-state)
        ;; 否则,直接返回同步结果
        result))))

;; 注册一个使用中间件的事件
(defn reg-event-async [event-key handler-fn]
  (let [wrapped-handler (async-event-middleware handler-fn)]
    (reg-event event-key wrapped-handler)))

(reg-event-async :load-todos-initial
  (fn [state _]
    ;; 返回一个效果描述,而不是直接执行fetch
    [:async-effect
     (fn []
       (-> (js/fetch "/api/todos")
           (.then #(.json %))
           (.then #(dispatch :load-todos-success {:todos (js->clj % :keywordize-keys true)}))
           (.catch #(dispatch :load-todos-failure {:error (.-message %)}))))]))

在组件中,我们可以在初始化时派发这个异步事件:

;; 在 main-panel 组件中,使用 reagent 的生命周期
(defn main-panel []
  (let [todos @(subs/filtered-todos)
        loading? @(subs/loading?)] ; 新增一个订阅
    (r/create-class
      {:component-did-mount
       (fn [_]
         (core/dispatch :load-todos-initial nil))
       :reagent-render
       (fn []
         [:div
          (if loading?
            [:div.loading "Loading..."]
            [:section.todoapp
             ;; ... 原有渲染逻辑 ...
             ])])})))

真实框架(如 re-frame )的异步效果处理会更加优雅和强大,通常通过 reg-fx (注册效果处理器)和 reg-event-fx (注册返回效果映射的事件)来实现,将副作用描述与状态更新完全分离。

4. 开发体验与高级特性探讨

4.1 热重载与开发工具集成

bookedsolidtech/reagent 框架通常能无缝融入ClojureScript的卓越开发体验。配合 figwheel shadow-cljs ,代码修改后几乎能实时在浏览器中看到更新,且状态得以保持(Hot Reload)。这对于UI调整和交互逻辑调试效率的提升是颠覆性的。

更高级的框架可能会提供专用的开发者工具,例如浏览器扩展,用于:

  • 检查状态树 :像Redux DevTools一样,可视化查看整个应用状态。
  • 事件追溯 :记录所有派发的事件及其载荷,支持时间旅行调试(Time Travel Debugging),可以回退到之前的状态。
  • 订阅可视化 :查看哪些组件订阅了哪些状态,帮助分析渲染性能。

4.2 性能优化策略

随着应用规模增长,性能成为关键。框架提供了多种优化手段:

  1. 订阅的细粒度化 :确保组件只订阅其真正依赖的最小状态单元。如果组件只关心 user.name ,就不要订阅整个 user 对象。框架的订阅系统通过 reaction 的依赖追踪,能自动实现这一点。
  2. 组件记忆化 :使用 reagent.core/memo 或类似高阶组件包装纯展示组件,避免在props未实际变化时重新渲染。
  3. 列表项键值 :在渲染列表时,始终为每个项提供稳定且唯一的 :key ,帮助Reagent(底层是React)高效地复用DOM节点。
  4. 异步渲染与并发模式 :如果框架基于较新的Reagent/React版本,可能支持并发特性(Concurrent Features),允许将非紧急的渲染工作拆分成小块,避免阻塞主线程,保证输入响应的流畅性。

4.3 测试策略

框架倡导的清晰架构让测试变得简单:

  • 事件处理器测试 :它们是纯函数!只需给定输入状态和载荷,断言输出状态即可。无需模拟任何外部依赖。
    (deftest add-todo-test
      (is (= {:todos [{:id 1 :text "Test" :done? false}] :next-id 2}
             (handle-event {:todos [] :next-id 1} [:add-todo {:text "Test"}]))))
    
  • 订阅测试 :同样是纯函数。测试给定状态时,订阅返回正确的派生值。
  • 组件测试 :对于展示组件,使用 reagent.core/as-element 和类似 jsdom 的环境,传入不同的props,断言渲染出的Hiccup结构。对于容器组件,可以模拟(mock)订阅返回的值,测试其渲染逻辑。
  • 集成测试 :使用 cljs.test 配合浏览器自动化工具(如 DevTools Protocol WebDriver ),模拟用户操作并断言UI结果。

4.4 与后端API的协作模式

在真实项目中,与后端通信是常态。框架的副作用管理系统使得API调用模式非常清晰:

  1. 定义API客户端 :创建一个独立的命名空间,封装所有HTTP请求函数,返回 core.async 通道或Promise。
  2. 创建效果处理器 :注册一个处理 :http 效果(或你自定义的效果关键字)的处理器。这个处理器调用上述API客户端。
  3. 派发事件 :UI组件或初始化逻辑派发事件,事件处理器返回一个包含 :http 效果描述的效果映射。
  4. 处理响应 :API调用成功后,派发另一个成功事件(如 :load-todos-success )来更新状态;失败则派发失败事件。

这种模式将异步逻辑、错误处理、加载状态管理都集中到了事件-效果循环中,保持了组件和事件处理器的纯洁性。

5. 常见陷阱、调试技巧与迁移建议

5.1 新手常犯的错误

  1. 在事件处理器中执行副作用 :这是最大的禁忌。事件处理器必须是纯的、同步的。所有副作用(HTTP、localStorage、 setTimeout )都应通过效果描述来触发。
  2. 过度订阅 :组件订阅了比它实际需要更多的状态。这会导致不必要的重新计算和渲染。定期使用开发者工具检查订阅关系。
  3. 在渲染函数中创建新函数 :例如,在列表项的 on-click 回调中直接写 #(dispatch ...) 。这会导致每次渲染都创建一个新的函数对象,使得子组件认为props发生了变化而重新渲染。正确的做法是在外层使用 useCallback (在React Hooks中)或Reagent的 with-let r/create-class 来记忆化回调函数。
  4. 忽略 :key 属性 :在动态列表渲染中,缺少或使用不稳定的 :key (如数组索引)会导致性能下降和状态错乱。始终使用唯一且稳定的标识符。

5.2 调试与问题排查

当UI不更新或行为异常时,可以按以下步骤排查:

问题现象 可能原因 排查步骤
组件完全不渲染 订阅返回 nil 或初始状态;组件函数有语法错误导致异常 1. 检查订阅函数逻辑,确保其返回值不为 nil (除非设计如此)。
2. 打开浏览器开发者控制台,查看是否有JavaScript错误。
3. 在组件第一行添加 (js/console.log “渲染组件”, props) 进行调试。
状态已变,但UI未更新 组件订阅的状态路径不正确;状态突变(Mutation)而非替换 1. 确认组件订阅的 sub-key 是否正确,订阅函数是否监听了正确的状态路径。
2. 核心 :检查事件处理器是否返回了 新的 状态对象( assoc , update , conj 等),而不是修改了原状态。ClojureScript的不可变数据结构是保障,但直接操作JavaScript对象会破坏这一原则。
异步操作后状态未更新 异步效果未正确派发成功/失败事件;事件名称拼写错误 1. 在效果处理器和事件派发处添加日志。
2. 使用开发者工具的事件监视器,查看 :load-todos-success 等事件是否被派发。
3. 检查网络请求是否成功,载荷格式是否正确。
性能问题,输入卡顿 存在昂贵的计算在订阅或渲染中;过度渲染 1. 使用React DevTools Profiler或类似工具分析组件渲染时间。
2. 检查订阅函数中是否有 filter map 等遍历大数组的操作,考虑使用记忆化或派生状态缓存。
3. 对复杂展示组件使用 memo

5.3 从传统Reagent项目迁移

如果你有一个现有的、结构松散的Reagent项目,想引入 bookedsolidtech/reagent 框架的模式,建议采用渐进式迁移:

  1. 引入状态存储 :首先,在项目中创建一个全局的 app-state 原子,将分散在各组件的重要状态逐步迁移到这个中心存储中。
  2. 定义核心事件 :为最重要的状态变更(如用户登录、加载核心数据)创建事件和处理器。
  3. 改造一个核心页面 :选择一个相对独立的功能模块,将其改造成使用新的事件系统和订阅系统。创建对应的容器组件和展示组件。
  4. 逐步替换 :以此模块为样板,逐步替换其他部分。对于不复杂的局部状态,可以暂时保留在组件本地。
  5. 引入副作用管理 :最后,再将HTTP请求等副作用从组件和事件处理器中抽离,纳入效果管理系统。

这个过程考验耐心,但每完成一步,代码的可维护性和可测试性都会得到显著提升。

5.4 生态与社区资源

bookedsolidtech/reagent 框架的价值不仅在于其代码,更在于它可能定义或倡导的一套最佳实践和社区共识。积极参与相关社区(如Clojurians Slack的 #reagent 频道,或项目的GitHub Discussions)非常重要。你可以从中获得:

  • 现成的解决方案 :对于常见需求(如表单处理、拖拽、图表集成),很可能已有社区开发的封装库或模式。
  • 代码评审 :将自己的代码片段或架构设计分享给社区,能获得宝贵的改进建议。
  • 学习案例 :研究其他采用相同框架的开源项目,是快速提升的最佳途径。

我个人在采用这类结构化框架后最深的体会是: 前期多花一点时间在架构设计上,后期在应对需求变更、调试复杂问题和 onboarding 新成员时,所节省的时间和减少的头痛是成倍的。 它迫使你更清晰地思考数据流和职责分离,最终产出的代码库更像一个精心设计的系统,而非一堆随机组合的脚本。对于任何计划长期维护或团队协作的ClojureScript前端项目,投入时间学习和应用这样的框架,是一项回报率极高的投资。

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐