news 2026/9/25 3:22:57

ReactPage:基于 React 与 TypeScript 的下一代可扩展 WYSIWYG 内容编辑器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ReactPage:基于 React 与 TypeScript 的下一代可扩展 WYSIWYG 内容编辑器
  • 前端
  • UI组件

【免费下载链接】react-page

Next-gen, highly customizable content editor for the browser - based on React and written in TypeScript. WYSIWYG on steroids.

项目地址:https://gitcode.com/gh_mirrors/rea/react-page
点击查看免费下载

ReactPage(曾用名 ORY Editor)是一个基于 React、使用 TypeScript 编写的开源“所见即所得”(WYSIWYG)内容编辑器核心库。它以可插拔的CellPlugin插件体系替代了传统contenteditable的诸多限制,并以可移植的 JSON 结构存储整页内容。通过阅读本文,你将掌握 ReactPage 的安装方式、<Editor />组件的核心用法、编辑/只读双模式、内置与自定义插件的完整配置,以及多语言与界面本地化等实战方案。

ReactPage 是什么

ReactPage 的定位非常明确——官方 README 中的一句话概括了它的核心主张:"If you are fed up with the limitations ofcontenteditable, you are in the right place."换句话说,它是一套**智能(smart)、可扩展(extensible)、现代化(modern)**的浏览器端内容编辑器方案,而不是另一个基于contenteditable的富文本包装器。

从仓库结构上看,这是一个 Yarn Workspaces + Lerna 管理的 monorepo(见 package.json 与 lerna.json),主要包含三大块:

  • packages/editor:编辑器核心库,最终以@react-page/editor发布,提供<Editor />组件、类型定义、reducer、migrations、渲染器等;
  • packages/plugins/content与packages/plugins/layout:官方维护的插件包(Slate 富文本、Image、Spacer、Divider、Video、Background 等);
  • examples:基于 Next.js 的示例应用,覆盖几乎所有功能的可用示例。

其整体架构可以用一句话概括:编辑器核心(Editor)+ 一组单元格插件(cellPlugins)+ 一份 JSON 内容(value)。内容被拆分为一个个"cell"(单元格),每个 cell 由某个插件负责渲染与编辑,这种设计让内容的增删、拖拽、嵌套、多语言都变得非常灵活。

安装与依赖

ReactPage 提供了稳定的stable与包含不稳定特性的beta两个 npm 发布频道。安装方式如下(命令完整摘录自 README.md):

# 稳定版 yarn add @react-page/editor # 或 npm install --save @react-page/editor # beta 频道(可能包含不稳定特性) yarn add @react-page/editor@beta # 或 npm install --save @react-page/editor@beta

关键依赖说明

根据 packages/editor/package.json,@react-page/editor的peerDependencies为react >= 16.14与react-dom >= 16.14,因此使用 React 16.14 及以上版本即可安装;仓库本身的开发环境已使用 React 18。

官方文档 docs/quick-start.md 还补充了以下实践要点:

  • 富文本插件通常是必需的:ReactPage 自带的富文本编辑能力来自 Slate 插件包,通常需要一并安装:
yarn add @react-page/plugins-slate # 或 npm i --save @react-page/plugins-slate
  • 编辑器 UI 基于 MUI(Material-UI 5):它是懒加载(lazy load)的,不会直接增加主包的 bundle 体积;如果希望覆盖主题,需要额外安装@emotion/react与@emotion/styled:
yarn add @emotion/react @emotion/styled # 或 npm i --save @emotion/react @emotion/styled
  • 记得在应用入口引入样式文件:
import '@react-page/editor/lib/index.css';

快速上手:从零搭建一个编辑器

仓库中的 examples/pages/examples/simple.tsx 是一个最小可运行的示例,其完整逻辑如下:

import React, { useState } from 'react'; // 编辑器核心 import type { Value } from '@react-page/editor'; import Editor from '@react-page/editor'; // 富文本插件(Slate) import slate from '@react-page/plugins-slate'; // 图片插件 import image from '@react-page/plugins-image'; // 定义允许使用的插件列表 const cellPlugins = [slate(), image]; export default function SimpleExample() { const [value, setValue] = useState<Value | null>(null); return ( <Editor cellPlugins={cellPlugins} value={value} onChange={setValue} /> ); }

这段代码展示了 ReactPage 的核心使用模型:

  1. cellPlugins:一个插件数组,决定了编辑器里"能添加什么"。上例中slate()提供富文本段落,image提供图片;
  2. value:当前内容的 JSON 状态,类型为Value | null;
  3. onChange:内容变化时的回调,与 React 的受控组件模式一致——把新的value存进 state,再传回给编辑器。

