探索 Tiptap V3 的最新功能

静态渲染器

版本下载量

静态渲染器帮助将 JSON 内容渲染为 HTML、markdown 或 React 组件,而无需编辑器实例。它只需要 JSON 内容和扩展列表。

为什么选择静态渲染?

静态渲染的主要用例是在服务器端渲染 Tiptap/ProseMirror JSON 文档,例如在 Next.js 或 Nuxt.js 应用程序中。通过这种方式,您可以在将内容发送到客户端之前将编辑器的内容渲染为 HTML,这可以通过不再需要在客户端或服务器上加载编辑器来提高应用程序的性能。

另一个用例是将编辑器的内容渲染为另一种格式,比如 markdown,这在您想要将其发送到基于 markdown 的 API 时非常有用。静态渲染器的构建方式使得输出可以是您想要的任何内容,只要您提供正确的映射即可。

但是什么使得它是静态的呢?静态渲染器不需要浏览器、DOM 甚至编辑器实例来渲染内容。它是一个纯 JavaScript 函数,接受文档(作为 JSON 或 Prosemirror Node 实例)并返回目标格式。

从 JSON 生成 HTML 字符串

给定一个 JSON 文档,renderToHTMLString 函数将返回一个表示 JSON 内容的 HTML 字符串。该函数接受三个参数:JSON 文档、扩展列表和一个选项对象。

import StarterKit from '@tiptap/starter-kit'
import { renderToHTMLString } from '@tiptap/static-renderer/pm/html'

renderToHTMLString({
  extensions: [StarterKit], // 使用您的扩展
  content: {
    type: 'doc',
    content: [
      {
        type: 'paragraph',
        content: [
          {
            type: 'text',
            text: '你好,世界!',
          },
        ],
      },
    ],
  },
})
// 返回: '<p>你好,世界!</p>'

generateHTML API

function renderToHTMLString(options: {
  extensions: Extension[]
  content: ProsemirrorNode | JSONContent
  options?: TiptapHTMLStaticRendererOptions
}): string
  • extensions: 用于渲染内容的 Tiptap 扩展数组。
  • content: 要渲染的内容。可以是 Prosemirror Node 实例或 Prosemirror 文档的 JSON 表示。
  • options: 带有附加选项的对象。
  • options.nodeMapping: 将 Prosemirror 节点映射到 HTML 字符串的对象。
  • options.markMapping: 将 Prosemirror 标记映射到 HTML 字符串的对象。
  • options.unhandledNode: 当遇到未处理的节点时调用的函数。
  • options.unhandledMark: 当遇到未处理的标记时调用的函数。

从 JSON 生成 Markdown

给定一个 JSON 文档,renderToMarkdown 函数将返回一个表示 JSON 内容的 markdown 字符串。该函数接受三个参数:JSON 文档、扩展列表和一个选项对象。

此包不验证 markdown 输出,存在多种 markdown 风格,此 包不强制执行任何一个。确保 markdown 输出是 有效的责任在于您。

import StarterKit from '@tiptap/starter-kit'
import { renderToMarkdown } from '@tiptap/static-renderer/pm/markdown'

renderToMarkdown({
  extensions: [StarterKit], // 使用您的扩展
  content: {
    type: 'doc',
    content: [
      {
        type: 'paragraph',
        content: [
          {
            type: 'text',
            text: '你好,世界!',
          },
        ],
      },
    ],
  },
})
// 返回: '你好,世界!'

generateMarkdown API

function renderToMarkdown(options: {
  extensions: Extension[]
  content: ProsemirrorNode | JSONContent
  options?: TiptapMarkdownStaticRendererOptions
}): string
  • extensions: 用于渲染内容的 Tiptap 扩展数组。
  • content: 要渲染的内容。可以是 Prosemirror Node 实例或 Prosemirror 文档的 JSON 表示。
  • options: 带有附加选项的对象。
  • options.nodeMapping: 将 Prosemirror 节点映射到 markdown 字符串的对象。
  • options.markMapping: 将 Prosemirror 标记映射到 markdown 字符串的对象。
  • options.unhandledNode: 当遇到未处理的节点时调用的函数。
  • options.unhandledMark: 当遇到未处理的标记时调用的函数。

从 JSON 生成 React 组件

给定一个 JSON 文档,renderToReactElement 函数将返回一个表示 JSON 内容的 React 组件。该函数接受三个参数:JSON 文档、扩展列表和一个选项对象。

