拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Tiptap React 官方集成包演进全解:@tiptap/react 的 Decorations、组件化 API 与渲染性能实践

Tiptap React 官方集成包演进全解tiptap/react 的 Decorations、组件化 API 与渲染性能实践【免费下载链接】tiptapThe headless rich text editor framework for web artisans.项目地址: https://gitcode.com/GitHub_Trending/ti/tiptap本文以仓库内 packages/react/CHANGELOG.md 为骨架系统梳理tiptap/react当前版本 3.30.3在 v3 时代引入的 Decorations 装饰 API、Tiptap /声明式组件、React MarkView / Widget 渲染器、Floating UI 菜单迁移、SSR 渲染策略与 NodeView 性能优化等关键能力。读完你将掌握如何在 React 应用中用官方 React 绑定搭建编辑器、用组件渲染装饰层与行内标记视图并理解immediatelyRender、trackNodeViewPosition、widgetkey等选项背后的设计动机与正确用法。一、tiptap/react 包概况与演进主线tiptap/react是 Tiptap 官方提供的 React 绑定层。从 package.json 可以看到它当前版本为 3.30.3类型为 ESM 模块type: module同时通过exports字段暴露两个子入口.—— 主入口包含编辑器组件、Hooks 与各种 Renderer./menus—— 菜单入口BubbleMenu/FloatingMenu3.20.0 起被独立出来以便把 floating-ui 保持为可选依赖。其依赖仅types/use-sync-external-store、fast-equals与use-sync-external-store与tiptap/core、tiptap/pm以 workspace 形式保持同版本发布peerDependencies 支持 React 17/18/19。从 src/index.ts 可见包的完整导出面包括EditorContent、NodeViewContent、NodeViewWrapper、ReactNodeViewRenderer、ReactRenderer、ReactMarkViewRenderer、ReactWidgetRenderer、Tiptap、useEditor、useEditorState、useReactNodeView等并透传导出tiptap/core的全部符号。该文件首行是use client指令——这是 3.27.4 引入的改动让tiptap/react可在 React Server Components 环境中被安全导入而不崩溃。纵览 CHANGELOG 从 3.0.1 到 3.30.3 的演进主线可以概括为四件事渲染层革命引入 MarkView标记自定义视图、Widget装饰组件与新的 Decorations API让“不改文档即可改外观”成为一等公民组件化声明式 API从命令式useEditor EditorContent扩展出Tiptap /复合组件菜单系统底层替换移除 tippy.js 全面迁往 Floating UI并持续补全定位与 DOM 挂载能力React 与 ProseMirror 双渲染循环的对齐工程围绕 flushSync、portal、SSR/hydration 做了大量修复与性能优化。下文按主题展开。二、Decorations API让扩展直接声明文档装饰3.30.0Decorations装饰在 ProseMirror 中并不是新概念但过去你必须手写一个 ProseMirror 插件、在插件 state 里维护装饰集合并在每次事务发生时手动映射位置。3.30.0 把这件事收进了扩展层扩展可以直接通过新的addDecorations()hook 声明装饰框架会把所有扩展声明的装饰聚合到同一个插件里因此多个扩展可以同时装饰同一份文档而互不打架。CHANGELOG 给出的最小示例节选自 CHANGELOG 第 3.30.0 节addDecorations() { return { create: ({ state }) // findMatches 可以是任何返回 { from, to } 数组的函数 findMatches(state.doc).map(match Decoration.Inline(match.from, match.to, { class: highlight }), ), } }三种装饰类型对应不同渲染目标Decoration.Inline()—— 给一段文本区间加样式Decoration.Node()—— 把属性放到某个块级节点的 DOM 元素上Decoration.Widget()—— 在某个单一位置上渲染你自定义的元素。典型用途包括搜索结果高亮、拼写错误标记、协作者光标、以及“在每个块旁边放一个拖拽手柄”等。减少每次按键的工作量默认情况下文档每次变化装饰都会重建。小文档无所谓大文档却很浪费因此 API 提供了两个“收窄范围”的手段shouldUpdate()跳过你不关心的事务。若你的装饰只依赖标题就忽略其它一切变更update: changedRanges配合createInRange()只重新扫描真正发生变化的块。在长文档上这相当于把“每次按键全量扫描”变成“每次按键只扫一个段落”。如果装饰的数据来自编辑器之外例如从服务器加载的评论应使用update: manual并自己通过editor.commands.updateDecorations()手动刷新。Widget 选项中的细节Widget 同时接受 ProseMirror 选项side、relaxedSide、stopEvent和ignoreSelection可用于控制 widget 相对文档位置的吸附方向、是否拦截事件、是否计入选区等行为。这部分选项在ReactWidgetRenderer中会原样透传见下节源码。CHANGELOG 版本发布节奏同时记录了 3.30.0 里若干相关的 React 修复例如 React node view 在选区覆盖到其已移开的位置时不再错误地呈现选中态31e176c。三、把真实组件渲染进 Widget 装饰ReactWidgetRendererDecorations 的 React 绑定由ReactWidgetRenderer以及 Vue 侧的VueWidgetRenderer承担。它们把真实组件渲染进 widget 装饰并且仍然处于你现有应用的 React 上下文内——Provider、Context、store 全部照常工作。这从 ReactWidgetRenderer.tsx 的实现可以看得很清楚组件经new ReactRenderer(component, ...)挂载到编辑器的 React 树中因此 hooks 与 context 可用装饰物料的创建通过createWidgetDecorationReactRenderer完成materialize阶段会重新调用renderer.render()并返回其 DOM 元素以保证即便首次渲染较晚编辑器内容组件可用后 portal 也能正确注册。一个典型用法示例源码注释中给出addDecorations() { return { create: ({ editor, state }) findMatches(state.doc).map(match ReactWidgetRenderer(MyWidget, { editor, pos: match.pos, key: match-${match.id}, props: { label: match.label }, }), ), } }key 是组件本地状态能否存活的开关Widget 需要一个key。只要复用同一个 key当文档在它周围变化时组件实例会保持挂载这样诸如“打开的菜单”“计数器”“输入到一半的内容”这类本地状态在编辑操作后依然存活。CHANGELOG 特别强调请使用你自己数据里的稳定 id如comment-${id}而不要用文档位置或列表下标作 key——否则组件会被卸载重挂状态随之丢失。源码中ReactWidgetRendererOptions.key的注释也印证了这一点。配套的包装参数包括as包裹元素标签默认span因 widget 通常是行内的与className它们只在渲染器首次创建时生效相同key的后续渲染会保留最初的标签与 class这也是组件实例被复用的另一面。四、MarkView用 React 组件渲染标记3.0.13.0.1及其 beta 期给 Tiptap 带来了对 ProseMirror MarkView 的支持你可以为某类 mark 渲染自定义视图——例如为文字颜色 mark 渲染一个颜色选择器或为链接 mark 渲染一个链接编辑器。这个能力在 React 侧通过ReactMarkViewRenderer接入。纯 JS 的 MarkView 基线Mark.create({ // Other options... addMarkView() { return ({ mark, HTMLAttributes }) { const dom document.createElement(b); const contentDOM document.createElement(span); dom.appendChild(contentDOM); return { dom, contentDOM, }; }; }, });React 绑定ReactMarkViewRendererimport { Mark } from tiptap/core; import { ReactMarkViewRenderer } from tiptap/react; import Component from ./Component.jsx; export default Mark.create({ name: reactComponent, parseHTML() { return [ { tag: react-component, }, ]; }, renderHTML({ HTMLAttributes }) { return [react-component, HTMLAttributes]; }, addMarkView() { return ReactMarkViewRenderer(Component); }, });对应的 React 组件形如下方代码。这里的关键是MarkViewContent /占位符它把 ProseMirror 的 contentDOM 挂到组件树中你指定的位置使标记内部的文本内容仍可正常编辑而组件其余 UI如按钮则可以设为contentEditable{false}。CHANGELOG 与 源码目录 中可见MarkViewRendererProps类型与 props 注入约定。import { MarkViewContent, MarkViewRendererProps } from tiptap/react; import React from react; export default (props: MarkViewRendererProps) { const [count, setCount] React.useState(0); return ( span classNamecontent>import { Mark } from tiptap/core; import { VueMarkViewRenderer } from tiptap/vue-3; export default Mark.create({ name: vueComponent, parseHTML() { return [{ tag: vue-component }]; }, renderHTML({ HTMLAttributes }) { return [vue-component, HTMLAttributes]; }, addMarkView() { return VueMarkViewRenderer(Component); }, });Vue 组件模板中对应地使用mark-view-content /承接内容、通过markViewProps接收 props。CHANGELOG 还记录了MarkViewContent的as可设为除span外的其它 HTML 标签2ea0475以及 3.5.2 修复 React MarkView 内容会被插入MarkViewContent中错误元素的问题。可继续在 src/ReactMarkViewRenderer.tsx 与仓库 GuideMarkViews 相关示例 中查看完整配套写法。五、声明式组件化 APITiptap / useTiptap3.18.03.20.03.18.0 引入了一个可选的、更符合 React 习惯的集成方式——声明式Tiptap /组件。官方称其为纯增量改动旧的命令式写法在本大版本内继续支持计划在下一大版本逐步弃用旧式设置。CHANGELOG 给出的示例import { Tiptap, useEditor } from tiptap/react; function MyEditor() { const editor useEditor({ extensions: [StarterKit], content: h1Hello from Tiptap/h1, }); return ( Tiptap instance{editor} Tiptap.Content / Tiptap.BubbleMenuMy Bubble Menu/Tiptap.BubbleMenu Tiptap.FloatingMenuMy Floating Menu/Tiptap.FloatingMenu MenuBar / {/* MenuBar 可用新的 useTiptap hook 从 context 读取 editor 实例 */} /Tiptap ); }结合 src/Tiptap.tsx 的源码这套组件化 API 的结构是Tiptap根 Provider同时提供TiptapContext新与兼容旧版的EditorContext旧useCurrentEditor()仍可用并通过editor推荐或instance3.27.2 起类型上保证二者必居其一旧的instance标注为已弃用接收编辑器实例若未传入非空实例会直接抛错Tiptap.Content /从 context 读取 editor 后渲染EditorContent无需手动传 editor propuseTiptap()读取 context 中的 editor 实例供MenuBar之类子组件使用useTiptapState()useEditorState的薄封装自动使用 context 中的 editor。从源码可推断Tiptap实际是一个通过Object.assign组合了Content子组件的包装组件displayName 分别为Tiptap与Tiptap.Content。3.20.0 同批还保证了由useTiptap拿到的 editor 实例非空简化了类型体操。六、HooksuseEditor / useEditorState 与 SSR 策略useEditor编辑器实例的生命周期管理src/useEditor.ts 中useEditor依赖内部EditorInstanceManager完成创建、更新与销毁。其核心逻辑包括通过useSyncExternalStore订阅实例变化服务端快照永远返回null回调型选项onCreate、onUpdate等不参与选项比较始终绑定最新闭包extensions数组做长度与逐个引用的浅比较以支持“在 options 里内联扩展数组”的常见写法渲染期间若仅选项变化则调用editor.setOptions()复用实例只有 deps 变化或实例已销毁才重建通过“推迟两个 tick 再销毁”的scheduleDestroy机制避免 Strict Mode 下的重复挂载误杀实例。immediatelyRender 与 SSR/hydration3.23.2、3.23.5immediatelyRender是 SSR 场景下的关键选项其默认值历史上发生过多次修正3.0.1时代在 SSR 模式下若未显式设置会抛错3.23.2改为“默认true但在检测到 SSR 时自动降为false”开发模式下仅打警告不再抛错。CHANGELOG 说明此前省略该选项在 Next.js 等 SSR 环境下“开发模式抛错、生产模式静默返回 null”是 AI 生成代码初始化编辑器时的常见崩溃源3.23.5修正了客户端型 Next.js 应用此前只要存在window.next即使显式传immediatelyRender: true也会被强制为false。新逻辑是仅在真正 SSRtypeof window undefined或“处于 Next.js 且未显式传值”时才强制关闭。源码中对应实现为isSSR typeof window undefinedisNext isSSR || window.next随后按上述规则在开发模式输出中文案警告。因此实践中纯客户端渲染传immediatelyRender: trueSSR/Next.js 传false或省略让 hook 自动探测。useEditorState按需订阅编辑器状态src/useEditorState.ts 实现了“选取部分编辑器状态、变化才重渲染”的订阅模型它同时监听transaction与update事件同一事务同时触发两者时去重并通过useSyncExternalStoreWithSelector 默认的deepEqual来自 fast-equals3.12.0 起替换了不再维护的 fast-deep-equal做值比较。典型用法const { currentSelection } useEditorState({ editor, selector: snapshot ({ currentSelection: snapshot.editor.state.selection }), })需要留意的是它只监听transaction与update两个事件。3.29.0 的修复正源于此editor.setEditable()只触发update而从不触发transaction过去导致组件在可编辑状态切换时不重渲染现已修复。RSC 兼容3.27.4与事件绑定3.28.03.27.4use client指令让tiptap/react可被 Server Components 导入而不崩溃同时经tiptap/react再导出的 core 符号也会跨越 client 边界因此在服务端代码中应直接从tiptap/core导入它们3.28.0useEditor初始化编辑器时补绑了onMount/onUnmount事件处理器1ecf814。七、菜单组件Floating UI 迁移、入口拆分与定位能力3.0.1tippy.js → Floating UI破坏性变更3.0.1 起移除了 tippy.js改用更轻量、可定制性更强的 Floating UI影响tiptap/extension-floating-menu、tiptap/extension-bubble-menu、tiptap/extension-mention、tiptap/suggestion、tiptap/react、tiptap/vue-2、tiptap/vue-3。迁移要点移除FloatingMenu/BubbleMenu组件上的tippyOptions替换为新的options对象需要自行安装 peer 依赖floating-ui/domnpm install floating-ui/dom^1.6.03.20.0独立tiptap/react/menus入口为避免未使用菜单的打包体积被 floating-ui 拖累3.20.0 把BubbleMenu/FloatingMenu移入tiptap/react/menus子路径3.19.0 曾先行发布同款变更。该入口对应源码目录 packages/react/src/menus包含BubbleMenu.tsx、FloatingMenu.tsx、getAutoPluginKey.ts、useMenuElementProps.ts等实现文件。定位与 DOM 能力补全时间线CHANGELOG 记录了一系列针对菜单的改进可按版本速查版本变更点3.4.3BubbleMenu增加可选的getPosition定位回调允许完全接管菜单坐标3.5.1FloatingMenu支持appendToBubbleMenu在 React/Vue 2/Vue 3 中透传该 prop用于规避裁剪与 z-index 问题3.6.3ReactFloatingMenu的 hook 依赖与BubbleMenu对齐能响应appendTo、pluginKey、shouldShow、options变化BubbleMenu修复浮层选项 prop 变更后不更新的问题并保证appendTo正确透传给底层插件3.16.0FloatingMenu支持updateEvent可通过setMeta(floatingMenu, updatePosition)编程式刷新定位3.20.3BubbleMenu/FloatingMenu把className、style、data-*、事件处理器等 HTML props 转发到定位后的菜单容器省略pluginKey时自动生成稳定的每实例插件 key避免多实例互相冲突对 React 侧而言菜单组件的问题大多源于 React 渲染与 ProseMirror 插件的双轨生命周期例如 3.9.0/3.8.0 两次发布均针对“组件每次重渲染都导致菜单插件重载”的回归进行修复。源码可在 packages/react/src/menus/BubbleMenu.tsx 与 packages/react/src/menus/FloatingMenu.tsx 查阅仓库中亦有对应的 BubbleMenu.spec.ts 与 FloatingMenu.spec.ts 测试用例佐证这些行为。八、React NodeView 渲染让 React 与 ProseMirror 两个渲染循环对齐NodeView把节点渲染为 React 组件是 React 绑定的核心难点因为 ProseMirror 的 DOM 视图与 React 虚拟 DOM 并存容易在更新顺序上互相错位。CHANGELOG 用大量 patch 记录了这场“对齐工程”最有代表性的是 flushSync 的去而复返beta 期3.0.0-beta.20从 NodeView 渲染中移除flushSync因为它造成性能回退且被 PMViewDesc 检查时仍会意外 reconcile 未使用的 NodeView3.0.1重新引入flushSync用于同步 React 与 ProseMirror 的渲染cce64973.22.2修复flushSync()在EditorContent /生命周期中执行时报错的问题8ab8bee。渲染性能方面随后持续收紧3.12.0修复 React node view 在 ProseMirror 与 React 渲染周期失步时从this.getPos()拿到非法位置、进而更新报错的问题41601d13.22.1NodeView 在节点位置变化如同级节点在同一父级内被移动但内容与装饰未变时不再错误地不重渲染ee03ac0同时避免 ProseMirror 已摘除 node view 位置查找时、延迟选区更新阶段 React node view 崩溃6f3b9fc3.23.5NodeView 在装饰或位置变化但内容未变时不再重渲染并新增可选的trackNodeViewPosition——开启后组件在每次位置移动时重渲染从而保证渲染输出里调用的getPos()始终是最新值同时删除内部nodeViewPositionRegistry并在ReactRenderer.updateProps()中加入浅比较 props 以消除多余渲染3.28.0把同一微任务内批量产生的 React node view portal store 通知合并处理规避大量 node view 同时挂载时的 nested update 警告86147303.29.1 / 3.29.2分别修复“在 React NodeView 渲染的块内按 Enter 时光标跳回上一块”“拆分块后光标落点错误”等与选区/光标相关的回归3.30.3修复contentComponent不可用时ReactNodeViewRenderer崩溃的问题1cb7ad3。渲染器的生命周期与清理底层渲染器是 src/ReactRenderer.tsx负责把 React 组件渲染进独立 portal 并保持与 ProseMirror DOM 同步。它的清理行为也是迭代重点3.3.0ReactRenderer.destroy()现在会在存在父节点时把自身.element从 DOM 中移除。此前很多 demo 把 renderer 的.element追加进document.bodydestroy 只销毁 portal 却遗留.react-rendererDOM 节点会累积泄漏3.15.2修复 Strict Mode 下已被销毁的 renderer 被重新加回的竞态问题。NodeView 的选中态还有一个独立开关selectedOnTextSelection3.22.5开启后当 TextSelection 完全落在节点区间内而不只是 NodeSelection时selectedprop 也会为true。可配合 ReactNodeViewRenderer.spec.ts 中的测试理解其行为边界。九、给你的升级核对清单若要在项目中使用上述能力可按 CHANGELOG 的破坏性变更做一次核对若从 v2 迁移移除一切tippyOptions改用options对象并npm install floating-ui/dom^1.6.0若只用编辑器主体从tiptap/react主入口导入useEditor与EditorContent若使用 Bubble/Floating 菜单从tiptap/react/menus导入合理设置appendTo容器裁剪/z-index 问题并按需提供pluginKeySSR / Next.js 环境显式传immediatelyRender: false纯客户端则传trueRSC 环境直接依赖use client指令即可安全导入使用 NodeView可开启trackNodeViewPosition以在渲染输出中使用最新getPos()否则优先把位置读取限制在事件回调内以减少重渲染使用装饰/widget通过addDecorations()声明装饰用shouldUpdate()或update: changedRangescreateInRange()控制性能widget 务必用稳定 id 作为key。上述每一项都能在当前仓库找到实现与测试作为依据核心 Renderer 与 Hooks 位于 packages/react/src行为测试分布在 BubbleMenu.spec.ts、EditorContent.spec.ts、ReactNodeViewRenderer.spec.ts、ReactWidgetRenderer.spec.ts 与 useEditorState.spec.ts 等文件中完整变更历史则逐条记录在 packages/react/CHANGELOG.md。【免费下载链接】tiptapThe headless rich text editor framework for web artisans.项目地址: https://gitcode.com/GitHub_Trending/ti/tiptap创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门