编辑器对外默认导出Editor组件,同时从 packages/editor/src/index.tsx 还导出了createValue、getTextContents、objIsNode、migrateValue、deepEquals、makeUniformsSchema、Migration等工具与类型,这些在序列化、迁移和文本提取场景中会用到。

编辑与只读双模式:一份内容,两种用途

ReactPage 的<Editor />组件既可以用于编辑内容,也可以用于展示内容,切换开关就是readOnly属性。这在 README 中被强调为项目的关键设计之一:用同一组件无缝衔接"内容生产"与"内容消费"两个场景。

// 编辑模式 <Editor cellPlugins={cellPlugins} value={value} onChange={setValue} /> // 只读模式:仅展示已保存内容,无需 onChange <Editor cellPlugins={cellPlugins} value={value} readOnly />

对应地,examples/pages/examples/readonly.tsx 展示了只读模式的完整写法——直接传入一份预置内容demoSimpleReadOnly即可渲染页面。

背后的实现:按需加载带来的体积优化

只读模式并非简单的"禁用编辑",而是根本不会加载编辑相关的代码。查看 packages/editor/src/editor/Editor.tsx 的实现可以看到:

  • readOnly的默认值是false,value默认null;
  • 编辑用的EditableEditor通过lazyLoad(() => import('./EditableEditor'))进行动态 import 懒加载;
  • 组件挂载时总是先以只读方式渲染(useState(true)),待useEffect根据readOnly决定是否切换到编辑 UI——这避免了 SSR(服务端渲染)时直接加载编辑代码的问题;
  • 因此,当readOnly={true}时,使用 webpack 等打包工具的应用可以显著减小首屏 bundle 体积。

官方文档 docs/editor.md 还给出一个实用场景:在对外公开的页面中默认只读展示,当检测到当前用户具备发布权限时,显示一个按钮把readOnly置为false并传入onChange,即可直接在线上页面完成内容修改与保存。

理解 value:可移植的 JSON 数据结构

ReactPage 的整页内容以一个JSON 对象(Value)表示,它同时包含"数据"(用户看到的正文)与"元数据"(渲染所需的信息:id、版本、插件标识等)。这份 JSON 是"不透明"的——官方明确建议不要依赖其内部结构直接修改,但它有非常明显的优势:

  • 不包含任何展示样式,没有 CSS 被写进内容;
  • 体积小、结构干净,相比经典富文本编辑器产出的"内容与样式混合的 HTML",同一份 JSON 可以被不同的渲染组件以不同方式呈现;
  • 可移植:可以复制到新文档中,用于版本管理或模板克隆。

通过 packages/editor/src/core/utils/createValue.ts 可以一窥Value的基本骨架:它由id、rows(行数组)和version(当前可编辑版本号,取自CURRENT_EDITABLE_VERSION)组成,每一行又包含若干cells,每个 cell 记录size(栅格宽度)、plugin(插件 id 与版本)、dataI18n(按语言存放的数据)等信息。

下面是一个包含图片单元格的实际 JSON 示例(界面效果见下图):

{ "id": "obknih", "version": 1, "rows": [ { "id": "b27eia", "cells": [ { "id": "e9htzt", "size": 12, "plugin": { "id": "ory/editor/core/content/slate", "version": 1 }, "dataI18n": { "default": { "slate": [ { "type": "HEADINGS/HEADING-TWO", "children": [{ "text": "This is a heading" }] }, { "type": "PARAGRAPH/PARAGRAPH", "children": [{ "text": "This is some paragraph text" }] } ] } }, "rows": [], "inline": null } ] }, { "id": "5j8lyl", "cells": [ { "id": "k0t2gk", "size": 12, "plugin": { "id": "ory/editor/core/content/image", "version": 1 }, "dataI18n": { "default": { "src": "https://www.nasa.gov/sites/default/files/styles/full_width/public/thumbnails/image/mars2020-sample-tubes.jpg?itok=SiZDKmmG" } }, "rows": [], "inline": null } ] } ] }

值得注意的是,内容数据被存放在dataI18n中并按语言分键——这正是多语言支持的数据基础(见下文"多语言"章节)。docs/editor.md中提供了更多Value结构的细节与示例。

插件体系:CellPlugin 深度解析

插件是 ReactPage 的灵魂。cell plugin定义了用户可以向文档添加的每一种"单元格"——它由唯一的id、一个title和一个Renderer(渲染该 cell 的 React 组件)构成。完整类型定义见 packages/editor/src/core/types/plugins.ts。

