HTML 转换

富文本转 HTML

将基于 Lexical 的富文本转换为 HTML 主要有两种方法:

  1. 按需生成 HTML(推荐):在需要的地方将 JSON 转换为 HTML,按需处理。
  2. 在 Collection 中生成 HTML:创建一个新字段,自动将保存的 JSON 内容转换为 HTML。这种方法不推荐,因为它会增加 Payload API 的负担,并且可能与实时预览功能不兼容。

按需转换

要按需将 JSON 转换为 HTML,可以使用 @payloadcms/richtext-lexical/html 中的 convertLexicalToHTML 函数。以下是在前端 React 组件中使用该函数的示例:

'use client'

import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import { convertLexicalToHTML } from '@payloadcms/richtext-lexical/html'

import React from 'react'

export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
  const html = convertLexicalToHTML({ data })

  return <div dangerouslySetInnerHTML={{ __html: html }} />
}

动态填充(高级用法)

默认情况下,convertLexicalToHTML 需要完全填充的数据(例如上传文件、链接等)。如果你需要动态获取并填充这些节点,可以使用异步变体 convertLexicalToHTMLAsync,它来自 @payloadcms/richtext-lexical/html-async。你必须提供一个 populate 函数:

'use client'

import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'

import { getRestPopulateFn } from '@payloadcms/richtext-lexical/client'
import { convertLexicalToHTMLAsync } from '@payloadcms/richtext-lexical/html-async'
import React, { useEffect, useState } from 'react'

export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
  const [html, setHTML] = useState<null | string>(null)
  useEffect(() => {
    async function convert() {
      const html = await convertLexicalToHTMLAsync({
        data,
        populate: getRestPopulateFn({
          apiURL: `http://localhost:3000/api`,
        }),
      })
      setHTML(html)
    }

    void convert()
  }, [data])

  return html && <div dangerouslySetInnerHTML={{ __html: html }} />
}

使用 REST populate 函数会为每个节点发送单独的请求。如果你需要填充大量节点,这可能会很慢。为了在服务器上获得更好的性能,你可以使用 getPayloadPopulateFn 函数:

import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'

import { getPayloadPopulateFn } from '@payloadcms/richtext-lexical'
import { convertLexicalToHTMLAsync } from '@payloadcms/richtext-lexical/html-async'
import { getPayload } from 'payload'
import React from 'react'

import config from '../../config.js'

export const MyRSCComponent = async ({
  data,
}: {
  data: SerializedEditorState
}) => {
  const payload = await getPayload({
    config,
  })

  const html = await convertLexicalToHTMLAsync({
    data,
    populate: await getPayloadPopulateFn({
      currentDepth: 0,
      depth: 1,
      payload,
    }),
  })

  return html && <div dangerouslySetInnerHTML={{ __html: html }} />
}

HTML 字段

lexicalHTMLField() 辅助函数会将 JSON 转换为 HTML,并通过 afterRead 钩子在每次读取时更新保存到字段中。通常不建议使用此方法,原因有二:

  1. 它会在另一列中创建重复内容的另一种格式
  2. 客户端实时预览中,会导致预览不再"实时"

除非有充分理由,否则建议使用按需 HTML 转换器JSX 转换器

import type { HTMLConvertersFunction } from '@payloadcms/richtext-lexical/html'
import type { MyTextBlock } from '@/payload-types.js'
import type { CollectionConfig } from 'payload'

import {
  BlocksFeature,
  type DefaultNodeTypes,
  lexicalEditor,
  lexicalHTMLField,
  type SerializedBlockNode,
} from '@payloadcms/richtext-lexical'

const Pages: CollectionConfig = {
  slug: 'pages',
  fields: [
    {
      name: 'nameOfYourRichTextField',
      type: 'richText',
      editor: lexicalEditor(),
    },
    lexicalHTMLField({
      htmlFieldName: 'nameOfYourRichTextField_html',
      lexicalFieldName: 'nameOfYourRichTextField',
    }),
    {
      name: 'customRichText',
      type: 'richText',
      editor: lexicalEditor({
        features: ({ defaultFeatures }) => [
          ...defaultFeatures,
          BlocksFeature({
            blocks: [
              {
                interfaceName: 'MyTextBlock',
                slug: 'myTextBlock',
                fields: [
                  {
                    name: 'text',
                    type: 'text',
                  },
                ],
              },
            ],
          }),
        ],
      }),
    },
    lexicalHTMLField({
      htmlFieldName: 'customRichText_html',
      lexicalFieldName: 'customRichText',
      // 可以传入额外的转换器或覆盖默认转换器
      converters: (({ defaultConverters }) => ({
        ...defaultConverters,
        blocks: {
          myTextBlock: ({ node, providedCSSString }) =>
            `<div style="background-color: red;${providedCSSString}">${node.fields.text}</div>`,
        },
      })) as HTMLConvertersFunction<
        DefaultNodeTypes | SerializedBlockNode<MyTextBlock>
      >,
    }),
  ],
}

将 Blocks 转换为 HTML

如果你的富文本包含 Lexical blocks,你需要提供将其转换为 HTML 的方法。例如:

'use client'

import type { MyInlineBlock, MyTextBlock } from '@/payload-types'
import type {
  DefaultNodeTypes,
  SerializedBlockNode,
  SerializedInlineBlockNode,
} from '@payloadcms/richtext-lexical'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'

import {
  convertLexicalToHTML,
  type HTMLConvertersFunction,
} from '@payloadcms/richtext-lexical/html'
import React from 'react'

type NodeTypes =
  | DefaultNodeTypes
  | SerializedBlockNode<MyTextBlock>
  | SerializedInlineBlockNode<MyInlineBlock>

const htmlConverters: HTMLConvertersFunction<NodeTypes> = ({
  defaultConverters,
}) => ({
  ...defaultConverters,
  blocks: {
    // 每个键应该匹配你的 block 的 slug
    myTextBlock: ({ node, providedCSSString }) =>
      `<div style="background-color: red;${providedCSSString}">${node.fields.text}</div>`,
  },
  inlineBlocks: {
    // 每个键应该匹配你的 inline block 的 slug
    myInlineBlock: ({ node, providedStyleTag }) =>
      `<span${providedStyleTag}>${node.fields.text}</span$>`,
  },
})

export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
  const html = convertLexicalToHTML({
    converters: htmlConverters,
    data,
  })

  return <div dangerouslySetInnerHTML={{ __html: html }} />
}

HTML 转 Richtext

如果你需要将原始 HTML 转换为 Lexical 编辑器状态,可以使用 @payloadcms/richtext-lexical 中的 convertHTMLToLexical 方法,配合 editorConfigFactory 获取编辑器配置

import {
  convertHTMLToLexical,
  editorConfigFactory,
} from '@payloadcms/richtext-lexical'
// 确保已安装 jsdom 和 @types/jsdom
import { JSDOM } from 'jsdom'

const html = convertHTMLToLexical({
  editorConfig: await editorConfigFactory.default({
    config, // 你的 Payload 配置
  }),
  html: '<p>text</p>',
  JSDOM, // 传入 JSDOM 导入;为了保持包体积小,它没有被捆绑
})