Plate 组件提取模式审计:如何划分插件包 Hook 与应用层 shadcn 组合的边界
Plate 组件提取模式审计如何划分插件包 Hook 与应用层 shadcn 组合的边界【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate本篇技术指南以 Plate 仓库中 .agents/rules/plate-ui/references/component-audit.md 组件审计为骨架系统梳理在当前仓库中验证过的包提取package extraction最佳实践。你将掌握三组经过真实代码验证的提取样板Media、TOC、Equation、base/live 静态与实时渲染分离模式、直接编辑器访问的取舍标准以及注册表接线时的依赖提醒从而在自己的插件开发中做到语义进包、UI 留在应用层。审计结论总览默认立场组件审计为 Plate UI 的所有组件提取工作确立了一条核心分界线可以概括为两句话提取语义extract semantics凡是代表编辑器真实状态、可复用逻辑契约的内容应下沉到包内 Hook。保持 shadcn 组合开放keep shadcn composition open工具栏、弹层、按钮、样式等 shadcn 风格 UI 组合应留在应用apps/www层由开发者自由拼装。这一立场直接对应仓库的物理结构packages/*提供无 UI 依赖的 Hook 与插件逻辑apps/www/src/registry/ui/*与apps/www/src/registry/components/editor/plugins/*提供面向最终用户的可组合组件。凡越过这条线、把只服务某一个 UI 表面的渲染属性/状态塞进包 Hook 的做法在审计中被明确标注为警告项而非可复制的先例。优秀包提取模式三个已验证样板审计文档给出了三个可以照抄的包提取样板每个都遵循同一结构包 Hook 拥有状态契约应用组件负责视觉组合。Media包 Hook 持有真实媒体/编辑器状态应用层组件apps/www/src/registry/ui/media-image-node.tsx包内 Hookpackages/media/src/react/media/useMediaState.tsuseMediaState是这一模式的典型代表。它通过useEditorRef、useElement、useFocused、useSelected、useReadOnly等 platejs/react 基础 Hook把媒体节点的真实编辑状态聚合为一份稳定的返回值包括id、align、focused、isUpload、isVideo、isYoutube、readOnly、selected、unsafeUrl等对于视频与媒体嵌入类型还会基于 URL 解析出embed信息并推断isTweet/isVideo/isYoutube。应用层组件则完全站在组合者的位置上用useMediaState()一次性取回全部媒体状态用useDraggable来自platejs/dnd接入拖拽用useResizableValue(width)读取可调整宽度自行组合MediaToolbar、Resizable、ResizeHandle、Caption/CaptionTextarea等 shadcn 风格 UI并通过mediaResizeHandleVariants应用本地按钮样式。注意组件末尾的withHOC(ResizableProvider, ...)包装以及contentEditable{false}的 figure 结构——这些都属于表面组合细节被刻意留在应用层。包只保证状态是对的至于图片长什么样、工具栏放哪、标题怎么写全部由应用层决定。TOC包 Hook 持有稳定的导航契约应用层组件apps/www/src/registry/ui/toc-node.tsx包内 Hookpackages/toc/src/react/hooks/useTocElement.tsTOC目录组件的提取边界更加清晰useTocElementState负责收集稳定的导航状态useTocElement负责生成交互契约。状态侧useTocElementState通过useEditorPlugin(TocPlugin)获取编辑器与插件选项含topOffset用useEditorSelector(getHeadingList, [])订阅标题列表再经useContentController借助IntersectionObserverisObserve: true、rootMargin: 0px 0px 0px 0px跟踪当前激活标题最终返回activeContentId、headingList、onContentScroll。交互侧useTocElement返回的props.onClick封装了完整的点击跳转逻辑阻止默认行为 → 通过NodeApi.get(editor, path)定位节点 →editor.api.toDOMNode(node)拿到 DOM 元素 → 调用onContentScroll平滑滚动。应用层组件则只做两件事用cva定义headingItemVariants不同深度pl缩进、激活态高亮样式以及把headingList渲染成Button列表空列表时给出Create a heading to display the table of contents.的占位提示。导航契约在包里行渲染与本地按钮样式在应用里。Equation包 Hook 只做一件持久的事——KaTeX 渲染应用层组件apps/www/src/registry/ui/equation-node.tsx包内 Hookpackages/math/src/react/hooks/useEquationElement.ts这是三个样板中职责最窄的一个。useEquationElement的完整实现只有一个useEffect当element.texExpression变化时调用katex.render(getEquationExpression(element), katexRef.current, options)。Hook 接收element、katexRef一个挂在目标 div 上的 ref和可选的KatexOptions不返回任何状态只负责副作用。所有 UI 组合都在应用层EquationElement自己维护选中态open、katexRef、lineBreakBadge传入 KaTeX 选项displayMode: true、errorColor: #cc0000、macros、output: htmlAndMathml、strict: warn、throwOnError: false等并组合Popover/PopoverContent/PopoverTrigger、Button、TextareaAutosize完成点击公式弹出编辑面板的完整交互。把这三个样板放在一起看模式是高度一致的包 Hook 的返回值要么是纯编辑器状态Media要么是稳定契约TOC要么是零返回值的渲染副作用EquationUI 弹层、工具栏、按钮、样式永远归应用层。跨平台方向10tap 参考的取舍审计文档同时给出了跨平台方向的参考——来自 10tap 编辑器的三份文件../10tap-editor/src/types/EditorBridge.ts、../10tap-editor/src/bridges/core.ts、../10tap-editor/src/RichText/useEditorBridge.tsx。需要说明的是这三份文件不在当前仓库内属于外部方向性参考因此下面仅转述审计文档的取舍结论不涉及仓库证据。要复制What to copy稳定的命令/状态契约放在 UI 之下stable command/state contract below UI基于扩展的能力模型extension-owned capability model——能力由扩展声明和拥有而不是由某个全局对象包办。不要照抄What not to copy literally把单体桥monolithic bridge作为唯一 API——它会让所有能力集中在单一入口违背 Plate 按插件拆分的理念包内持有 UI 组合package-owned UI composition——这正好与默认立场提取语义、保持 shadcn 组合开放相悖。也就是说10tap 的价值在于其契约先于 UI的架构思路而不是其具体实现形态。在 Plate 中这个契约落地为useEditorPlugin(plugin)返回的编辑器句柄与editor.getApi(plugin)/editor.getTransforms(plugin)能力访问方式见下文。base/live 分离静态与实时渲染器共用一套逻辑审计文档推荐对同时存在静态渲染与实时渲染的新表面复制 base/live 分离模式仓库中的四份文件是现成范例apps/www/src/registry/components/editor/plugins/footnote-base-kit.tsx 与 footnote-kit.tsxapps/www/src/registry/components/editor/plugins/math-base-kit.tsx 与 math-kit.tsx以math-base-kit.tsx为例其实现非常短import { BaseEquationPlugin, BaseInlineEquationPlugin } from platejs/math; import { EquationElementStatic, InlineEquationElementStatic, } from /registry/ui/equation-node-static; export const BaseMathKit [ BaseInlineEquationPlugin.withComponent(InlineEquationElementStatic), BaseEquationPlugin.withComponent(EquationElementStatic), ];可见 base 变体的本质是同一个插件通过withComponent挂上静态渲染组件如EquationElementStatic用于导出 HTML、Markdown 序列化、SEO 预览等非交互场景而 live 变体math-kit.tsx则挂上可交互的实时组件如equation-node.tsx。静态与实时之间共享同一套插件逻辑与状态 Hook只是渲染器不同。这种分离带来两个直接收益静态导出不会携带任何交互副作用不触发聚焦、弹层、编辑器订阅性能与确定性更好新增一个既有静态又有实时渲染器的表面时只需要复制一份 base 数组 一份 live 数组的结构分别注册到对应 registry 条目即可。直接编辑器访问useEditorPlugin 与 getApi/getTransforms并非所有组件都需要包 Hook。审计文档指出两个应用层节点组件展示了更轻量的模式apps/www/src/registry/ui/comment-node.tsxapps/www/src/registry/ui/link-node.tsx复制规则如下当整个文件以某个插件为中心时使用useEditorPlugin(plugin)——它同时给出editor与getOptions还建立了插件级别的订阅关系当只需要某个插件暴露的能力时使用editor.getApi(plugin)访问该插件 API或editor.getTransforms(plugin)访问该插件变换——这比包一层 Hook 更简单直接。这条规则的实质是按需取用包 Hook 的价值在于把一段需要组合多个基础 Hook 的复杂状态逻辑收拢并复用如果组件只是要调用插件提供的某个变换直接用editor.getTransforms(plugin)即可不必为一次调用建立 Hook 抽象。Registry 接线提醒不要漏掉样式依赖包提取完成后组件要进入 shadcn registry 才能被消费。审计文档特别提醒关注两个注册表清单文件apps/www/src/registry/registry-kits.tsapps/www/src/registry/registry-examples.tsregistry-kits.ts中每条 kit 条目的结构示例以align-base-kit为例如下{ dependencies: [platejs/basic-styles], files: [ { path: components/editor/plugins/align-base-kit.tsx, type: registry:component, }, ], name: align-base-kit, registryDependencies: [], type: registry:component, }其中dependencies是 npm 包依赖registryDependencies是 registry 内其他条目的依赖。接线时最容易犯的错误是当组件或示例使用了共享的高亮 tokenhighlight tokens时忘记声明highlight-style之类的样式依赖导致组件安装后高亮样式缺失。审计文档的措辞是不要忘记样式依赖如highlight-style当组件或示例使用共享高亮 token 时Do not forget style deps likehighlight-stylewhen a component or example uses shared highlight tokens。因此每次新增或修改 kit/example 后都应同时检查registry-kits.ts与registry-examples.ts中对应条目的依赖是否完整。当前注意事项与未来立场审计文档没有回避现有代码中的问题明确指出仓库中存在一些组件可能把只服务一个 UI 表面的内容过度提取进了包 Hook。对待这些旧代码的态度是当作警告而不是先例Treat that as a warning, not a precedent。在此基础上两个立场的完整表述为默认立场Default stance提取语义保持 shadcn 组合开放。主版本立场Major-release stance主要返回渲染器专属 UI props/state 的 React 包 Hook 属于迁移债务migration debt未来工作应避免新增这类 Hook——即使旧代码仍然包含它们。换句话说判断一个 Hook 是否过度提取的标准是它的返回值是否只对某一个渲染器有意义。如果一个包 Hook 返回的大多是某个组件的专属 props 或本地 UI 状态那么它不该留在包里正确的做法是让应用层组件自己持有这些本地状态正如EquationElement自行维护open选中态而包只保留编辑器语义如useMediaState的聚焦/选中/只读/对齐与稳定契约如 TOC 的标题列表与滚动回调。实践清单如何在你的插件中落地这套审计结论综合全文可以沉淀为一份可直接对照的检查清单先问归属这段逻辑是编辑器语义/契约还是某一个 UI 表面的渲染属性前者进包后者留在应用组件。有状态就给包 Hook需要组合useElement/useFocused/useSelected/useReadOnly等多基础 Hook 才能得出的状态封装成use*State形式参考 useMediaState.ts。交互契约也给包 Hook点击跳转、滚动定位等与编辑器 DOM 强相关的交互封装为use*Element返回 props参考 useTocElement.ts。单一副作用也可以进包如 KaTeX 渲染这种只做一件持久的事的副作用适合放进包 Hook参考 useEquationElement.ts。有静态实时双渲染器时做 base/live 分离base 用静态组件live 用交互组件共享同一插件逻辑参考 math-base-kit.tsx。能直接访问就别包一层整文件以插件为中心用useEditorPlugin(plugin)只要单个能力用editor.getApi(plugin)/editor.getTransforms(plugin)参考 comment-node.tsx 与 link-node.tsx。接线时核对依赖新增组件/示例后检查 registry-kits.ts 与 registry-examples.ts 中的 npm 依赖、registry 依赖与样式依赖如highlight-style是否齐全。不复制旧的反模式遇到包 Hook 返回渲染器专属 UI props/state的旧代码视其为迁移债务新代码一律避免。【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考