插件的基础属性

一个典型的自定义插件长这样(示例来自 docs/custom-cell-plugins.md):

import { CellPlugin } from '@react-page/editor'; import React from 'react'; // 注意:请使用 type 而不是 interface 来声明数据类型 type Data = { title: string; }; const myFirstCellPlugin: CellPlugin<Data> = { Renderer: ({ data }) => ( <SomeCustomComponent title={data.title} /> ), id: 'myFirstCellPlugin', title: 'My first cell plugin', description: 'My first cell plugin just displays a title', version: 1, controls: { type: 'autoform', schema: { properties: { title: { type: 'string', default: 'someDefaultValue', }, }, required: ['title'], }, }, };

然后把它和其他插件一起传给<Editor />:

<Editor cellPlugins={[myFirstCellPlugin, ...otherPlugins]} value={value} onChange={onChange} />

基础属性一览(与源码中的CellPlugin类型字段一一对应):

属性类型说明
idstring插件的唯一标识,所有使用的插件 id 必须互不相同
titlestring插件的展示名称
descriptionstring附加说明,会显示在插件抽屉(Plugin Drawer)中
iconReactNode插件抽屉中显示的图标,推荐使用 MUI icons
versionnumber当前数据版本号,数据形状变化时需要递增并配套 migration
RendererComponent渲染 cell 内容的组件,接收data、children、readOnly、onChange、focused、lang、isPreviewMode、isEditMode等 props
cellStyleCSSProperties \| (data) => CSSProperties应用到 cell 最外层 div 的样式(而非Renderer本身),常用于调整 cell 内边距
controlsAutoformControlsDef \| CustomControlsDef \| ControlsDefList定义编辑该 cell 的表单控件(见下文)
cellPluginsCellPlugin[] \| (cellPlugins, data) => CellPlugin[]限定该 cell 内部允许嵌套的子插件,既可以是数组,也可以是接收父级插件列表与当前数据的函数
createInitialData(cell) => Data新增 cell 时生成初始数据
createInitialChildren() => PartialRow[]若Renderer渲染children,可在此预置初始的行与子 cell
migrationsMigration[]数据形状变更时,从旧版本迁移到新版本的迁移器数组(需同时递增version)
isInlinableboolean是否可作为浮动内联元素插入其他插件(典型用途:在富文本中插入浮动图片)
allowInlineNeighboursboolean该 cell 是否允许接收isInlinable的邻居
allowClickInsideboolean编辑模式下 ReactPage 默认拦截 cell 的点击/事件以保证可选中、可缩放;设为true可解除拦截(富文本插件即如此)
hideInMenuboolean在插件抽屉中隐藏该插件
ProviderComponent同时包裹Renderer与选中时的 BottomToolbar 的上下文组件,用于在两者间共享状态(Slate 插件用它高亮当前激活的格式)
childConstraints{ maxChildren: number }实验性:当 cell 内行数达到上限后隐藏"+"按钮

三种 controls:autoform / custom / 多标签

controls决定"用户如何编辑这个 cell 的数据",共有三种形态:

  1. { type: 'autoform' }(自动表单):基于你提供的 JSON Schema 自动生成表单,底层使用 uniforms。支持字符串、日期、整数、嵌套对象与数组,也支持自定义字段组件(例如上传图片到 S3 并返回 URL 的字段、用 GraphQL 查询选项的下拉框等)。还可通过 schema 字段中的uniforms属性透传 props:
