- 前端
- Web框架
【免费下载链接】re-frame
A ClojureScript framework for building user interfaces, leveraging React
导读
re-frame 将所有应用状态集中存放在一个名为app-db的ratom(Reagent atom)中,其内部值是一个 Clojuremap。当你的服务端采用关系型数据库、并返回范式化(normalised)数据时,如何让这个内存map同样以规范化形态组织数据,从而与数据库结构一一对应?本文围绕官方 FAQ《DB Normalisation》展开,先讲清app-db的"内存数据库"本质,再介绍 compound、SubGraph、pull、reflet 等可复用库的取舍,最后讨论激进方案——用 DataScript 替换app-db(re-posh)。读完后,你将能在实践中依据数据规模与查询模式,选择正确的规范化落地方式。
app-db为什么是一个"map"?——先理解它的本质
在深入规范化之前,需要明确 re-frame 官方文档对app-db的定位。在 application-state.md 中,re-frame 作者明确指出:
re-frame puts all application state into one place, which is called
app-db.
它实际就是一个 Reagent atom,初始值为一个空 map。仓库源码 src/re_frame/db.cljc 中的定义非常简洁:
(ns re-frame.db (:require [re-frame.interop :refer [ratom]])) (def app-db (ratom {}))而ratom在 CLJS 端是 Reagent 提供的响应式 atom(src/re_frame/interop.cljs):
(defn ratom [x] (reagent.core/atom x))在 JVM 端(用于跑测试或 JVM REPL),则退化为普通的 Clojureatom(src/re_frame/interop.clj):
(defn ratom [x] (atom x))官方文档特意强调:与其把app-db想成"atom 里的一个 map",不如把它想成一个"内存数据库"(in-memory database)。既然是数据库,你就需要查询它、对它做 CRUD 和各种变换、还经常要原子地"事务式"更新它。这正是理解"规范化"这一话题的前提——规范化是数据库领域的概念,把它引入app-db,就是让内存中的这一份数据,结构上尽可能贴近你服务端数据库中的形态。
几点值得注意的官方约定:
- 文档与代码中区分
app-db(ratom 本身)与db(ratom 内部当前存放的 map 值),阅读代码时注意这个命名差异。 - re-frame 会自动创建并管理
app-db,你通常无需自己声明(src/re_frame/db.cljc)。 app-db并非必须是一个"装着 map 的 ratom"——理论上它可以是任何能在变化时通知你的数据库(详见后文 DataScript 部分)。
FAQ 原文的问题描述如下:
app-dbcontains amap. How do I store normalised data in amap, bettering mirroring the structure in my server-side database?
翻译过来就是:app-db里存的是一个 map,如何在这个 map 里存储规范化数据,以更好地镜像服务端数据库的结构?答案的落点,是推荐一批专门解决该问题的 Clojure/ClojureScript 库。
推荐方案一:引入专用规范化库
FAQ 原文首先推荐了四个值得关注的库,它们在"规范化与反规范化"这个主题上各有侧重:
| 库 | 定位 | 侧重点 |
|---|---|---|
| compound | 规范化/反规范化工具库 | 通用数据转换,适合自建数据层 |
| SubGraph | 订阅层规范化 | 与 re-framesubscribe/reg-sub深度结合 |
| pull | Datomic 风格 pull API | 用类似 Datomic 的语法从规范化数据中取图 |
| reflet | 一套完整的 re-frame 封装 | 在同一个app-db中混合使用规范化和非规范化数据 |
这些库的共同思路是:以 ID 而非嵌套结构来组织数据。比如一个包含作者信息的文章列表,非规范化的写法是:
;; 反规范化:文章里直接嵌套作者对象 {:articles [{:id 1 :title "A" :author {:id 7 :name "X"}} {:id 2 :title "B" :author {:id 7 :name "X"}}]}规范化的写法则是把作者单独存一张"表",文章只引用 ID:
;; 规范化:作者独立存放,文章通过 ID 引用 {:articles [{:id 1 :title "A" :author-id 7} {:id 2 :title "B" :author-id 7}] :authors {7 {:id 7 :name "X"}}}当服务端数据库返回的就是这种"ID 引用"形态的数据时,直接把它放进app-db,两者结构即可严格对应。随后用 pull 或 SubGraph 这类工具,在订阅时把规范化数据重新"拼装"成组件需要的嵌套视图。
FAQ 原文还特别提到:reflet 允许你在同一个app-db中同时使用规范化和非规范化数据。这是非常实用的现实场景——一个应用里,有些数据天然嵌套(例如表单草稿),有些数据来自服务端需要按 ID 引用(例如用户、文章),强制全部规范化反而增加负担。reflet 的价值正是解除这种"非此即彼"的约束。
FAQ 原文同时引用了一个 GitHub issue 中的讨论(re-frame issue #304 的评论),对"如何在 map 中存储规范化数据"做了更深入的背景阐述,建议对数据建模有疑问的读者查阅该讨论。
推荐方案二:整体转向 DataScript——"全情投入"路线
如果规范化需求已经到了极致,FAQ 原文给出了另一个更彻底的选择:
If you want to go whole hog and ditch
app-dband embrace DataScript, then you might find re-posh interesting.
即:彻底放弃app-db,拥抱 DataScript 数据库,并用 re-posh 把它接入 re-frame。
DataScript 是一个运行在内存中的 Datalog 数据库,天然以规范化(EAVT 三元组)方式存储数据,并提供强大的查询能力。application-state.md 中作者对这个方案的态度非常鲜明——"we'd love! to be using datascript - so damn cool - but we had too much data in our apps",即 re-frame 团队非常欣赏 DataScript,只是他们的应用数据量太大,才没有全面采用。
需要明确的是:DataScript 方案并非开箱即用。app-db在 re-frame 中承担着订阅、事件处理、trace 调试等核心流程的输入信号角色,从源码结构看:
- 事件处理默认把
app-db的当前值放进:coeffects的:db键(src/re_frame/cofx.cljc); - 订阅的默认
signal-fn返回app-db(src/re_frame/subs.cljc); :dbeffect 通过reset!用新 map 替换app-db(src/re_frame/fx.cljc)。
因此要用 DataScript,就必须"tweak re-frame a bit"(做一点改造),而 re-posh 正是帮你完成这一层对接的桥梁。FAQ 原文对这条路的定性是"whole hog"(全情投入)——它收益最大,代价也最大,适合数据关联关系复杂、查询模式多变、且内存数据量可控的应用。
规范化与app-db的天然契合点
规范化数据形态之所以与app-db契合,底层原因可以从 re-frame 的架构中看出来:
1. 单点数据源 + 订阅查询。app-db是唯一真源,任何视图只能通过订阅读取。规范化数据配合reg-sub层做拼装查询,恰好形成"存储规范化、视图反规范化"的分层:底层存 ID 引用,订阅时用pull/get-in/select-keys组合出组件要的嵌套结构。官方文档在 subscriptions.md 中把订阅视为对app-db的查询层,这正与数据库的"视图"概念同构。
2. 单一reset!即"事务提交"。官方文档指出,全部状态收拢进一个 atom 后,一次reset!就能让应用从一种状态原子地过渡到另一种状态,不存在中间不一致态。规范化数据天然要求"引用完整性"——比如删除作者时必须同步清理引用它的文章——而单点原子更新让这类多表联动可以在一个事务性更新中完成。re-frame 的:dbeffect 也实现了"新旧值相同则跳过重置"的优化(src/re_frame/fx.cljc)。
3. 树形结构直觉 vs 关系型结构现实。刚接触 re-frame 时,很多人习惯在app-db里维护一棵与 UI 组件树对应的嵌套树。但当数据主要来自服务端关系型数据库时,嵌套树会带来严重的重复存储与同步问题——同一份用户数据散落在多个地方,任何一处更新都要记得同步其余副本。规范化把"一份数据只存一份"这个原则落到内存中,是消除这类 bug 的根本手段。
如何验证数据形态:配合 schema 与调试工具
选择了规范化方案后,app-db的数据形态会变得更像"多个表",此时给数据加上 schema 约束的收益会成倍放大。官方文档在 application-state.md 中建议为app-db编写 clojure.spec,并且可以在每次事件处理完之后对全量数据做校验,从而尽早且精准地捕获错误。
调试方面,FAQ《Inspecting app-db》提供了实用的查看手段(docs/FAQs/Inspecting-app-db.md):
- REPL 中直接检查
re-frame.db/app-db; - JS console 中查看
window.re_frame.db.app_db.state; - 在视图 hiccup 里嵌入打印,观察某个子路径:
[:pre (with-out-str (pprint (:part @re-frame.db/app-db)))] ;; see a part of it!规范化之后数据层级变深、引用变多,这类"只看一部分"的调试手段会比整体打印实用得多。
小结与选型建议
回到 FAQ 的核心问题——"如何在 map 中存储规范化数据以镜像服务端数据库结构"——官方给出的答案路径是清晰的:
- 多数场景:保持
app-db为普通 map,把服务端返回的规范化数据原样存入(按 ID 组织、引用而非嵌套),再借助 compound、SubGraph、pull 等库在订阅层做查询与拼装;reflet 让你可以在同一个app-db中按需混合规范化和非规范化数据。 - 重度关联场景:考虑"全情投入"——放弃
app-db,用 DataScript 作为响应式数据源,通过 re-posh 桥接 re-frame。这条路线需要改造 re-frame 默认的数据接入方式(事件:coeffects、订阅默认signal-fn、:dbeffect 的reset!),从源码结构看(src/re_frame/cofx.cljc、src/re_frame/subs.cljc、src/re_frame/fx.cljc)是可行但需要额外工作的。 - 无论哪条路:都应配合 spec 校验与订阅层查询设计,把"存储规范化、视图反规范化"的分层坚持到底。
这条 FAQ 于 2017 年加入 re-frame 官方文档(见 docs/releases/2017.md 与 mkdocs.yml 中的导航条目),至今仍是 re-frame 数据建模问题的高频入口。对数据组织有疑问时,不妨先从"这份数据是一棵与组件树对应的树,还是一张与数据库对应的表"这个问题开始思考。
- 前端
- Web框架
【免费下载链接】re-frame
A ClojureScript framework for building user interfaces, leveraging React
相关推荐
Skopeo镜像元数据查询:内存数据库应用
Skopeo镜像元数据查询:内存数据库应用 引言:容器元数据的实时处理挑战 在Kubernetes集群中,镜像拉取失败导致的Pod启动故障占比高达23%,其中6
云原生CLI镜像仓库Nilesoft Shell:让 Windows 右键菜单只留你常用的那几项
Nilesoft Shell:让 Windows 右键菜单只留你常用的那几项 Nilesoft Shell 帮你接管 Windows 资源管理器的右键菜单。哪几
桌面应用在SCSS中使用Map数据结构的技术指南
在SCSS中使用Map数据结构的技术指南 什么是SCSS中的Map Map是SCSS Sass 中一种强大的数据结构,它允许开发者将一组相关的键值对存储在一起。
文档教程知识库
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考