news 2026/9/7 19:12:06

Penpot 共享层解析:common/ 目录的 CLJC 架构、命名空间分层规则与跨运行时设计约束

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Penpot 共享层解析:common/ 目录的 CLJC 架构、命名空间分层规则与跨运行时设计约束

Penpot 共享层解析:common/ 目录的 CLJC 架构、命名空间分层规则与跨运行时设计约束

【免费下载链接】penpotPenpot: The open-source design platform for Product teams that need scalable collaboration.项目地址: https://gitcode.com/GitHub_Trending/pe/penpot

本文以仓库内的架构记忆文档 .serena/memories/common/core.md 为主体,完整展开 Penpot 共享代码层common/的命名空间地图、分层抽象规则与跨运行时(JVM + CLJS)约束,并结合 common/src/app/common 下的真实源码与 common/deps.edn 中的依赖事实进行纵深印证。读完后你能掌握:app.common.*各命名空间的职责边界、"泛型数据层不感知业务域"的分层纪律如何在源码中落地,以及组织/团队权限这类跨端共享逻辑(fail-closed 规则)的实际实现。

一、common/ 是什么:一份代码,多个运行时

Penpot 的前端(浏览器端编辑器)、后端(Clojure 服务端)、exporter(导出服务)以及库/文件工具链,都依赖同一份共享逻辑。这份共享代码放在 common/ 目录,使用CLJC(Clojure/ClojureScript)编写——同一份.cljc源码既编译到 JVM 又编译到 CLJS。正如架构记忆文档所述:"shared CLJC for frontend, backend, exporter, library/file tooling, tests. Small semantic changes can affect multiple runtimes"(共享给前端、后端、exporter、库/文件工具与测试;一处微小的语义变更可能影响多个运行时)。

这不是修辞,而是 common/deps.edn 中可见的工程事实:

  • org.clojure/clojure1.12.5 与org.clojure/clojurescript1.12.145 同时出现,确认同一模块需要双运行时构建;
  • metosin/malli0.20.1 与expound0.9.0 提供跨端一致的数据校验(Malli 同时支持 JVM 与 CLJS,这是共享 schema 层能存在的前提);
  • com.cognitect/transit-cljcom.cognitect/transit-cljs成对出现,对应同一序列化格式在两个运行时的实现;
  • 测试别名:test使用 kaocha(-m kaocha.runner),配合 JS 侧测试入口(pnpm run test:quiet),形成"同一 CLJC 测试、双端各跑一遍"的验证方式。

正因"一份语义、多个消费者",对common/的任何修改都应默认考虑前端、后端、exporter 三个方向的兼容性影响。

二、稳定的命名空间地图(Stable Namespace Map)

架构文档给出了app.common.*的职责划分,以下逐条对照 common/src/app/common 的实际目录结构验证:

1. app.common.data 与 app.common.data.macros:与业务域无关的通用数据工具

对应文件 common/src/app/common/data.cljc 与 common/src/app/common/data/macros.cljc,以及同目录的 common/src/app/common/data/undo_stack.cljc。