import StarterKit from '@tiptap/starter-kit'
import { renderToReactElement } from '@tiptap/static-renderer/pm/react'

renderToReactElement({
  extensions: [StarterKit], // 使用您的扩展
  content: {
    type: 'doc',
    content: [
      {
        type: 'paragraph',
        content: [
          {
            type: 'text',
            text: '你好,世界!',
          },
        ],
      },
    ],
  },
})
// 返回一个 react 节点,评估后将等同于: '<p>你好,世界!</p>' 而不需要 Tiptap 编辑器实例

generateReactElement API

function renderToReactElement(options: {
  extensions: Extension[]
  content: ProsemirrorNode | JSONContent
  options?: TiptapReactStaticRendererOptions
}): ReactElement
  • extensions: 用于渲染内容的 Tiptap 扩展数组。
  • content: 要渲染的内容。可以是 Prosemirror Node 实例或 Prosemirror 文档的 JSON 表示。
  • options: 带有附加选项的对象。
  • options.nodeMapping: 将 Prosemirror 节点映射到 React 组件的对象。
  • options.markMapping: 将 Prosemirror 标记映射到 React 组件的对象。
  • options.unhandledNode: 当遇到未处理的节点时调用的函数。
  • options.unhandledMark: 当遇到未处理的标记时调用的函数。

React NodeViews

静态渲染器不自动支持节点视图,因此您需要为每个希望作为节点视图渲染的节点类型提供映射。以下是如何将节点视图渲染为 React 组件的示例:


import { Node } from '@tiptap/core'
import { ReactNodeViewRenderer } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import { renderToReactElement } from '@tiptap/static-renderer/pm/react'

// 此组件没有 NodeViewContent,因此不渲染其子级的富文本内容
function MyCustomComponentWithoutContent() {
  const [count, setCount] = React.useState(200)

  return (
    <div className='custom-component-without-content' onClick={() => setCount(a => a + 1)}>
      {count} 这是一个 react 组件!
    </div>
  )
}

const CustomNodeExtensionWithoutContent = Node.create({
  name: 'customNodeExtensionWithoutContent',
  atom: true,
  renderHTML() {
    return ['div', { class: 'my-custom-component-without-content' }] as const
  },
  addNodeView() {
    return ReactNodeViewRenderer(MyCustomComponentWithoutContent)
  },
})

renderToReactElement({
  extensions: [StarterKit, CustomNodeExtensionWithoutContent],
  options: {
    nodeMapping: {
      // 使用预期的节点视图 React 组件渲染自定义节点
      customNodeExtensionWithoutContent: MyCustomComponentWithoutContent,
    },
  },
  content: {
    type: 'doc',
    content: [
      {
        type: 'customNodeExtensionWithoutContent',
      },
    ],
  },
})
// 返回: <div class="my-custom-component-without-content">200 这是一个 react 组件!</div>

但是,如果您希望渲染节点视图的富文本内容,该如何操作?您可以通过将 NodeViewContent 组件作为节点视图组件的子组件提供来实现:

import { Node } from '@tiptap/core'
import {
  NodeViewContent,
  ReactNodeViewContentProvider,
  ReactNodeViewRenderer
} from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import { renderToReactElement } from '@tiptap/static-renderer/pm/react'

const CustomNodeExtensionWithContent = Node.create({
  name: 'customNodeExtensionWithContent',
  content: 'text*',
  group: 'block',
  renderHTML() {
    return ['div', { class: 'my-custom-component-with-content' }, 0] as const
  },
  addNodeView() {
    return ReactNodeViewRenderer(MyCustomComponentWithContent)
  },
})

function MyCustomComponentWithContent() {
  return (
    <div className="custom-component-with-content">
      具有内容的自定义组件!
      <NodeViewContent />
    </div>
  )
}

renderToReactElement({
  extensions: [StarterKit, CustomNodeExtensionWithContent],
  options: {
    nodeMapping: {
      customNodeExtensionWithContent: ({ children }) => {
        // 为了将内容传递到 NodeViewContent 组件中,我们需要使用 ReactNodeViewContentProvider 包装自定义组件
        return (
          <ReactNodeViewContentProvider content={children}>
            <MyCustomComponentWithContent />
          </ReactNodeViewContentProvider>
        )
      },
    },
  },
  content: {
    type: 'doc',
    content: [
      {
        type: 'customNodeExtensionWithContent',
        // 富文本内容
        content: [
          {
            type: 'text',
            text: '你好,世界!',
          },
        ],
      },
    ],
  },
})