controls: { type: 'autoform', schema: { properties: { description: { type: 'string', uniforms: { label: 'My field', // 自定义 label placeholder: 'fill me', // 自定义占位符 component: MyCustomComponent, // 覆盖字段组件 showIf: (data) => data.myOtherField === 'something', // 条件显示 multiline: true, // 多行文本 rows: 4, }, }, }, }, }

自定义字段组件通过 uniforms 的connectField实现,例如一个图片上传字段:

import { connectField } from 'uniforms'; const ImageUploadField = connectField(({ value, onChange }) => { return ( <div> {value ? <img style={{ width: 150 }} src={value} /> : null} <input type="file" onChange={async (e) => { // 假设 uploadFile 会把文件上传(如 S3)并返回可访问 URL const url = await uploadFile(e.target.files[0]); onChange(url); }} /> </div> ); });
  1. { type: 'custom' }(自定义控件):当自动表单不适合(已有表单组件库、或有非常特殊的编辑需求)时,直接提供一个自定义组件,它接收与Renderer相同的 props,并通过onChange(data)回写数据。该组件会显示在选中 cell 时的底部工具栏(BottomToolbar)中。

  2. 多控件数组:传入[{ title, controls }, ...],选中该 cell 时工具栏会显示多个 Tab 以切换不同编辑面板,典型场景是"基础配置 / 高级配置"分离:

controls: [ { title: 'Base config', controls: { type: 'autoform', schema: { properties: { title: { type: 'string' } }, required: [], }, }, }, { title: 'Advanced', controls: { type: 'autoform', schema: { type: 'object', required: [], properties: { advancedProperty: { type: 'string' } }, }, }, }, ],

完整示例见 examples/plugins/customContentPlugin.tsx,以及docs/custom-cell-plugins.md中更详尽的属性说明。

官方内置插件

ReactPage 将若干常用插件作为独立 npm 包随官方仓库维护,见 docs/builtin_plugins.md。它们都遵循同一种接入模式:作为cellPlugins数组的一员传入<Editor />。

Slate 富文本插件

Slate 插件是 ReactPage 预置的富文本编辑方案,本质上也是一个 cell plugin(插件 id 形如ory/editor/core/content/slate)。它支持标题、段落、列表、链接、引用、行内元素等丰富的排版能力,详细配置见 docs/slate.md 与 docs/recipes.md。

Image 图片插件

图片插件支持两种图片来源:已有 URL 直链,或上传到任意目标(S3、Backblaze 等)。图片还可以链接到任意 URL,并可设置"在新窗口打开"。

export const cellPlugins = [ // ... 其他插件 imagePlugin({ imageUpload: uploadImage('https://example.com/default.jpg'), }), ];

上传处理器是一个接收File、返回Promise<{ url: string }>的函数。官方文档给出了一个"2 秒后返回默认 URL"的 shim 示例,实际场景中只需在 Promise 内真正执行上传:

function uploadImageShim(defaultUrl) { return function (file, reportProgress) { return new Promise((resolve) => { setTimeout(() => { resolve({ url: defaultUrl }); }, 2000); }); }; }

安装包:@react-page/plugins-image。

Spacer 间隔插件

Spacer 用于在行与行、列与列之间创建可控的空白区域——插在行下方产生垂直间距,插在列旁边产生水平间距。安装包:@react-page/plugins-spacer。

Divider 分割线插件

Divider 用于插入一条水平分隔线。安装包:@react-page/plugins-divider。

Background 背景插件

Background 是一个布局插件:可以为某个区块设置背景图片和/或背景色,且其他 cell 插件可以放在背景插件之上。它通过enabledModes按位组合控制三个模式标签页的显隐:

export const cellPlugins = [ // ... 其他插件 background({ imageUpload: uploadImage('/images/default.svg'), enabledModes: ModeEnum.COLOR_MODE_FLAG | ModeEnum.IMAGE_MODE_FLAG | ModeEnum.GRADIENT_MODE_FLAG, }), ];
  • IMAGE_MODE_FLAG:允许设置背景图片;
  • COLOR_MODE_FLAG:允许设置背景颜色;
  • GRADIENT_MODE_FLAG:允许创建多色渐变。

安装包:@react-page/plugins-background。

此外,仓库还维护了 Video、HTML5 Video 等插件(见 packages/plugins/content),并支持 examples/plugins/cellPlugins.ts 中展示的多插件组合用法。

多语言内容与界面本地化

多语言内容:lang+languages

ReactPage 支持按 cell 级别的多语言内容。向<Editor />传入languages(语言列表)与lang(当前语言 id):

const LANGUAGES = [ { lang: 'en', label: 'English' }, { lang: 'de', label: 'Deutsch' }, ]; <Editor cellPlugins={cellPlugins} value={value} lang="en" onChange={setValue} languages={LANGUAGES} />

工作机制是:每个 cell 默认显示default语言的内容,直到用户为特定语言单独创建了翻译版本。这样编辑者只需翻译需要翻译的 cell,而不用"复制整个页面到另一种语言"——这正是很多 CMS 的多语言痛点。此外,cell 还可以按语言单独隐藏。

界面文本本地化:uiTranslator

uiTranslator接收一个(label?: string) => string函数,所有编辑器界面文案都会经过它。仓库的 examples/pages/examples/i18n.tsx 给出了一个中文翻译示例:

const TRANSLATIONS: { [key: string]: string } = { 'Edit blocks': '编辑', 'Add blocks': '添加', 'Move blocks': '移动', 'Resize blocks': '调整大小', 'Preview blocks': '预览模式', }; const uiTranslator = useCallback((label?: string | null) => { if (label && TRANSLATIONS[label] !== undefined) { return TRANSLATIONS[label]; } return `${label}(to translate)`; }, []);

未命中翻译的文案会以(to translate)后缀提示,方便后续补齐翻译。

布局控制与其他配置

cellSpacing:单元格间距

控制 cell 之间的间距,可传数字或{x, y}对象:

cellSpacing = { x: 15, // 水平间距 y: 20, // 垂直间距 };

childConstraints(实验性)

限制整个编辑器可添加的最大行数,超过后隐藏"+"按钮:

childConstraints: { maxChildren: number, }

注意它目前仅控制按钮显隐,仍可通过拖拽添加新 cell,属于实验性功能。

components(实验性):覆盖编辑器 UI

如果需要更细粒度的 UI 控制,可以通过components覆盖内部组件(目前主要是BottomToolbar,即选中 cell 时显示插件控件与操作的底部工具栏)。官方建议将其作为"最后手段"使用,并优先通过 Issue 反馈自定义需求,以便沉淀为通用特性。

核心 Props 速查表

综合 docs/editor.md 与源码 packages/editor/src/editor/Editor.tsx,<Editor />的核心 props 如下:

Prop类型说明
valueValue \| null要显示的内容(JSON),来源可为文件、数据库、API 等
onChange(newValue: Value) => void内容变化回调,readOnly时可不传
readOnlybooleantrue时仅展示内容,编辑代码不加载,减少 bundle 体积
cellPluginsCellPlugin[]该编辑器可用的插件数组
langstring当前内容语言 id,需与languages配合
languages{ lang: string; label: string }[]可用语言列表
cellSpacingnumber \| { x: number; y: number }单元格间距
uiTranslator(label?: string) => string界面文案翻译函数
childConstraints{ maxChildren: number }实验性:限制最大行数
components对象实验性:覆盖内部 UI 组件

项目背景、现状与本地运行

历史与现状

ReactPage 前身为ORY Editor,由@aeneasr(ORY 团队)创建,如今以@react-page/editor等 scoped 包名发布。需要特别说明的是,仓库 README 顶部明确标注了"LOOKING FOR MAINTAINERS"(正在寻找维护者)——这意味着项目目前处于社区维护力量有限的状态,评估是否在生产环境采用时需自行权衡。

据 README 记载,ReactPage 已被用于以下生产场景(表述以仓库 README 为准):

  • GuestBell Hotel App:用作酒店落地页的 CMS;
  • Veloplus 在线商店:用于创建与展示任意内容页(含落地页);
  • Bike2School:用于骑行上学推广项目的内容编辑。

本地运行示例应用

仓库的examples目录是一个完整的 Next.js 应用(见 examples/package.json)。你可以克隆本仓库后在本地运行全部示例:

git clone https://gitcode.com/gh_mirrors/rea/react-page.git cd react-page yarn install yarn dev

启动后浏览器访问本地 Next.js 服务端口(默认 3000),即可查看simple、readonly、i18n、multicontrols、cellSpacing、nestedPlugins、customToolbar等全部示例(对应 examples/pages/examples 下的页面)。此外仓库还提供了:

  • yarn build:构建所有包;
  • yarn test:运行 Jest 测试(含覆盖率);
  • yarn docs:在 3100 端口启动 docsify 文档站。

社区渠道

项目使用 GitHub Discussion 作为社区讨论板,欢迎在react-page/react-page/discussions发起讨论。文档方面,本仓库的docs目录提供了从快速上手(docs/quick-start.md)、编辑器配置(docs/editor.md)、自定义插件(docs/custom-cell-plugins.md)到内置插件(docs/builtin_plugins.md)、服务端渲染(docs/server-side-rendering.md)、React Admin 集成(docs/integration-react-admin.md)与实用技巧(docs/recipes.md)的完整指南,可作为深入学习的入口。

  • 前端
  • UI组件

【免费下载链接】react-page

Next-gen, highly customizable content editor for the browser - based on React and written in TypeScript. WYSIWYG on steroids.

项目地址:https://gitcode.com/gh_mirrors/rea/react-page
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 3:22:33

Etherpad标题插件ep_headings2:从钩子机制到导出还原的部署指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 3:21:50

RisingWave 实时流式写入 Cassandra / ScyllaDB 完整实战指南

数据库流处理后端数据工程 【免费下载链接】risingwave Event streaming platform for agentic AI. Continuously ingest, transform, and serve event streams in real time, at scale. 项目地址&#xff1a; https://gitcode.com/gh_mirrors/ri/risingwave 点击查看 免费下载…

作者头像 李华