以 macros.cljc 为例,可以看到这一层"不依赖 Penpot 域实体"的具体含义:

  • select-keysclojure.core/select-keys的宏版本,当键集合在编译期已知时展开为逐个get,注释标明可获得约 600% 的性能提升,且语义上与核心版略有差异(不会删除不存在的键);
  • get-in:宏版本get-in,键向量为常量时编译为->链式get,注释标明 20-40% 的性能提升;
  • export:通过 reader conditional(#?(:clj ...))在编译期分别为 CLJS/CLJ 生成"再导出"代码,CLJS 分支甚至调用cljs.analyzer.api解析目标 var 的元数据。

这些工具全部操作"任意 map/向量",完全不知道 shape、component、file 为何物——这正是分层规则第一条的活例证。

2. app.common.types.*:单实体域的类型、schema 与谓词

common/src/app/common/types 目录实际包含 27 个命名空间:file.cljcpage.cljcshape.cljcshape_tree.cljccomponent.cljcvariant.cljctoken.cljctokens_lib.cljctypography.cljcgrid.cljccolor.cljcfills.cljcstroke.cljctext.cljcpath.cljclibrary.cljcproject.cljcteam.cljcorganization.cljcprofile.cljcfont.cljcplugins.cljc等。每个命名空间守住一个领域实体的 schema、谓词与"实体局部操作"。

架构文档特别点名的 types/organization.cljc 值得精读,因为它完整演示了"types.*保存单实体不变量 + fail-closed 权限规则":

  • schema:organization(第 12-27 行):定义了组织实体的 Malli schema,核心字段:id:name:slug:owner-id:avatar-bg-url,以及可选的:permissions子 map——:create-teams"any"|"onlyMe")、:delete-teams"onlyMe"|"onlyOwners")、:move-teams"always"|"myOrganizations"|"never")、:new-team-members"anyone"|"members")。由于组织逻辑同时运行在浏览器与 JVM 上,权限判断必须共享实现,这正是该 schema 放在common/而非后端的理由。
  • apply-organization(第 40-56 行):把组织字段以嵌套:organizationmap 形式合并进 team map。实现细节很讲究——对每个organization->team-keys中的字段,值非 nil 则assoc,否则dissoc,从而正确处理"挂接组织(字段全有)"与"解绑组织(org 为 nil 或字段全缺)"两个方向。
  • fail-closed 权限规则(第 78-176 行):defaults给出五个权限键的保守默认值(如:delete-teams "onlyOwners":send-invitations "ownersAndAdmins");action-rules:create-team:delete-team:move-team:send-invitations:add-anybody-to-team五个动作映射到各自的 check 函数;allowed?的文档字符串直接写明 "Returns true only for explicitly allowed actions (fail-closed)"——未知动作一律返回 false。而can-send-invitations?(第 164 行起)展示了共享逻辑如何读取功能开关:仅当flags/*current*包含:admin-console且团队挂了组织时才走组织级规则,否则回退到团队级 owner/admin 判断。这种"同一谓词、双端可用、显式降级"的写法是types.*层的典型形态。

3. app.common.files.*:文件级操作、shape 树、变更应用与迁移

common/src/app/common/files 目录包含 16 个文件:changes.cljc(变更应用)、changes_builder.cljc(变更构建器)、migrations.cljc(文件数据迁移)、validate.cljc(校验)、repair.cljc(修复)、indices.cljcpage_diff.cljcshapes_builder.cljcshapes_helpers.cljcbuilder.cljcdefaults.cljccomp_processors.cljctokens.cljcvariant.cljcfocus.cljcstats.cljc。它们承担架构文档所说的"file-level operations, shape tree helpers, change application, migrations, validation, and undo/redo-related logic"。

配套的聚焦记忆文档 changes-architecture.md 补充了变更记录的形状(:add-obj/:mod-obj/:del-obj:add-component等家族)与changes-builder的高频 API(pcb/empty-changespcb/update-shapespcb/add-objects等),并强调测试应通过thf/apply-changes走生产变更管线,而非直接改对象 map。

4. app.common.logic.*:跨实体的较高层工作流/算法

common/src/app/common/logic 目录现有五个命名空间:libraries.cljcshapes.cljctokens.cljcvariants.cljcvariant_properties.cljc,对应文档中"files, shapes, components, variants, libraries, tokens 之上的较高层 workflow/algorithm"。它与types.*的区别在于:允许协调一个文件内的多个实体,但仍不承载 UI 事件或后端 RPC 层面的业务流程。

5. app.common.geom.*:几何助手与变换

common/src/app/common/geom 目录包含align.cljcbounds_map.cljcgrid.cljcline.cljcmatrix.cljcpoint.cljcrect.cljcsnap.cljcshapes.cljc以及子目录shapes/constraints.cljceffects.cljcfit_frame.cljcflex_layout.cljcgrid_layout.cljcmin_size_layout.cljcpixel_precision.cljc等)。几何是设计工具的数值核心,flex_layoutgrid_layout子模块对应 Penpot 的弹性布局/网格布局能力。几何相关的不变量与坐标浮点比较细节,分别由 geometry-invariants.md 与 decimals-and-coordinates.md 两个聚焦记忆文档覆盖。

6. app.common.schema / app.common.schema.*:Malli 抽象层

common/src/app/common/schema.cljc 与 common/src/app/common/schema 子目录(desc_js_like.cljcdesc_native.cljcgenerators.cljcopenapi.cljcregistry.cljctest.cljc)构成对 Malli 的统一封装:::sm/uuid::sm/text等类型别名(organization.cljc第 12-27 行的 schema 即建立在此层之上),openapi.cljc支持从 schema 生成 OpenAPI 描述,test.cljc则把 schema 校验接入测试。这层让所有types.*files.*命名空间以一致方式声明与检查数据结构。

7. 跨运行时工具与测试助手

  • app.common.mathapp.common.timeapp.common.uuidapp.common.json:对应 math.cljc、time.cljc、uuid.cljc、json.cljc。注意 uuid.cljc 与 common/src/app/common/UUIDv8.java、common/src/app/common/uuid_impl.js 的组合——同一份 CLJC 逻辑通过平台特定实现文件(JVM 端 Java、JS 端 JavaScript)落地 UUID 生成,这是后文"reader conditional 规则"的典型用例。
  • common/src/app/common/weak 与 weak.cljc:弱引用容器的跨端抽象,impl_weak_map.js/impl_loadable_weak_value_map.clj按运行时选择实现;
  • app.common.test_helpers.*:common/src/app/common/test_helpers 目录提供生产路径测试助手——files.cljc(如sample-fileapply-changes)、components.cljcvariants.cljcshapes.cljccompositions.cljctokens.cljcids_map.cljc。据 testing.md,测试命名空间惯用thf/tho/thv/等短别名引用这些助手,且使用 label→uuid 助手的测试应以(t/use-fixtures :each thi/test-fixture)开头以便在每个用例间重置。

三、分层与跨运行时规则(Layering and Cross-Runtime Rules)

架构文档的核心纪律可归纳为两条,均能在源码中找到支撑。

平台特定代码必须用 reader conditional 隔离

"Use reader conditionals for platform-specific code. Because CLJC runs on JVM and CLJS targets, avoid assuming browser-only or JVM-only behavior unless the reader conditional isolates it."

源码实例:macros.cljc 第 10-14 行用#?(:cljs (:require-macros ...))处理宏自身在 CLJS 下的加载;第 12-14 行#?(:clj [cljs.analyzer.api :as aapi] :clj [clojure.core ...] :cljs [cljs.core ...])在同一:require中按运行时选择依赖;export宏的整个定义被#?(:clj ...)包裹(第 54 行起),因为它本身就是编译期工具,仅在 JVM 编译 CLJS 源码时生效。weak.cljc 按运行时分发到impl_weak_map.js或 JVM 端实现,也是同一模式。

抽象方向必须自低向高保持

文档给出了五层职责方向(新代码与重构都应遵守):

  1. 泛型数据工具不感知 Penpot 域概念——app.common.data*只处理任意集合/map(见第二节第 1 点);
  2. types.*守住单个域实体或 ADT 的不变量——如organization.cljc只围绕组织实体的 schema 与权限谓词;
  3. files.*可协调一个文件内的多个实体并维持引用完整性——变更应用、校验、修复都发生在此层;
  4. changes*应把可序列化的变更记录适配为低层操作,避免在其中内嵌宽泛业务算法——变更记录本身是持久化载荷与撤销/重做基础(见 changes-architecture.md 的 "A change set is both the persistence payload and the basis for undo/redo");
  5. logic.*与前端/后端事件层拥有更高层的 workflow/业务行为

文档同时提醒:"Some legacy code violates this layering; do not copy those violations into new code when a focused refactor is practical."——遗留代码存在违反分层的情况,但不应把违反扩散到新代码。

四、记忆路由:修改 common/ 前该读哪份聚焦文档

架构文档的 "Focused memory routing" 节把common/的细粒度知识分发到 13 份聚焦记忆。下表完整继承该路由,并标注对应源文件位置,便于按图索骥:

领域聚焦记忆(相对仓库根目录)覆盖内容
模型/持久化形状data-model-change-checklist.md文件/页面/shape/组件属性变更的跨模块检查清单、导入导出面、inspector/codegen
Tokentokens-schema-subtleties.mdtoken 数据结构、导入导出、active theme/set 语义、schema 强制转换行为
几何与布局geometry-invariants.mdshape 几何不变量、冗余几何字段、几何敏感测试
几何与布局decimals-and-coordinates.md坐标漂移与近似浮点比较
几何与布局layout-grid-subtleties.md布局/网格的 assign、deassign、元数据清理、自动定位
变更管线changes-architecture.md变更记录、undo/redo 架构、changes-builder API、生产路径变更指南
变更管线file-change-validation-migration-subtleties.md变更应用、shape 树编辑、校验/修复、迁移、second-pass touched 行为
组件/变体component-data-model.md组件/变体数据模型、ref 链、touched 覆盖语义、克隆路径
组件/变体component-swap-pipeline.md组件 swap、变体切换、keep-touched 管线
组件/变体component-debugging-recipes.md实时检查片段、临时运行时 patch、测试侧调试助手
文本与测试text-subtleties.md共享文本数据转换、DraftJS 兼容、现代文本内容、派生定位数据
文本与测试testing.md常用测试命令、助手约定、生产路径测试变更、运行时覆盖选择
全局测试纪律testing.md(memories 根级)跨切面测试原则、反模式、验证清单

这些记忆与源码目录一一对应:例如changes-architecture.md指向 files/changes.cljc 的process-operation多方法与 files/changes_builder.cljc;testing.md给出的命令(从common/目录执行clojure -M:dev:test跑 JVM 全量测试、pnpm run test:quiet跑 JS 全量测试、--focus common-tests.logic.variants-switch-test聚焦命名空间)与 common/deps.edn 的:test别名、common/scripts/test 等脚本直接呼应。

五、没有聚焦记忆的领域:以源码和测试为准

架构文档的最后一节明确列出"几乎没有专门记忆"的common/领域:colors、media/SVG 助手、path 操作、缩略图助手、通用池、弱引用及部分工具命名空间。对照源码,这些正是 colors.cljc、media.cljc、common/src/app/common/svg(path.cljcpath/子目录)、thumbnails.cljc、generic_pool.clj、weak/ 等文件。文档给出的工作方式很明确:Treat work there as source/test-led unless a focused memory exists——在这些领域直接以源码与测试为主要依据推进,不要期待或虚构不存在的记忆文档。

六、把 common/ 改动落到验证:最小操作路径

综合 testing.md 与 common/deps.edn 的别名定义,验证common/改动的标准动作(均在common/目录下执行):

  1. JVM 全量clojure -M:dev:test(kaocha 驱动,别名:test定义于 deps.edn 第 75-77 行);
  2. JS 全量pnpm run test:quiet(始终先构建再运行);
  3. 聚焦单测:JVM 侧clojure -M:dev:test --focus common-tests.logic.variants-switch-test/test-basic-switch;JS 侧pnpm run test:quiet -- --focus common-tests.logic.comp-sync-test,可追加--log-level warn控制日志;
  4. 新增 JS 测试命名空间须登记到common_tests/runner.cljc,已有命名空间新增 var 则无需改动;
  5. 几何敏感测试先读 geometry-invariants.md,优先使用保持几何不变量的助手或生产变更助手,而非直接编辑单个字段。

七、小结

common/的价值不在于"共享"二字,而在于它把 Penpot 文件数据模型、几何、变更管线与权限规则收敛成一份跨 JVM/CLJS 的语义来源,并用严格的分层方向(data → types → files → changes → logic/事件层)与 reader conditional 纪律约束这份语义只在一处实现。对贡献者而言,架构文档给出的三条行动准则是:先按"记忆路由表"读对聚焦文档,再对照 common/src/app/common 对应命名空间动手;新代码保持抽象方向不自上而下泄漏;在没有聚焦记忆的区域,以源码与测试为唯一事实来源。

【免费下载链接】penpotPenpot: The open-source design platform for Product teams that need scalable collaboration.项目地址: https://gitcode.com/GitHub_Trending/pe/penpot

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 19:11:33

QMK 固件入门:7 个环节走完,把自定义固件刷进你的机械键盘

QMK 固件入门:7 个环节走完,把自定义固件刷进你的机械键盘 【免费下载链接】qmk_firmware Open-source keyboard firmware for Atmel AVR and Arm USB families 项目地址: https://gitcode.com/GitHub_Trending/qm/qmk_firmware 想让 Delete 不用…

作者头像 李华
网站建设 2026/9/7 19:06:50

专业字体管理工具的功能对比与实战应用

1. 为什么我们需要专业的字体管理工具 作为一名长期与字体打交道的设计师,我深刻体会到字体管理的重要性。每当接手新项目时,最头疼的就是面对数百个杂乱无章的字体文件——有些重复安装,有些版本冲突,还有些根本不知道什么时候装…

作者头像 李华
网站建设 2026/9/7 19:05:25

S7-200模拟器bet2.5e使用指南:无硬件也能调试PLC程序

干工控这行的都知道,调试PLC程序最怕什么?不是逻辑写不出来,是设备不在手边,或者项目还没进场,没法实际验证。尤其是西门子S7-200这种老平台,现在新项目里不常见了,但存量设备维护、职校教学、个…

作者头像 李华