Penpot 共享代码架构解读:Clojure 前后端同构的 common 模块全景分析
【免费下载链接】penpotPenpot: The open-source design platform for Product teams that need scalable collaboration.项目地址: https://gitcode.com/GitHub_Trending/pe/penpot
本文聚焦 Penpot 开源设计平台架构中的关键一环 —— 位于 common/src/app/common 的共享代码(Common code)模块。由于 Penpot 前端(ClojureScript/React)与后端(Clojure/JVM)使用同一种语言,核心数据模型、几何运算与业务逻辑得以收敛到独立的前后端通用包中。读完本文,你将掌握该模块的目录组织方式、#?条件编译的用法,理解几何、路径、数据模型、事务化变更操作及各类工具库的分工,并能结合真实源码在 Penpot 仓库中快速定位、拓展这些基础设施。
为什么需要一套"共享代码"层
Penpot 是典型的 SPA 架构:前端应用用 ClojureScript 编写并基于 React,运行在浏览器中;后端应用用 Clojure 编写,编译为 JVM 字节码运行,数据持久化于 PostgreSQL,详见 架构总览。
同一门语言带来一个天然红利:前端与后端可以共享一大批代码与数据结构而不必重复实现。这正是 common 模块存在的根本原因。仓库中的具体代码文件使用.cljc扩展名,正是 Clojure/ClojureScript 双平台共用源文件的标准做法——例如 uuid.cljc、transit.cljc、geom/point.cljc 等。后端需要读文件、建模、算几何,前端需要同样的文件模型、几何变换与业务规则,二者共用一份源码,从源头避免了"两套实现、行为漂移"的经典问题。
条件编译:#? reader 条件构造
虽然后端运行在 JVM(Clojure + Java 互操作),前端运行在 JavaScript 引擎(ClojureScript + JS 互操作),但大多数共享代码可以写得与平台无关。对于少数确实不同的片段,Penpot 使用 Clojure 的 reader 条件(reader conditional)#?进行条件编译,根据编译目标是:clj(Clojure)还是:cljs(ClojureScript)选择不同实现。
原文档给出的经典例子是判定某个对象是否为有序集合的实现差异:
(defn ordered-set? [o] #?(:cljs (instance? lks/LinkedSet o) :clj (instance? LinkedSet o)))其中:cljs分支检查 ClojureScript 的lks/LinkedSet,:clj分支检查 Java 端的LinkedSet类型。这段代码清晰地展示了共享命名空间如何为同一函数提供平台专属实现。
同样的模式在当前仓库源码中大量出现。以 SVG 路径解析为例,svg/path.cljc 为两个平台分别引入了不同语言的解析器:
(ns app.common.svg.path #?(:clj (:import app.common.svg.path.Parser app.common.svg.path.Parser$Segment)) #?(:cljs (:require ["./path/parser.js" :as parser])))arc->beziers函数随之在两端分发到不同实现:Clojure 分支调用 Java 编写的Parser/arcToBeziers,ClojureScript 分支调用同名的 JS 实现parser/arcToBeziers。目录 common/src/app/common/svg/path 中同时存在的Parser.java、parser.js、arc_to_bezier.js等文件,正是这种"一份逻辑、双平台原生实现"策略的直接证据。
共享代码目录总览
原文档给出了模块的心智模型目录结构:
▾ common/src/app/common/ ▸ geom/ ▸ pages/ ▸ path/ ▸ types/ ...对照当前仓库的实际源码树,共享层已演进为更细的分区。核心目录与模块包括:
- geom:2D 几何运算,含
point.cljc、matrix.cljc、shapes.cljc以及功能更细的 geom/shapes 子目录(flex/grid 布局、相交检测、变换、文本排版等); - files:文件数据模型及其演进,含
changes.cljc、changes_builder.cljc、defaults.cljc、helpers.cljc、migrations.cljc、validate.cljc等; - types:向抽象数据类型(ADT)范式重构后的领域类型定义,如
shape.cljc、file.cljc、page.cljc、color.cljc、component.cljc及其子目录; - svg/path:SVG 路径解析与变换(双平台实现);
- 顶层工具:data.cljc、math.cljc、logging.cljc、text.cljc、transit.cljc、uuid.cljc 等。
说明:原文档中描述的
pages/模块在当前源码树中已对应迁移至 app.common.files 命名空间(Clojure 命名空间文件名以-代替_,如changes_builder.cljc对应app.common.files.changes-builder)。文档本身也提示"部分模块仍需重构以更清晰地组织",下文将结合现状与演进方向分别阐述。
数据模型与业务逻辑
数据模型层是共享代码中最核心的部分,它定义"一个 Penpot 文件到底是什么",并实现不依赖 UI 与存储的业务逻辑。原文档将这些职责归纳在pages(文件数据模型)模块及几何、路径两个基础模块中。
geom:2D 几何实体
geom提供管理二维几何实体的函数,是各类形状变换、对齐、吸附、布局计算的地基。
- point:定义 2D
Point类型及大量几何变换函数。在源码 geom/point.cljc 中,Point 被声明为 record(cr/defrecord Point [x y]),并配套 schema 校验(schema:point-attrs、valid-point?)、字符串/JSON 编解码与平台序列化支持(引入 fressian 与 transit)。 - matrix:定义 2D 变换矩阵类型及其运算,支撑旋转、缩放、平移与坐标系换算(参考 geom/matrix.cljc,几何模块目录 geom 下还有
rect.cljc、line.cljc、align.cljc、snap.cljc等配套文件)。 - shapes:把形状作为带包围矩形(bounding rectangle)的点集合来管理。在 geom/shapes.cljc 中可以看到
bounding-box(返回经全部变换后包裹形状的矩形)、left-bound/top-bound(变换前的坐标边界)、translate-to-frame/translate-from-frame(形状与所在画板 frame 之间的坐标平移)等函数;功能细化后拆出的 geom/shapes 子目录进一步覆盖变换、相交、圆角、描边、效果、弹性/网格布局等具体逻辑。
path:SVG 路径管理
path负责管理 SVG 路径:解析、变换,以及把其他类型形状转换为路径。相关源码位于 app.common.svg.path,parse函数把路径字符串解析为 segment 向量:
(defn parse [path-str] (if (empty? path-str) path-str #?(:clj (into [] (map (fn [segment] (.toPersistentMap ^Parser$Segment segment))) (Parser/parse path-str)) :cljs (into [] (map (fn [segment] (.toPersistentMap ^js segment))) (parser/parse path-str)))))这是#?条件编译在"业务级"共享代码中的一个典型用法。此外 svg/path 目录还保留了圆弧转贝塞尔曲线(arc_to_bezier)、旧版解析器(legacy_parser2.cljc)等能力,供布尔运算、轮廓描边等编辑器功能复用。
pages(文件数据模型)与业务逻辑
原文档将 Penpot 数据模型定义与"概念层业务逻辑"集中描述在pages模块,并细分出以下组成:
- spec:定义文件与形状的数据结构定义,以及
changes模块中的变换操作定义,使用 Clojure spec 描述结构与校验器。在当前的共享代码中,数据定义与运行时校验职责主要由 schema 体系 与各模块顶部的 schema 声明承接——例如 changes.cljc 中以schema:operation的多方法 schema 定义:assign、:set、:set-touched、:set-remote-synced等操作载荷。 - init:定义文件、页面、形状的默认内容。当前仓库中对应 files/defaults.cljc(命名空间
app.common.files.defaults),例如新建画板/页面/形状时所需的初始属性集合。 - helpers:提供操纵数据结构的辅助函数,对应 files/helpers.cljc,同时被
geom、types等多个模块引用。 - migrations:负责数据模型结构随时间的演化——它接收一份文件数据内容,识别其版本号,再按需应用迁移,机制与 SQL 数据库迁移脚本非常相似。对应源码 files/migrations.cljc,其命名空间引入面极广(几何、形状、组件、字体、token 等模块几乎全部参与),可见文件结构迁移是一项横切大量领域的系统性工作。
- changes 与 changes_builder:定义一组事务性操作,接收文件数据内容后,按业务语义执行一次操作(如新增页面或形状、修改形状属性、改动文件资产)。对应 files/changes.cljc 与 files/changes_builder.cljc。在共享层之上,后端 RPC 与前端的本地状态管理复用同一套语义操作,保证一次编辑在前端"乐观"生效与在后端持久化时行为一致——这构成了 Penpot 多用户协作与撤销/重做能力的基础。
types:向抽象数据类型(ADT)范式重构
原文档特别指出,Penpot 正在将pages模块增量式地重构为更符合抽象数据类型(Abstract Data Types, ADT)范式,每次按需重写一个模块,逐步推进。
仓库中对应的重构成果集中在 types 目录:shape.cljc、file.cljc、page.cljc、path.cljc、color.cljc、component.cljc、text.cljc、grid.cljc等命名空间各自封装一个领域实体(或其实体片段)的类型定义、schema 与操作函数。配套的分层说明见 abstraction-levels.md,该文档明确描述了当前目标架构的分层约束:每个层级只允许使用同层或更低层级,不允许反向依赖。在同一份文档中还强调,尽管命名空间结构已经按该规则组织,仍有大量历史代码尚未完全合规,需要在后续重要功能改动时持续推进——这与 common.md 中"部分模块仍需重构"的判断相互印证。
常用工具库
除了领域模型,共享模块还沉淀了一批"放进标准库也不违和"的通用工具。以下是原文档列出的主要工具及其在当前仓库中的落点:
- data:基础数据结构与通用工具函数,可视为 Clojure 标准库的补充。源码位于 data.cljc,并配套 data/macros.cljc(如
dm/get-prop、dm/str等宏在上文源码片段中被高频使用)与 data/undo_stack.cljc。 - math:一些同样"够格进标准库"的数学函数,见 math.cljc,被几何模块以
mth别名广泛引用。 - file_builder:解析
.penpot导出文件内容并据此构建 File 数据结构。从当前代码结构看,对应的装载逻辑涉及 files/builder.cljc 及后端导入流程(backend 下 binfile 模块负责二进制文件读写),阅读时可将files目录视为该能力的延续与重组。 - logging:为调试与使用分析生成跟踪信息的日志函数,见 logging.cljc。
- text:建立在 DraftJS 编辑器之上的适配层,用于在工作区中编辑文本形状。前端仓库中保留了 frontend/packages/draft-js 的本地化依赖,印证了这套文本编辑链路:共享文本模型(common 的
text适配层)对接前端编辑器组件,再由 render-wasm 端完成排版与栅格化。 - transit:将 Clojure 对象编码/解码为 Transit 格式的工具函数。Transit 是与 JSON 类似但表达能力更强的序列化格式;实现见 transit.cljc。Penpot 的 RPC、缓存与跨端消息传递都依赖这类高效编码。
- uuid:生成 UUID 的工具函数,见 uuid.cljc 与其平台实现 uuid_impl.js。Penpot 模型中所有对象都用 UUID 作为标识符,在没有中央协调节点的情况下仍能获得"实际上可保证唯一"的对象 ID——这对分布式、多实例部署与离线导入导出的场景至关重要。
小结:从共享代码理解 Penpot 的整体工程观
站在 docs/technical-guide/developer/architecture/index.md 描述的 SPA 架构之上回看,common 模块是 Penpot"一套语言、两处运行"工程策略的最大受益者:
- 用
.cljc+#?条件编译,让几何、路径、文件模型、事务变更操作在浏览器与 JVM 两端保持一致语义; - 数据模型相关的
init/helpers/migrations/changes职责清晰对应到 files 下的同名命名空间,SQL 层之外的"文件结构版本迁移"由此获得与 数据库迁移 类似的治理手段; - 正在进行的 types ADT 化重构,配合 abstraction-levels.md 的分层约束,为后续扩展新形状类型、新文件操作提供更稳的边界。
对希望参与 Penpot 开发或二次定制的工程师而言,理解本模块是阅读 frontend 与 backend 两侧代码的前置条件:绝大多数跨端共享的业务规则与数据结构,都定义在这个"即不属于前端、也不属于后端"的 common 层中。
【免费下载链接】penpotPenpot: The open-source design platform for Product teams that need scalable collaboration.项目地址: https://gitcode.com/GitHub_Trending/pe/penpot
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考