// 返回: <div class="custom-component-with-content">具有内容的自定义组件!<div data-node-view-content="" style="white-space:pre-wrap">你好,世界!</div></div>
// 注意: NodeViewContent 组件渲染为带有属性 data-node-view-content 的 div,富文本内容渲染在其中

共享选项

renderToHTMLStringrenderToMarkdownrenderToReactElement 函数接受一个选项对象作为参数。可以使用该对象通过提供自定义节点和标记映射,或处理未处理的节点和标记,来自定义渲染器的输出。

import StarterKit from '@tiptap/starter-kit'
import { renderToHTMLString, serializeChildrenToHTMLString } from '@tiptap/static-renderer/pm/html'

renderToHTMLString({
  extensions: [StarterKit], // 使用您的扩展
  content: {
    type: 'doc',
    content: [
      {
        type: 'paragraph',
        content: [
          {
            type: 'text',
            text: '你好,世界!',
          },
        ],
      },
    ],
  },
  options: {
    // 自定义节点映射
    nodeMapping: {
      paragraph({ children }) {
        return `<div class="custom-paragraph">${serializeChildrenToHTMLString(children)}</div>`
      },
    },
    // 自定义标记映射
    markMapping: {
      bold({ children }) {
        return `<strong>${serializeChildrenToHTMLString(children)}</strong>`
      },
    },
    // 处理未处理的节点
    unhandledNode: ({ node }) => {
      return `[未知节点 ${node.type.name}]`
    },
    // 处理未处理的标记
    unhandledMark: ({ mark }) => {
      return `[未知标记 ${mark.type.name}]`
    },
  },
})

技术细节

命名空间导入

为了减少应用程序中的捆绑大小,静态渲染器被拆分为三个独立的包:@tiptap/static-renderer/pm/html@tiptap/static-renderer/pm/markdown@tiptap/static-renderer/pm/react。这样,您只需导入所需的静态渲染器部分。如果您希望获得更大的灵活性,可以使用 @tiptap/static-renderer 导入整个静态渲染器包。

// 仅 HTML 渲染器
import { renderToHTMLString } from '@tiptap/static-renderer/pm/html'

// 仅 markdown 渲染器
import { renderToMarkdown } from '@tiptap/static-renderer/pm/markdown'

// 仅 React 渲染器
import { renderToReactElement } from '@tiptap/static-renderer/pm/react'

// 整个静态渲染器
import { renderToHTMLString, renderToMarkdown, renderToReactElement } from '@tiptap/static-renderer'

json 命名空间中的包也可用于无运行时依赖地静态渲染。但是,这些包无法自动将 Prosemirror 节点和标记映射到目标格式。您需要为这些包中的每个节点和标记提供自定义映射。

// 仅 HTML 渲染器
import { renderJSONContentToString } from '@tiptap/static-renderer/json/html'

// 仅 React 渲染器
import { renderJSONContentToReactElement } from '@tiptap/static-renderer/json/react'

这些包与 pm 命名空间具有相同的 API,但是:

  • 需要您为每个节点和标记提供自定义映射
  • 不需要 extensions,因为它们不依赖于 prosemirror 包

自定义映射

静态渲染器使用 Prosemirror 节点和标记到目标格式的默认映射。通过在选项对象中提供自定义映射,可以覆盖这些映射。这使您能够根据需要自定义渲染器的输出。

要将自定义节点和标记转换为目标格式,您应提供一个映射函数,该函数以 nodemark 对象作为参数,并返回适当的目标格式元素。如果遇到未处理的节点或标记,则可以提供一个函数,该函数将与未处理的节点或标记作为参数调用。

这是如何工作的?

json 命名空间中的静态渲染器包遍历 JSON 内容,并为每个节点和标记调用适当的映射函数。renderJSONContentToString 函数返回一个表示 JSON 内容的字符串,而 renderJSONContentToReactElement 函数返回表示 JSON 内容的 React 元素。

pm 命名空间中的静态渲染器包扩展了 json 命名空间中的包,利用 Tiptap 扩展的 renderHTML 方法生成 Prosemirror 节点/标记到目标格式的默认映射。这些可以通过在选项中提供自定义映射完全覆盖。

源代码

packages/static-renderer/