LizardGlobalGH/payload-richtext-lexical-react-native
GitHub: LizardGlobalGH/payload-richtext-lexical-react-native
该项目是 PayloadCMS Lexical 富文本渲染器的 React Native 实现,用于在移动应用中渲染序列化的 Lexical 编辑器内容。
Stars: 0 | Forks: 0
该包提供了一个 Rich Text Renderer 的 React Native 实现,用于渲染序列化的 Lexical Editor 内容,fork 自 `@payloadcms/richtext-lexical`。它包含用于在 React Native 应用中渲染和管理富文本内容的组件和实用工具。
## 目录
- [安装](#installation)
- [内部安装](#internal-install)
- [用法](#usage)
- [处理外部链接](#handling-external-links)
- [Primitives](#primitives)
- [Converters](#converters)
- [实现](#implementation)
- [Primitives](#primitives-1)
- [支持的节点和 Converters](#supported-nodes-and-converters)
- [暴露的 API](#exposed-api)
- [预期用法](#expected-usage)
- [边缘情况、风险和关注点](#edge-cases-risks-and-concerns)
- [未知或自定义节点类型](#unknown-or-custom-node-types)
- [内部链接](#internal-links)
- [大型文档树和渲染性能](#large-document-trees-and-render-performance)
- [表格](#tables)
- [图片](#images)
- [URL 处理默认值](#url-handling-defaults)
# 安装
使用您偏好的包管理器安装该包:
```
# npm
npm install @lizardglobal/payload-richtext-lexical-react-native
# yarn
yarn add @lizardglobal/payload-richtext-lexical-react-native
# pnpm
pnpm add @lizardglobal/payload-richtext-lexical-react-native
```
## 内部安装
如果您需要在内部集成此包而不进行发布,可以使用内置的 CLI 来构建并将 `dist` 复制到您的本地模块路径中:
```
pnpm internal:install --target YOUR_PROJECT/modules/richtext-lexical
```
此命令执行的操作等同于:
```
pnpm build && rm -rf YOUR_PROJECT/modules/richtext-lexical/* && cp -R dist/* YOUR_PROJECT/modules/richtext-lexical/
```
如果您已经构建完成,只想重新复制文件,请使用:
```
pnpm internal:install --target YOUR_PROJECT/modules/richtext-lexical --skip-build
```
然后在您的项目中按如下方式引入该包:
```
import { RichText } from '@/modules/richtext-lexical/exports/react-native'
const MyComponent = () => {
return (
)
}
```
**重要提示:** 由于这是一个静态构建,因此没有内置的包解析机制。**请仅使用您所支持的功能的代码路径。** 例如,如果您只支持 React Native 导出,请仅从 `exports/react-native` 引入,而不要从 `exports/react` 或 `exports/client` 引入。从不支持的路径引入可能会因缺少依赖项而导致错误。
# 用法
渲染 Lexical 内容最简单的方法是使用 `RichText` 组件来处理您的序列化数据:
```
import { RichText } from "@lizardglobal/payload-richtext-lexical/react-native";
function ArticleContent({ article }) {
return ;
}
```
这将使用默认的 React Native primitives(`View`、`Text`、`Image` 等)以及针对所有受支持的 Lexical 节点类型的内置 converters 来渲染内容。
## 处理外部链接
由于 React Native 不像 Web 浏览器那样具有自动链接处理功能,您应该提供一个 `onExternalLinkPress` 处理函数来控制外部 URL 的打开方式:
```
import { RichText } from "@lizardglobal/payload-richtext-lexical/react-native";
import { Linking, Alert } from "react-native";
function ArticleContent({ article }) {
const handleExternalLink = async (url: string) => {
const supported = await Linking.canOpenURL(url);
if (supported) {
await Linking.openURL(url);
} else {
Alert.alert("Error", `Cannot open URL: ${url}`);
}
};
return (
);
}
```
## Primitives
Primitives 是用于渲染内容的基础构建块(例如 `Text`、`View`、`Image`、`Pressable`)。您可以覆盖这些组件以与应用的设计系统集成,或添加自定义行为。您只需覆盖想要自定义的 primitives。任何未指定的 primitives 都将使用默认的 React Native 组件:
```
import { RichText } from "@lizardglobal/payload-richtext-lexical/react-native";
import { Text as CustomText } from "@/components/ui/Text";
import { View as CustomView } from "@/components/ui/View";
import { Pressable as CustomPressable } from "@/components/ui/Pressable";
function ArticleContent({ article }) {
return (
);
}
```
## Converters
Converters 负责将 Lexical 节点类型转换为 React Native 组件。您可以覆盖默认的 converters 来更改特定内容类型的渲染方式:
```
import { RichText } from "@lizardglobal/payload-richtext-lexical/react-native";
import { View, Text } from "react-native";
import { useNavigation } from "@react-navigation/native";
function ArticleContent({ article }) {
const navigation = useNavigation();
// Custom converter for heading nodes
const customHeadingConverter = {
converter: ({ node, children, primitives }) => {
const HeadingText = primitives.Text;
const fontSize = node.tag === 'h1' ? 32 : node.tag === 'h2' ? 24 : 18;
return (
{children}
);
},
};
// Custom converter for link nodes with internal navigation
const customLinkConverter = {
converter: ({ node, children, primitives }) => {
const LinkPressable = primitives.Pressable;
const LinkText = primitives.Text;
const handlePress = () => {
if (node.fields?.doc?.relationTo === 'articles') {
// Navigate internally for article links
navigation.navigate('Article', { id: node.fields.doc.value.id });
} else if (node.fields?.url) {
// Handle external links
Linking.openURL(node.fields.url);
}
};
return (
{children}
);
},
};
return (
);
}
```
# 实现
此实现通过添加一个与现有渲染器(特别是 React 渲染器)类似且带有 RN primitives 的入口点 `@lizardglobal/payload-richtext-lexical-react-native/react-native`,为 `@lizardglobal/payload-richtext-lexical-react-native` 包添加了 **React Native 渲染支持**。
该 RN 入口点旨在为 RN 应用中的序列化 Lexical 内容提供一个仅限渲染器的 API。我尽量使暴露的 API 与现有的 React 渲染器保持尽可能接近,以便在大多数情况下将其作为直接替代品使用,同时仍允许通过覆盖 converters 和 primitives 来实现特定平台的行为。
## Primitives
与 React 渲染器不同,我在 RN 渲染器中为 primitives 引入了一层抽象。由于可以预期 React 会在 DOM 环境中运行,因此 React 渲染器可以安全地假设 `div`、`span` 和 `img` 等 primitives 是可用的。相比之下,RN 有一套不同的 primitives(`View`、`Text`、`Image` 等),它们并非以相同的方式全局可用。它们需要从 `react-native` 中“手动”引入,并且也可以被应用程序包装或自定义。即便如此,某些 primitives(如 `Text`)在特定的应用组件中还具有特定的行为和嵌套规则。
因此,我没有在每个 converter 中直接引入 RN primitives,而是创建了一个 `resolvePrimitive` 实用工具,它将抽象的 primitive 名称(如 `Text`、`View`、`Image` 等)映射到实际的 RN 组件。这种映射可以通过 converter context 进行覆盖,从而允许注入 primitive 以进行自定义(例如,*主题设置、分析埋点、无障碍约定或导航集成,而无需重写所有的 converters*)。
Primitive 解析执行一次,并通过 converter context 传递。实际上,当用户部分覆盖 primitives(例如,仅替换 `Text` 和 `Pressable`)时,集中式的 primitive 解析提高了的一致性,而不是必须重新实现整个 converter 集合。它还使 converter 的实现专注于特定于节点的逻辑,而不是特定于平台的组件管理。
## 支持的节点和 converters
`TODO: 在此处添加支持的节点和 converters 列表,以及与 React 渲染器在行为上的任何显著差异。`
## 暴露的 API
为了与 React 渲染器暴露的 API 保持一致,我尝试模仿相同的结构。主要入口点是 `RichText` 组件,它接受序列化的 Lexical 内容并使用 RN converters 和 primitives 对其进行渲染。对于需要更多控制权的用户,还暴露了底层的转换函数(`convertLexicalToReactNative` 和 `convertLexicalNodesToReactNative`)以供直接使用。我还暴露了 primitive 实用工具和 converter 类型,以保持自定义集成的类型安全,并与包的默认设置保持一致。
## 预期用法
预期的集成流程与 React 渲染器保持类似的统一。数据获取留给用户完成,而该包专注于通过 `RichText` 组件渲染序列化的 Lexical 内容。开发者可以选择性地提供 `primitives` 和 `converters` 覆盖以进行自定义,但默认设置应能涵盖大多数用例。他们还可以定义一个 `onExternalLinkPress` 处理函数来显式管理外部 URL 行为,这在 URL 处理方式可能因环境而异的 RN 中尤为重要。默认情况下,外部链接将尝试使用 `Linking.openURL` 打开,但提供显式的处理程序可以跨平台实现更好的控制和一致性。**综上所述:**
1. 用户从 Payload 获取序列化的 Lexical 数据。
2. 使用来自 `@lizardglobal/payload-richtext-lexical-react-native/react-native` 的 `RichText` 对其进行渲染。
3. 提供 `onExternalLinkPress` 以实现明确的外部 URL 行为。
4. 在与应用设计系统集成时添加 `primitives` 覆盖。
5. 在默认节点行为不足以满足需求时添加 `converters` 覆盖。
## 边缘情况、风险和关注点
### 未知或自定义节点类型
如果存储的内容包含没有注册 converter 的节点类型,这些节点的输出可能会丢失。目前,对于不支持的节点,默认行为是不进行任何渲染,如果不仔细管理,这可能会导致内容悄无声息地丢失。我还没有时间去研究其他实现是如何处理这个问题的,但如果您认为更严格默认行为更合适的话(例如,在开发环境中渲染占位符或警告),我愿意实现它。
### 内部链接
在 RN 渲染器中,默认情况下不解析内部链接。我将其留给开发者去覆盖默认的 `Link` converter 并实现特定于应用导航逻辑,因为内部链接的解析高度依赖于应用的路由结构和导航库。在 React 渲染器中,为了简单起见,暴露的 Link converter 允许提供 `internalDocToHref` 属性来进行内部链接解析。我不确定是否要实现这个功能。我能想到实现它的唯一理由是为了保持渲染器之间的功能对等。由于时间不足,我目前选择不实现它。
### 大型文档树和渲染性能
显而易见,大型树的递归转换会增加渲染成本,并影响低端设备上的滚动性能。在任何环境中进行富文本渲染时,这都是一个普遍的关注点,但在 JS 线程性能通常(可能?)受到更多限制的 RN 中尤为突出。我将尝试添加性能分析测试和指标以识别特定的瓶颈,并尽可能优化 converter 的实现,但我没有足够的信心,也不确定这是否属于此功能请求本身的范围。很乐意听取关于此点的反馈。
### 表格
渲染器中的表格目前使用基本的 `View` 和 `Text` primitives 实现,这可能不支持所有所需的表格功能(如固定表头、响应式布局或复杂的单元格合并)。坦白说(无意双关),这是一个仅涵盖基本渲染的骨干实现,可能无法满足所有用例。这是有意为之。我希望获得反馈,了解这对于初始实现是否足够,或者是否有人能提供更好的方法来帮忙。
### 图片
在 RN 中,有意不复制 Web 特有的响应式图片行为。React 渲染器得益于浏览器原生功能,如 CSS 媒体查询和通过 srcset 属性进行的自动图片缩放。React Native 没有对应的机制。图片尺寸必须显式指定或在 runtime 计算得出。此外,应用程序代码应利用 Payload 的上传元数据(尺寸、方向、文件大小)来实现合适的 RN 特定行为(例如,基于网络条件的预加载、缓存策略或自适应质量选择)。请明确记录这一差距,以免开发者期望其与 Web 渲染器具有对等性。
### URL 处理默认值
如果省略 `onExternalLinkPress`,runtime 级别的 URL 处理可能会因环境而异。建议在生产应用中始终提供显式的处理函数。React Native 的 `Linking.openURL` 行为在 iOS 和 Android 上有所不同,并且在非原生环境(Web、SSR 上下文、测试)中行为是未定义的。在没有显式处理函数的情况下默认使用 `Linking.openURL` 可能会导致静默失败或特定于平台的错误。通过要求开发者提供 `onExternalLinkPress` 处理函数,该包将责任转移给了使用者,同时保持了安全性和可预测性。
标签:Lexical, PayloadCMS, React Native, 前端组件, 富文本渲染, 移动开发, 自动化攻击