Lexical
Lexical是一个可扩展的JavaScript网页文本编辑器框架,注重可靠性、可访问性和性能。Lexical旨在提供一流的开发者体验,让您能够轻松地进行原型设计和功能构建。结合高度可扩展的架构,Lexical允许开发者创建独特的、可扩展的文本编辑体验。
有关Lexical的文档和更多信息,请务必访问Lexical网站。
以下是一些使用Lexical可以实现的例子:
概述:
React入门
注意:Lexical不仅限于React。只要为特定库创建了绑定,Lexical就可以支持任何基于DOM的底层库。
安装lexical
和@lexical/react
:
npm install --save lexical @lexical/react
以下是使用lexical
和@lexical/react
的基本纯文本编辑器示例(自己试试)。
import {$getRoot, $getSelection} from 'lexical';
import {useEffect} from 'react';
import {LexicalComposer} from '@lexical/react/LexicalComposer';
import {PlainTextPlugin} from '@lexical/react/LexicalPlainTextPlugin';
import {ContentEditable} from '@lexical/react/LexicalContentEditable';
import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';
import {OnChangePlugin} from '@lexical/react/LexicalOnChangePlugin';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';
const theme = {
// 主题样式在这里
// ...
}
// 当编辑器发生变化时,您可以通过LexicalOnChangePlugin获得通知!
function onChange(editorState) {
editorState.read(() => {
// 在这里读取EditorState的内容。
const root = $getRoot();
const selection = $getSelection();
console.log(root, selection);
});
}
// Lexical React插件是React组件,这使它们具有高度的可组合性。
// 此外,您可以按需懒加载插件,这样在实际使用之前不会产生插件的开销。
function MyCustomAutoFocusPlugin() {
const [editor] = useLexicalComposerContext();
useEffect(() => {
// 当effect触发时,聚焦编辑器!
editor.focus();
}, [editor]);
return null;
}
// 捕获Lexical更新期间发生的任何错误并记录它们
// 或根据需要抛出它们。如果不抛出,Lexical将
// 尝试优雅地恢复而不丢失用户数据。
function onError(error) {
console.error(error);
}
function Editor() {
const initialConfig = {
namespace: 'MyEditor',
theme,
onError,
};
return (
<LexicalComposer initialConfig={initialConfig}>
<PlainTextPlugin
contentEditable={<ContentEditable />}
placeholder={<div>输入一些文本...</div>}
ErrorBoundary={LexicalErrorBoundary}
/>
<OnChangePlugin onChange={onChange} />
<HistoryPlugin />
<MyCustomAutoFocusPlugin />
</LexicalComposer>
);
}
Lexical是一个框架
Lexical的核心是一个无依赖的文本编辑器框架,允许开发者构建强大的、简单和复杂的编辑器界面。Lexical有一些值得探索的概念:
编辑器实例
编辑器实例是将所有内容连接在一起的核心。您可以将可编辑的DOM元素附加到编辑器实例,并注册监听器和命令。最重要的是,编辑器允许更新其EditorState
。您可以使用createEditor()
API创建一个编辑器实例,但在使用如@lexical/react
这样的框架绑定时通常不需要担心这一点,因为这已经为您处理好了。
编辑器状态
编辑器状态是表示您想在DOM上显示的内容的底层数据模型。编辑器状态包含两个部分:
- Lexical节点树
- Lexical选择对象
一旦创建,编辑器状态是不可变的,要创建一个编辑器状态,您必须通过editor.update(() => {...})
来完成。但是,您也可以使用节点转换或命令处理程序"挂钩"到现有的更新中 - 这些在现有的更新工作流程中调用,以防止更新的级联/瀑布效应。您可以使用editor.getEditorState()
检索当前的编辑器状态。
编辑器状态也可以完全序列化为JSON,并且可以使用editor.parseEditorState()
轻松地反序列化回编辑器。
读取和更新编辑器状态
当你想要读取和/或更新词法节点树时,必须通过 editor.update(() => {...})
来执行。你也可以通过 editor.read(() => {...})
或 editor.getEditorState().read(() => {...})
对编辑器状态进行只读操作。
传递给 update 或 read 调用的闭包很重要,且必须是同步的。这是你拥有完整"词法"上下文的活动编辑器状态的唯一地方,并为你提供访问编辑器状态节点树的权限。我们提倡使用带有 $
前缀的函数(如 $getRoot()
)的约定,以表明这些函数必须在此上下文中调用。在 read 或 update 之外尝试使用它们将触发运行时错误。
对于熟悉 React Hooks 的人来说,你可以将这些 $函数视为具有类似功能:
特性 | React Hooks | Lexical $函数 |
---|---|---|
命名约定 | useFunction | $function |
所需上下文 | 只能在渲染时调用 | 只能在 update 或 read 中调用 |
可组合 | Hooks 可以调用其他 hooks | $函数可以调用其他 $函数 |
必须是同步的 | ✅ | ✅ |
其他规则 | ❌ 必须无条件地以相同顺序调用 | ✅ 无 |
节点转换和命令监听器是在隐式的 editor.update(() => {...})
上下文中调用的。
允许进行嵌套更新或嵌套读取,但更新不应嵌套在读取中,反之亦然。例如,editor.update(() => editor.update(() => {...}))
是允许的。允许在 editor.update
的末尾嵌套 editor.read
,但这会立即刷新更新,并且在该回调中的任何额外更新都会抛出错误。
所有 Lexical 节点都依赖于相关的编辑器状态。除了少数例外,你应该只在 read 或 update 调用中调用 Lexical 节点的方法和访问属性(就像 $
函数一样)。Lexical 节点上的方法首先会尝试使用节点的唯一键从活动编辑器状态中定位最新(可能是可写的)版本的节点。一个逻辑节点的所有版本都有相同的键。这些键由编辑器管理,仅在运行时存在(不序列化),应被视为随机和不透明的(不要编写假设键有硬编码值的测试)。
这样做是因为编辑器状态的节点树在协调后会被递归冻结,以支持高效的时间旅行(撤销/重做和类似用例)。更新节点的方法首先调用 node.getWritable()
,这将创建一个冻结节点的可写克隆。这通常意味着任何现有引用(如局部变量)都会引用节点的过时版本,但让 Lexical 节点始终引用编辑器状态可以实现更简单、更不容易出错的数据模型。
:::提示
如果你使用 editor.read(() => { /* 回调 */ })
,它会首先刷新任何待处理的更新,因此你总会看到一个一致的状态。当你在 editor.update
中时,你总是在处理待处理的状态,其中节点转换和 DOM 协调可能还没有运行。editor.getEditorState().read()
将使用最新协调的 EditorState
(在任何节点转换、DOM 协调等已经运行之后),任何待处理的 editor.update
变更尚未可见。
:::
DOM 协调器
Lexical 有自己的 DOM 协调器,它接收一组编辑器状态(总是"当前"和"待处理"的),并对它们应用"差异"。然后它使用这个差异来只更新 DOM 中需要改变的部分。你可以将此视为一种虚拟 DOM,但 Lexical 能够跳过大部分差异比较工作,因为它知道在给定的更新中发生了什么变化。DOM 协调器采用了有利于内容可编辑典型启发式的性能优化,并能够自动确保 LTR 和 RTL 语言的一致性。
监听器、节点转换和命令
除了调用更新外,使用 Lexical 的大部分工作是通过监听器、节点转换和命令完成的。这些都源于编辑器,并以 register
为前缀。另一个重要特性是所有注册方法都返回一个函数,可以轻松取消订阅。例如,这里展示了如何监听 Lexical 编辑器的更新:
const unregisterListener = editor.registerUpdateListener(({editorState}) => {
// 发生了更新!
console.log(editorState);
});
// 确保稍后移除监听器!
unregisterListener();
命令是用于在 Lexical 中连接所有内容的通信系统。可以使用 createCommand()
创建自定义命令,并使用 editor.dispatchCommand(command, payload)
将其分派给编辑器。当触发按键和其他重要信号时,Lexical 内部会分派命令。命令也可以使用 editor.registerCommand(handler, priority)
进行处理,传入的命令会按优先级通过所有处理程序传播,直到某个处理程序停止传播(类似于浏览器中的事件传播)。
使用 Lexical
本节介绍如何独立于任何框架或库使用 Lexical。对于打算在 React 应用程序中使用 Lexical 的人来说,建议查看 @lexical/react
中提供的 hooks 的源代码。
创建编辑器并使用它
使用 Lexical 时,通常使用单个编辑器实例。编辑器实例可以被视为负责将 EditorState 与 DOM 连接起来的对象。编辑器也是你可以注册自定义节点、添加监听器和转换的地方。
可以从 lexical
包创建编辑器实例,它接受一个可选的配置对象,允许主题设置和其他选项:
import {createEditor} from 'lexical';
const config = {
namespace: 'MyEditor',
theme: {
...
},
};
const editor = createEditor(config);
一旦你有了编辑器实例,准备就绪后,你可以将编辑器实例与文档中的内容可编辑 <div>
元素关联:
const contentEditableElement = document.getElementById('editor');
editor.setRootElement(contentEditableElement);
如果你想清除编辑器实例与元素的关联,可以传入 null
。或者,如果需要,你可以切换到另一个元素,只需将另一个元素引用传递给 setRootElement()
。
使用编辑器状态
在 Lexical 中,真实来源不是 DOM,而是 Lexical 维护并与编辑器实例相关联的底层状态模型。你可以通过调用 editor.getEditorState()
从编辑器获取最新的编辑器状态。
编辑器状态可序列化为 JSON,编辑器实例提供了一个有用的方法来反序列化字符串化的编辑器状态。
const stringifiedEditorState = JSON.stringify(editor.getEditorState().toJSON());
const newEditorState = editor.parseEditorState(stringifiedEditorState);
更新编辑器
有几种方法可以更新编辑器实例:
- 使用
editor.update()
触发更新 - 通过
editor.setEditorState()
设置编辑器状态 - 通过
editor.registerNodeTransform()
在现有更新中应用更改 - 使用
editor.registerCommand(EXAMPLE_COMMAND, () => {...}, priority)
的命令监听器
更新编辑器最常见的方式是使用 editor.update()
。调用此函数需要传入一个函数,该函数将提供访问权限以修改底层编辑器状态。当开始一个新的更新时,当前编辑器状态会被克隆并用作起点。从技术角度来看,这意味着 Lexical 在更新期间利用了一种称为双缓冲的技术。有一个编辑器状态代表屏幕上当前显示的内容,另一个正在进行中的编辑器状态代表未来的更改。
协调更新通常是一个异步过程,允许 Lexical 将编辑器状态的多个同步更新批量处理为对 DOM 的单次更新 - 从而提高性能。当 Lexical 准备将更新提交到 DOM 时,更新批次中的底层变更和更改将形成一个新的不可变编辑器状态。然后调用 editor.getEditorState()
将返回基于更新中的更改的最新编辑器状态。
以下是如何更新编辑器实例的示例:
import {$getRoot, $getSelection, $createParagraphNode} from 'lexical';
// 在 `editor.update` 内部,你可以使用特殊的 $ 前缀辅助函数。
// 这些函数不能在闭包之外使用,如果尝试使用会报错。
// (如果你熟悉 React,你可以想象这些有点像在 React 函数组件之外使用 hook)。
editor.update(() => {
// 从 EditorState 获取 RootNode
const root = $getRoot();
// 从 EditorState 获取选择
const selection = $getSelection();
// 创建一个新的 ParagraphNode
const paragraphNode = $createParagraphNode();
// 创建一个新的 TextNode
const textNode = $createTextNode('Hello world');
// 将文本节点附加到段落
paragraphNode.append(textNode);
// 最后,将段落附加到根节点
root.append(paragraphNode);
});
如果你想知道编辑器何时更新以便对更改做出反应,你可以向编辑器添加一个更新监听器,如下所示:
editor.registerUpdateListener(({editorState}) => {
// 最新的 EditorState 可以在 `editorState` 中找到。
// 要读取 EditorState 的内容,请使用以下 API:
editorState.read(() => {
// 就像 editor.update(),.read() 期望一个闭包,
// 你可以在其中使用 $ 前缀的辅助函数。
});
});
为 Lexical 做贡献
请阅读 CONTRIBUTING.md。
可选但推荐,使用 VSCode 进行开发
-
下载并安装 VSCode
- 从这里下载(建议使用未修改版本)
-
安装扩展
- Flow Language Support
- 请确保按照 README 中的步骤进行设置
- Prettier
- 在
editor.defaultFormatter
中将 prettier 设置为默认格式化程序 - 可选:设置保存时格式化
editor.formatOnSave
- 在
- ESlint
- Flow Language Support
文档
浏览器支持
- Firefox 52+
- Chrome 49+
- Edge 79+(当 Edge 切换到 Chromium 时)
- Safari 11+
- iOS 11+(Safari)
- iPad OS 13+(Safari)
- Android Chrome 72+
注意:Lexical 不支持 Internet Explorer 或旧版 Edge。
贡献
- 创建一个新分支
git checkout -b my-new-branch
- 提交你的更改
git commit -a -m '更改描述'
- 有很多方法可以做到这一点,这只是一个建议
- 将你的分支推送到 GitHub
git push origin my-new-branch
- 转到 GitHub 中的仓库页面,点击"Compare & pull request"
- GitHub CLI 允许你跳过这一步的网页界面(以及更多操作)
支持
如果你有任何关于 Lexical 的问题,想讨论 bug 报告,或对新集成有疑问,欢迎加入我们的 Discord 服务器。
Lexical 工程师会定期查看这里。
运行测试
npm run test-unit
仅运行单元测试。npm run test-e2e-chromium
仅运行 chromium e2e 测试。npm run debug-test-e2e-chromium
以调试模式运行 chromium e2e 测试。npm run test-e2e-firefox
仅运行 firefox e2e 测试。npm run debug-test-e2e-firefox
以调试模式运行 firefox e2e 测试。npm run test-e2e-webkit
仅运行 webkit e2e 测试。npm run debug-test-e2e-webkit
以调试模式运行 webkit e2e 测试。
许可证
Lexical 采用 MIT 许可证。