ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

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

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/ 目录使用CLJCClojure/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-clj与com.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.cljc、page.cljc、shape.cljc、shape_tree.cljc、component.cljc、variant.cljc、token.cljc、tokens_lib.cljc、typography.cljc、grid.cljc、color.cljc、fills.cljc、stroke.cljc、text.cljc、path.cljc、library.cljc、project.cljc、team.cljc、organization.cljc、profile.cljc、font.cljc、plugins.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-teamsany|onlyMe、:delete-teamsonlyMe|onlyOwners、:move-teamsalways|myOrganizations|never、:new-team-membersanyone|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 ownersAndAdminsaction-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.cljc、page_diff.cljc、shapes_builder.cljc、shapes_helpers.cljc、builder.cljc、defaults.cljc、comp_processors.cljc、tokens.cljc、variant.cljc、focus.cljc、stats.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的高频 APIpcb/empty-changes、pcb/update-shapes、pcb/add-objects等并强调测试应通过thf/apply-changes走生产变更管线而非直接改对象 map。4. app.common.logic.*跨实体的较高层工作流/算法common/src/app/common/logic 目录现有五个命名空间libraries.cljc、shapes.cljc、tokens.cljc、variants.cljc、variant_properties.cljc对应文档中files, shapes, components, variants, libraries, tokens 之上的较高层 workflow/algorithm。它与types.*的区别在于允许协调一个文件内的多个实体但仍不承载 UI 事件或后端 RPC 层面的业务流程。5. app.common.geom.*几何助手与变换common/src/app/common/geom 目录包含align.cljc、bounds_map.cljc、grid.cljc、line.cljc、matrix.cljc、point.cljc、rect.cljc、snap.cljc、shapes.cljc以及子目录shapes/constraints.cljc、effects.cljc、fit_frame.cljc、flex_layout.cljc、grid_layout.cljc、min_size_layout.cljc、pixel_precision.cljc等。几何是设计工具的数值核心flex_layout与grid_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.cljc、desc_native.cljc、generators.cljc、openapi.cljc、registry.cljc、test.cljc构成对 Malli 的统一封装::sm/uuid、::sm/text等类型别名organization.cljc第 12-27 行的 schema 即建立在此层之上openapi.cljc支持从 schema 生成 OpenAPI 描述test.cljc则把 schema 校验接入测试。这层让所有types.*、files.*命名空间以一致方式声明与检查数据结构。7. 跨运行时工具与测试助手app.common.math、app.common.time、app.common.uuid、app.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-file、apply-changes、components.cljc、variants.cljc、shapes.cljc、compositions.cljc、tokens.cljc、ids_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 端实现也是同一模式。抽象方向必须自低向高保持文档给出了五层职责方向新代码与重构都应遵守泛型数据工具不感知 Penpot 域概念——app.common.data*只处理任意集合/map见第二节第 1 点types.*守住单个域实体或 ADT 的不变量——如organization.cljc只围绕组织实体的 schema 与权限谓词files.*可协调一个文件内的多个实体并维持引用完整性——变更应用、校验、修复都发生在此层changes*应把可序列化的变更记录适配为低层操作避免在其中内嵌宽泛业务算法——变更记录本身是持久化载荷与撤销/重做基础见 changes-architecture.md 的 A change set is both the persistence payload and the basis for undo/redologic.*与前端/后端事件层拥有更高层的 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/codegenTokentokens-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.mdmemories 根级跨切面测试原则、反模式、验证清单这些记忆与源码目录一一对应例如changes-architecture.md指向 files/changes.cljc 的process-operation多方法与 files/changes_builder.cljctesting.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/svgpath.cljc及path/子目录、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/目录下执行JVM 全量clojure -M:dev:testkaocha 驱动别名:test定义于 deps.edn 第 75-77 行JS 全量pnpm run test:quiet始终先构建再运行聚焦单测JVM 侧clojure -M:dev:test --focus common-tests.logic.variants-switch-test/test-basic-switchJS 侧pnpm run test:quiet -- --focus common-tests.logic.comp-sync-test可追加--log-level warn控制日志新增 JS 测试命名空间须登记到common_tests/runner.cljc已有命名空间新增 var 则无需改动几何敏感测试先读 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),仅供参考
返回列表