ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Penpot 共享代码架构解读:Clojure 前后端同构的 common 模块全景分析

2026/9/8 23:48:34 拓冰建站 浏览量
Penpot 共享代码架构解读:Clojure 前后端同构的 common 模块全景分析 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 条件构造虽然后端运行在 JVMClojure Java 互操作前端运行在 JavaScript 引擎ClojureScript JS 互操作但大多数共享代码可以写得与平台无关。对于少数确实不同的片段Penpot 使用 Clojure 的 reader 条件reader conditional#?进行条件编译根据编译目标是:cljClojure还是:cljsClojureScript选择不同实现。原文档给出的经典例子是判定某个对象是否为有序集合的实现差异(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/arcToBeziersClojureScript 分支调用同名的 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/ ...对照当前仓库的实际源码树共享层已演进为更细的分区。核心目录与模块包括geom2D 几何运算含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/pathSVG 路径解析与变换双平台实现顶层工具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文件数据模型模块及几何、路径两个基础模块中。geom2D 几何实体geom提供管理二维几何实体的函数是各类形状变换、对齐、吸附、布局计算的地基。point定义 2DPoint类型及大量几何变换函数。在源码 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 子目录进一步覆盖变换、相交、圆角、描边、效果、弹性/网格布局等具体逻辑。pathSVG 路径管理path负责管理 SVG 路径解析、变换以及把其他类型形状转换为路径。相关源码位于 app.common.svg.pathparse函数把路径字符串解析为 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),仅供参考