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

资讯详情

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

Tiptap 静态渲染器 @tiptap/static-renderer 深度指南:无需 Editor 实例,把 Tiptap JSON 一键渲染为 HTML、React 与 Markdown

Tiptap 静态渲染器 @tiptap/static-renderer 深度指南:无需 Editor 实例,把 Tiptap JSON 一键渲染为 HTML、React 与 Markdown Tiptap 静态渲染器 tiptap/static-renderer 深度指南无需 Editor 实例把 Tiptap JSON 一键渲染为 HTML、React 与 Markdown【免费下载链接】tiptapThe headless rich text editor framework for web artisans.项目地址: https://gitcode.com/GitHub_Trending/ti/tiptap导读tiptap/static-renderer是 Tiptap 生态中专用于“无头渲染”的包它不实例化Editor、不挂载 DOM、不运行动态插件与事务流而是直接基于扩展列表构建 ProseMirror schema把一份 Tiptap JSON 文档或 ProseMirrorNode按每个扩展声明的renderHTML静态转换成 HTML 字符串、React 元素乃至 Markdown 文本。它非常适合 SEO 预渲染、SSR/SSG 文章页、邮件模板、导出功能以及任何“只读输出文档”的场景。读完本文你将掌握它的全部公开入口、渲染管线原理、已知局限如UniqueID、TableOfContents属性缺失与官方推荐的前置预处理方案并能利用nodeMapping/markMapping自定义任意节点的输出形态。一、包定位从编辑器到纯静态输出的桥梁仓库内 packages/static-renderer/package.json 将本包描述为statically render Tiptap JSON声明了关键词tiptap static renderer。其 README.md 开头也点明了项目背景Tiptap is a headless wrapper around ProseMirror – a toolkit for building rich text WYSIWYG editors.Tiptap 的本质是基于 ProseMirror 的无头富文本编辑器框架编辑器运行时负责“编辑”而当你只需要把编辑器产出JSON 文档渲染成最终输出时——例如博客服务端渲染、静态站点、导出 HTML 时——真正需要的是一个可以脱离编辑器生命周期运行的纯函数式渲染器这正是tiptap/static-renderer的定位。安装与入口点该包支持 CommonJS 与 ESM 双格式main指向dist/index.cjsmodule指向dist/index.js并以exports字段划分了 6 个官方入口入口导出内容适用场景tiptap/static-renderer全部常用导出聚合大多数日常使用默认入口tiptap/static-renderer/pm/html-stringrenderToHTMLString、domOutputSpecToHTMLString输出 HTML 字符串tiptap/static-renderer/pm/reactrenderToReactElement、mapAttrsToHTMLAttributes输出 React 元素tiptap/static-renderer/pm/markdownrenderToMarkdown输出 Markdown 文本tiptap/static-renderer/json/html-string底层TiptapStaticRenderer与escapeHTML/serialize*字符串工具自建字符串渲染器tiptap/static-renderer/json/react底层类型与renderJSONContentToReactElement自建 React 渲染器以包名tiptap/static-renderer/pm/html-string为例README 的核心示例即如此导入其顶层导出由 src/index.ts 统一汇聚了helpers、json/html-string、json/react、pm/html-string、pm/markdown、pm/react六组模块。源码目录上呈现为两组分工src/pm/*ProseMirror 域是面向最终用户的“构建 schema 再渲染”封装src/json/*纯 JSON 域则提供底层的渲染器基座与类型定义被 pm 层复用。peer 依赖方面package.json需要tiptap/core与tiptap/pm同 workspace 源码工程内为workspace:*若使用 React 相关入口还需自行安装react/react-dom支持^17 || ^18 || ^19。二、核心设计只跑 renderHTML不实例化 Editor要理解本包能做什么、不能做什么必须抓住 README.md 中“Limitations workarounds”一节的关键定义The static renderer builds the ProseMirror schema and runs each extensionsrenderHTML, but it doesnotinstantiate anEditor.即它的工作方式是用扩展数组构建出 schema然后逐个调用扩展的renderHTML把节点/标记序列化为 DOMOutputSpec编辑器实例从头到尾都不会被创建。对照 src/pm/extensionRenderer.ts 中的renderToElement所有 pm 渲染器的公共底座渲染管线可以概括为resolveExtensions(extensions)规范化扩展数组并依次执行getAttributesFromExtensions、splitExtensions把扩展拆成节点扩展与标记扩展resolveRenderContent把输入内容解析为 ProseMirrorNode详见第四节遍历节点/标记扩展通过mapNodeExtensionToReactNode/mapMarkExtensionToReactNode取到每个扩展的renderHTML字段用getExtensionField据此为每种 node/mark 类型生成渲染函数合并进doc/text等内置映射与用户提供的覆盖映射交给底层TiptapStaticRenderer做递归渲染。其中步骤 3 的实现位于 src/pm/extensionRenderer.ts调用renderHTML时传入{ node/mark, HTMLAttributes }而HTMLAttributes由getHTMLAttributessrc/helpers.ts根据扩展属性中rendered: true的那部分计算而来renderHTML的返回值如[div, { class: x }, 0]这样的 DOMOutputSpec 数组再交给domOutputSpecToElement转成目标类型。也正因没有编辑器README 明确列出了不会执行的动态机制addProseMirrorPlugins返回的 ProseMirror 插件例如拖动把手、占位符、选择装饰不会安装onCreate、onUpdate及各类 transaction 钩子不会触发因而任何依赖这些机制在文档上“事后补写属性”的扩展其属性在静态输出中会缺失。三、已知局限与标准 Workaround先预处理 JSON再渲染这是 README.md 中篇幅最重、实战价值最高的部分。典型受影响扩展有二UniqueID负责在文档上生成唯一data-id其逻辑运行在插件/事务机制中TableOfContents负责为标题生成id与data-toc-id同样依赖运行时机制。静态渲染跳过这些机制后输出里自然看不到这些属性。官方给出的方案是在渲染前对 JSON 文档做函数式预处理——这也是本仓库特意为这两个扩展暴露纯函数的原因generateUniqueIds(doc, extensions)定义于 packages/extension-unique-id/src/generate-unique-ids.ts签名(doc: JSONContent, extensions: Extensions) JSONContentgenerateTocIds(doc, extensions)定义于 packages/extension-table-of-contents/src/generate-toc-ids.ts其文档注释示例即为generateTocIds(doc, [StarterKit, TableOfContents])。两者都与渲染端约定一致传入同一份extensions数组让预处理能依据扩展配置如 ID 前缀、属性名计算出与编辑器中一致的属性然后原样返回可安全渲染的新 JSON。README 给出的完整链路如下已按原文完整呈现import { generateUniqueIds } from tiptap/extension-unique-id import { generateTocIds } from tiptap/extension-table-of-contents import { renderToHTMLString } from tiptap/static-renderer/pm/html-string let doc sourceJson doc generateUniqueIds(doc, extensions) // if using UniqueID doc generateTocIds(doc, extensions) // if using TableOfContents const html renderToHTMLString({ content: doc, extensions, staticEditorOptions: { textDirection: auto }, // mirrors a subset of EditorOptions })要点拆解两条预处理是可选的、按需组合的只用UniqueID就只调generateUniqueIds不用的步骤可以省略处理是串行叠加的generateTocIds输入的是上一步已带data-id的文档先后顺序不敏感因为它们写的是不同属性extensions是整套用于渲染的扩展如StarterKit 上述扩展保证“配置如何、输出如何”的一致性。该局限在源码中的印证src/pm/extensionRenderer.ts 的StaticEditorOptions类型注释、src/pm/html-string/html-string.ts 的函数头注释都重复声明了同一段约束静态渲染器“builds the schema and runs each extensionsrenderHTMLbut does not instantiate anEditor”并明确把generateUniqueIds/generateTocIds作为受支持的补救路径。仓库测试 packages/static-renderer/tests/json-string.spec.ts 中也有直接断言extensionsWithUid、extensionsWithToc场景保证“预处理后属性出现在 HTML 中”这一契约不回归。四、staticEditorOptions可复现的编辑器级选项子集因为编辑器选项大多依赖运行时 view 或事务流静态渲染不可能全部复刻。仓库选择的策略README 原文Editor-level options that affect output are accepted via thestaticEditorOptionsobject (currentlytextDirection). Other editor options that depend on a runtime view or transaction stream are out of scope.即凡是会直接影响最终输出、且不需要运行时即可复现的编辑器选项才被收进staticEditorOptions目前仅一项staticEditorOptions?: { textDirection?: ltr | rtl | auto }类型定义见 src/pm/extensionRenderer.ts。它在源码中的处理逻辑是applyStaticEditorOptionsToExtensionssrc/pm/extensionRenderer.ts当提供了textDirection时会把这个选项驱动的TextDirection扩展前置到用户扩展数组最前面从而与new Editor({ textDirection })行为对齐——若用户扩展里自带TextDirection位于其后按 Tiptap “同名扩展后者优先”的覆盖规则用户的配置仍然生效。该函数随后会经由renderToElement应用到每次渲染。两点值得注意的边界源码注释明确标注它只检查顶层扩展若某个TextDirection被藏在 StarterKit 之类的 kit 内部不会被检测用于覆盖判断目前仓库内没有内置TextDirection的 kit因此这更多是理论边界见 src/pm/extensionRenderer.ts未设置textDirection时函数直接原样返回扩展数组零额外开销。五、输出 HTML 字符串renderToHTMLString 的底层机制renderToHTMLString定义于 src/pm/html-string/html-string.ts签名如下renderToHTMLString({ content: Node | JSONContent, extensions: Extensions, staticEditorOptions?: StaticEditorOptions, options?: PartialTiptapStaticRendererOptionsstring, Mark, Node, }): string其中content接受 Tiptap JSONJSONContent或已经构造好的 ProseMirrorNode两种形态统一走renderToElement管线并把doc文档根映射为“拼接所有子节点”、text映射为转义后的文本内容escapeHTML会把、、分别转义为amp;、lt;、gt;src/json/html-string/string.ts文本节点输出天然免疫注入属性序列化serializeAttrsToHTMLString会跳过值为null/undefined的属性对齐 ProseMirrorDOMSerializer.renderSpec行为避免产生classnull这类脏输出并对属性值再做引号转义quot;src/json/html-string/string.ts。DOMOutputSpec 到字符串的转换每个扩展的renderHTML返回的是一条 ProseMirrorDOMOutputSpec形如[a, { href }, 0]、[span, { class }, text, 0]或纯文本字符串。domOutputSpecToHTMLStringsrc/pm/html-string/html-string.ts将其编译为一个“接收 children 返回 HTML 片段”的函数处理细节包括带命名空间的标签当tag内含空格ProseMirror 语法namespace tag时改写为tag xmlnsnamespacechildren 占位符0DOMOutputSpec中的数字0表示“这里插入节点的渲染子内容”序列化时在对应位置注入空元素/自闭合策略无子节点的普通标签输出自闭合tag/但对iframe、script、style、title、textarea、div、span、a、button这一组 HTML 规范不允许自闭合的标签强制输出成对闭合tag/tag常量NON_SELF_CLOSING_TAGSsrc/pm/html-string/html-string.ts未知 spec 报错遇到既非字符串也非数组的 spec会抛出带cause的明确错误提示检查renderHTML的返回或提供节点映射。六、输出 React 元素renderToReactElement 与属性映射当目标不是字符串而是 React 组件树时例如在 Next.js 页面里把文章 JSON 直接渲染为 React 元素便于复用组件与 hooks使用renderToReactElementsrc/pm/react/react.tsrenderToReactElement({ content: Node | JSONContent, extensions: Extensions, staticEditorOptions?: StaticEditorOptions, options?: PartialTiptapStaticRendererOptionsReact.ReactNode, Mark, Node, }): React.ReactNode其与字符串渲染唯一的本质区别在于domOutputSpecToElement与mapDefinedTypesdoc被渲染为React.Fragment避免多出多余的包裹 DOMtext直接返回node.textdomOutputSpecToReactElement把每条DOMOutputSpec编译成React.createElement调用。由于 React 的 JSX 属性名与 HTML 属性名存在差异DOMOutputSpec 中的属性需要经过mapAttrsToHTMLAttributessrc/pm/react/react.ts做适配规则包括class→className字符串形式的style如color: red; font-size: 12px会被解析成驼峰命名的 React style 对象colspan→colSpan、rowspan→rowSpan映射表HTML_ATTRIBUTE_TO_REACT_ATTRIBUTE为每个元素注入递增的key。一个完整的端到端示例可参考仓库演示 demos/src/Examples/StaticRenderingAdvanced/React/index.tsx它通过options.nodeMapping把heading换成可点击且能使用 React hooks 的自定义组件并用ReactNodeViewContentProvider 自定义组件组合来“模拟”那些依赖 node view 的原子节点直观展示了“静态渲染器不执行 node views但你可以在映射层手工提供组件”这一替代路径。同一类演示还包括 demos/src/Examples/StaticRendering/React/index.tsx、demos/src/GuideContent/StaticRenderHTML 与 demos/src/GuideContent/StaticRenderReact。七、渲染器的扩展点nodeMapping / markMapping / unhandledNode / unhandledMark无论输出到字符串还是 React所有 pm 渲染器都通过第四个入参options暴露TiptapStaticRendererOptions的四个控制面类型定义见 src/json/renderer.ts选项作用优先级nodeMapping按节点名覆盖/新增渲染函数例如heading、image最高覆盖扩展派生的默认映射markMapping按标记名覆盖渲染函数例如bold、link最高unhandledNode对 schema 中不存在的节点类型的兜底渲染仅当 JSON 引用了 schema 外的类型unhandledMark对 schema 中不存在的标记类型的兜底渲染仅当 JSON 引用了 schema 外的类型其中覆盖映射的合并顺序src/pm/extensionRenderer.ts为扩展自动生成的映射 → 内置doc/text映射 → 用户nodeMapping→ 占位类型路由。即用户的映射永远排在最后、获得最高优先级想替换任何节点的默认renderHTML输出都非常直接。递归渲染器与惰性 children底层的TiptapStaticRenderersrc/json/renderer.ts是所有渲染器的核心它接收一个renderComponent与映射表返回renderContent递归函数。每个渲染器函数接收的NodeProps/MarkProps语义如下NodePropsnode当前节点、parent父节点根节点外恒有值、children惰性 getter首次访问时才递归渲染子节点避免无关子树的白白计算、renderElement手动渲染某个子内容MarkPropsmark、node/parent被标记包裹的节点、children。标记的处理方式是对节点渲染结果做 reduce 层层包裹content.marks按顺序把每个 mark 的渲染函数套到已渲染的子结果外层因此[text, marks: [bold, italic]]会得到bold(italic(内容))的嵌套输出与 ProseMirror 的标记模型一致src/json/renderer.ts。若某个节点或标记类型在映射中找不到 handler会抛出missing handler for node type ...的明确错误。未知类型兜底placeholder 替换机制当你传入的 JSON 里引用了扩展 schema 中不存在的节点/标记类型例如历史旧文档、跨版本内容pm 渲染器并不会直接崩溃。resolveRenderContentsrc/pm/extensionRenderer.ts会先用hasUnknownType遍历检测若发现未知类型且你提供了对应的unhandledNode/unhandledMark则构造一个包含内部占位类型__tiptapUnhandledNode__/__tiptapUnhandledMark__的 schema 副本withPlaceholderTypes把未知类型节点替换为携带原始type/attrs的占位节点substituteUnknownTypes渲染到占位类型时通过 ProxywithOriginalIdentity向兜底函数暴露原类型名{ name }、原始 attrs 以及能还原原始 JSON 的toJSON()src/pm/extensionRenderer.ts。因此兜底渲染器里只能依赖type.name/attrs/toJSON()不要依赖type.spec、type.schema、isInline等真实类型信息。反之若没有提供兜底函数未知类型仍会照常抛出 schema 解析错误这属于有意保留的“显式失败”行为。相关行为在 packages/static-renderer/tests/pm-unhandled-types.spec.ts 中有完整覆盖含兜底成功、缺失 handler 抛错、markdown/HTML/React 三种输出的场景断言。缺失 renderHTML 的扩展若某节点/标记扩展根本没有定义renderHTML且用户未通过nodeMapping覆盖则生成的渲染函数会在调用时抛出带指引信息的错误“……请实现 renderHTML 或覆盖对应 nodeMapping”src/pm/extensionRenderer.ts若提供了unhandledNode则这类扩展会回落到该兜底函数。八、renderToMarkdown 与“任意输出格式”的灵活性静态渲染器的架构价值不限于 HTML/React。src/pm/markdown/markdown.ts 提供的renderToMarkdown只是“以 markdown 演示扩展灵活性”的示例实现源码注释明确写道This is not a full implementation of a markdown renderer它本质上是对renderToHTMLString的复用不更换底层管线只通过nodeMapping/markMapping注入一套 markdown 语法渲染函数例如heading→#.repeat(level) childrencodeBlock→ 带attrs.language的围栏代码块bulletList/listItem→- itemorderedList/listItem→ 依父级索引编号的N. itemlink→textimage→althighlight→…bold→**…**等甚至利用TableMap来自tiptap/pm/tables计算真实列数为含colspan/rowspan的表格补齐空白占位、保持 markdown 网格矩形src/pm/markdown/markdown.ts。这个包给你的想象空间是只要你能写出目标格式的 node/mark 渲染函数就能基于同一条 schema 管线输出任意格式——这正是它被设计成“渲染器基座 映射表”的原因。九、典型工作流与测试保障把以上机制拼装起来一条典型的生产流程是编辑阶段用户用Editor配UniqueID、TableOfContents等扩展产生 JSON 文档并持久化预处理阶段静态输出前按需执行generateUniqueIds(doc, extensions)与generateTocIds(doc, extensions)补齐运行时才会生成的属性渲染阶段调用renderToHTMLStringSEO/邮件/导出或renderToReactElement页面内只读展示可配合nodeMapping注入自定义组件或renderToMarkdown传入同一份extensions与需要的staticEditorOptions.textDirection。这四个环节共用同一份扩展配置因此从编辑到静态输出的语义天然一致这是该包最关键的工程收益。仓库内为这套行为提供了成体系的自动化保障值得读者对照源码深入学习packages/static-renderer/tests/json-string.spec.tsHTML 字符串输出的节点/标记/属性/转义断言含 UniqueID/TableOfContents 预处理场景packages/static-renderer/tests/react-string.spec.tsReact 元素输出与属性映射断言packages/static-renderer/tests/md-string.spec.tsmarkdown 输出含表格断言packages/static-renderer/tests/pm-unhandled-types.spec.ts未知类型兜底机制断言。十、小结一句话总结tiptap/static-renderer的取舍它用“放弃编辑器运行时”换来了“零 DOM、零生命周期、纯函数”的确定性输出——schema 照建、renderHTML照跑但插件、onCreate/onUpdate、事务钩子一律不执行。由此带来的属性缺失问题UniqueID的data-id、TableOfContents的id/data-toc-id已由官方预处理函数generateUniqueIds/generateTocIds闭环解决影响输出的编辑器级选项则收敛为staticEditorOptions.textDirection这个明确子集。配合nodeMapping/markMapping/unhandled*四个控制面HTML、React、Markdown 乃至自定义格式都能在同一套扩展语义下稳定产出。本包遵循 MIT 许可见 LICENSE.md其完整说明文档Introduction、Limitations workarounds、License 等章节位于 packages/static-renderer/README.mdTiptap 全量官方文档在项目官网持续维护可与此处源码互为印证。【免费下载链接】tiptapThe headless rich text editor framework for web artisans.项目地址: https://gitcode.com/GitHub_Trending/ti/tiptap创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表