尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Plate 跨平台分层规则:语义核心、平台适配器与 Open UI 三层模型

Plate 跨平台分层规则:语义核心、平台适配器与 Open UI 三层模型 Plate 跨平台分层规则语义核心、平台适配器与 Open UI 三层模型【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate本文基于 Plate 仓库中plate-ui技能规则文件 cross-platform.md 展开讲解 Plate 构建新组件能力时采用的三层模型语义核心 / 平台适配器 / Open UI、React 包“可以拥有”与“不可拥有”的能力边界以及对10tap-editor经验的取舍。读完后你能判断一段新代码应该落在packages/*/src/lib、packages/*/src/react还是apps/www/src/registry/ui并理解仓库中useMediaState、useTocElementState、useEquationElement等真实 hook 是如何体现这些分层原则的。为什么 Plate 需要跨平台分层边界Plate 是一个以 shadcn/ui 风格开放代码为特色的富文本编辑器体系其 UI 组件大量来自 shadcn 生态并允许用户直接持有和修改源码。SKILL.md 定义了仓库的四大 UI 表面apps/www/src/registry/ui—— 在线组件与节点渲染器live renderersapps/www/src/registry/components/editor/plugins—— base/live kit 接线apps/www/src/registry/registry-*.ts—— 注册表元数据与依赖packages/*—— 持久的 transforms、queries、controllers 与公开 hooks。其中的核心设计原则之一是“Design below JSX”跨平台复用应当发生在命令/状态契约、controllers、queries 和 transforms 这一层而不是包内持有的 shadcn 组合层。cross-platform.md 就是这条原则的具体落地规则它回答的问题是当你为某个编辑能力媒体、目录、公式等写新代码时每一行代码到底该放在哪一层。三层模型Three-layer model规则文件把新组件能力划分为三个层次按职责从下到上依次是第一层语义核心packages/*/src/lib拥有文档的“语义”与任何具体渲染平台无关transforms—— 对文档结构的修改操作queries—— 对文档结构的查询schema/types—— 节点与数据的类型定义serialization—— 序列化逻辑controllers—— 稳定的控制器command/state contracts—— 命令与状态的契约。在仓库中可以清楚看到这一层的实际形态。以 media 包为例packages/media/src/lib 下组织着BaseImagePlugin.ts、BaseVideoPlugin.ts、BaseMediaEmbedPlugin.ts等插件定义media/parseMediaUrl.ts负责 URL 解析media-embed/parseVideoUrl.ts、parseTwitterUrl.ts负责各家平台的 URL 识别image/transforms/下是insertImage.ts、insertImageFromFiles.ts等文档变换——全部是纯逻辑、无 JSX、可在任意平台复用的语义代码。第二层平台适配器packages/*/src/react规则原文写道packages/*/src/react以及“未来的 native adapter”。该层负责React effects—— 与 DOM/浏览器生命周期同步的副作用DOM observers—— 观察 DOM 状态的逻辑store/context binding—— 把语义核心绑定到 React 的 store 与 contextstable view-model hooks—— 那些“合理推断会有 native 对应物”的稳定视图模型 hook。仓库中有三个被规则点名为“好例子”的 hook它们正是这一层的样板下面逐一拆解。例 1useMediaState—— 稳定的媒体视图模型useMediaState.ts 接收可选的urlParsers类型为EmbedUrlParser[]内部通过useElementTMediaElement TResizableElement()、useFocused()、useSelected()、useReadOnly()等编辑器上下文 hook 读取当前元素与交互状态并基于第一层的parseMediaUrl派生出embed、isVideo、isYoutube、isTweet等字段最终返回return { id, align, embed, focused, isTweet, isUpload, isVideo, isYoutube, name, readOnly, selected, unsafeUrl: url, };这个返回值全部是有明确领域含义的状态对齐方式、是否聚焦/选中/只读、解析后的嵌入信息。它不含任何菜单项数组、弹层开闭状态或文案因此无论未来是 Web 渲染器还是 native 渲染器消费这份契约都成立。例 2useTocElementState—— 目录的领域契约useTocElement.ts 中的useTocElementState组合了第一层的getHeadingList通过useEditorSelector响应式获取、TocPlugin的topOffset选项以及useContentController提供的activeContentId/onContentScroll。它对外暴露的是“当前文档有哪些标题、哪个标题处于激活状态、如何滚动到某个标题”——这是目录功能的核心契约同样不绑定任何具体 UI 形态。例 3useEquationElement—— 典型的 DOM 副作用useEquationElement.ts 展示了第二层中“React effects / DOM observers”的职责它接收katexRefReact.MutableRefObjectHTMLDivElement | null与可选的KatexOptions在element.texExpression变化时调用katex.render(getEquationExpression(element), katexRef.current, options)把 TeX 表达式渲染进真实 DOM 节点。表达式提取逻辑getEquationExpression位于第一层的 getEquationExpression.internal.ts而“把渲染结果写进 DOM”这件事天然属于 React 平台适配器不应下沉到语义核心。第三层Open UIapps/www/src/registry/ui这一层是用户可以直接持有、阅读、diff 和修改的开放代码拥有shadcn composition—— shadcn 风格的组件组合local labels/copy—— 本地化的标签与文案menu/popover/dialog state—— 菜单、弹层、对话框的开闭状态class decisions—— 样式类的决策one-surface event wiring—— 只服务于单个表面的事件接线。media-embed-node.tsx 是三层协作的完整样例它从第二层取状态在第一层解析器之上做 shadcn 组合。export const MediaEmbedElement withHOC( ResizableProvider, function MediaEmbedElement(props: PlateElementPropsTMediaEmbedElement) { const { align center, embed, focused, isTweet, isVideo, isYoutube, readOnly, selected, } useMediaState({ urlParsers: [parseTwitterUrl, parseVideoUrl], }); const width useResizableValue(width); const provider embed?.provider; return ( MediaToolbar plugin{MediaEmbedPlugin} PlateElement classNamepy-2.5 {...props} // …LiteYouTubeEmbed / Tweet / ReactPlayer 等具体 UI可以看到MediaToolbar弹层状态、LiteYouTubeEmbed的长串 Tailwind 类名、maxWidth: isTweet ? 550 : 100%这类展示决策全部留在 app 组件内包只提供了align/embed/focused/selected/readOnly这些跨渲染器成立的字段。同样的模式也出现在 media-image-node.tsx、media-audio-node.tsx、media-video-node.tsx、media-file-node.tsx 等兄弟渲染器中——多个表面消费同一份契约这正是把useMediaState放进包里的正当理由。React 包“可以拥有”什么规则给出的判定标准是一句话React 包中的 hook 只有在暴露“稳定能力契约”时才合法而不是为某个组件的私有渲染器提供胶水renderer glue。被点名的好例子及其共同特征Hook来源拥有的领域含义useMediaStatepackages/media/src/react/media/useMediaState.ts媒体节点的对齐、选区/焦点状态、URL 嵌入解析结果useTocElementStatepackages/toc/src/react/hooks/useTocElement.ts标题列表、激活标题 ID、滚动到标题的控制器useEquationElementpackages/math/src/react/hooks/useEquationElement.ts把元素表达式同步渲染进 KaTeX DOM 节点的平台 effect它们的共性是返回值表达的是文档/功能域的状态与操作而不是某个 TSX 文件的界面细节。React 包“不可拥有”什么规则文件明确列出了不得放进包级 React hook 的内容menu item arrays—— 菜单项数组shadcn popover open state—— 弹层开闭状态labels/copy—— 标签与文案one renderers class decisions—— 某个渲染器的样式类决策one components local recovery buttons—— 某个组件的本地恢复按钮a bag of props that only one renderer consumes—— 只被一个渲染器消费的 props 集合。并给出一条极其实用的检验标准原文If the hook mostly exists to make one TSX file shorter, it does not belong in the package. 如果一个 hook 的主要存在意义只是让某个 TSX 文件变短那它就不属于包。SKILL.md 中给出了对应的反模式示例帮助读者识别这类“渲染器胶水 hook”// Bad: 包级 hook 只为喂给一个 shadcn 组件的局部 UI const state useSingleComponentOnlyState(); return Popover open{state.open}.../Popover; // Bad: 返回的全是渲染器胶水文案、菜单、弹层状态 const { dialogTitle, menuItems, onOpenChange, popoverOpen, } useToolbarMenuState();与之相对“Major-Release Law”见 SKILL.md进一步把它升级为版本策略主要以渲染器 UI props/状态为返回值的包级 React hook 应当被废弃并迁回 app 本地包层只保留跨平台的语义/视图模型契约即使某个既有 hook 已经违反这条规则也不要在新代码中模仿它——在 SKILL.md 的术语中这类 hook 被视为“迁移债migration debt”而非先例。10tap 经验应该借鉴什么、避免什么规则文件把10tap-editor作为跨平台编辑器架构的参照系给出了明确的取舍清单借鉴Copystable command/state contracts—— 稳定的命令/状态契约extension-owned capabilities—— 由扩展plugin拥有的能力而不是由 UI 拥有的能力UI composition on top—— 让 UI 组合建立在契约之上。不要借鉴Do not copya monolithic bridge as the only API—— 把巨型单体桥接对象作为唯一 APIhiding ordinary UI composition in the shared layer—— 把普通 UI 组合藏进共享层forcing web and native to share presentational hooks—— 强迫 Web 与 native 共享“展示层 hook”。从源码结构看Plate 的当前形态与这份清单高度一致editor.getApi(plugin)/editor.getTransforms(plugin)这类按插件取能力的方式见 SKILL.md 的 “direct plugin access, no local wrapper layer” 示例对应“extension-owned capabilities”而useMediaState这类细粒度 hook 对应“UI composition on top”仓库刻意避免了单一巨型 bridge 对象作为包级唯一出口。实践清单新增一个组件能力时如何定位代码结合规则文件与 SKILL.md 的提取测试Extraction Test可以把判断流程压缩为几个问题这段代码拥有文档语义吗schema、serialization、transforms、导航契约→ 是则放入packages/*/src/lib多个 UI 表面或多个平台需要同一个行为契约吗→ 是则可以放在packages/*/src/react作为稳定视图模型 hook返回值是否以标签、JSX 接线、样式类决策或弹层/菜单状态为主→ 是则留在apps/www/src/registry/ui的 app 组件内未来 native 消费方能合理复用这份契约吗→ 不能合理复用的 React/Web 专属抽象不要进入包层如果提取的唯一动机是“这个文件太长了”或“类型太麻烦”放弃提取。以 media 包为例验证这条清单parseMediaUrl、parseVideoUrl等解析器留在lib第 1 条useMediaState返回的字段被 image/video/audio/file/embed 五个渲染器共同消费第 2 条成立而MediaToolbar、ResizeHandle、tweet 的maxWidth: 550等展示细节全部留在 app 组件第 3 条三层各司其职且每一层都保留了 shadcn 风格开放代码的可读性。小结cross-platform.md 虽然篇幅不长却定义了 Plate UI 体系最关键的架构纪律语义核心packages/*/src/lib拥有文档事实平台适配器packages/*/src/react只做稳定的跨平台视图模型与 DOM 副作用Open UIapps/www/src/registry/ui承载全部 shadcn 组合、文案与样式决策。配合 SKILL.md 的提取测试与“Major-Release Law”这套规则让 Plate 在保持 shadcn 开放代码体验的同时为未来的 native 端复用预留了清晰的契约边界——任何包级 React hook都应该先回答“我暴露的是能力契约还是一个 TSX 文件的私有胶水”。【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表