Lexical

⚠️ Lexical 目前处于早期开发阶段,API 和包可能会经常更改.

有关 Lexical 的文档和更多信息,请务必访问 Lexical 网站
示例:

安装 lexical@lexical/react:

  1. npm install --save lexical @lexical/react

下面是一个基本的纯文本编辑器示例,使用 lexical@lexical/react (在线沙盒).

  1. import {$getRoot, $getSelection} from 'lexical';
  2. import {useEffect} from 'react';
  3. import LexicalComposer from '@lexical/react/LexicalComposer';
  4. import LexicalPlainTextPlugin from '@lexical/react/LexicalPlainTextPlugin';
  5. import LexicalContentEditable from '@lexical/react/LexicalContentEditable';
  6. import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';
  7. import LexicalOnChangePlugin from '@lexical/react/LexicalOnChangePlugin';
  8. import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
  9. const theme = {
  10. // 主题样式放在这里
  11. ...
  12. }
  13. // 当编辑器变化时,你可以通过LexicalOnChangePlugin组件收到通知
  14. // LexicalOnChangePlugin!
  15. function onChange(editorState) {
  16. editorState.read(() => {
  17. // Read the contents of the EditorState here.
  18. const root = $getRoot();
  19. const selection = $getSelection();
  20. console.log(root, selection);
  21. });
  22. }
  23. // 插件(plugins)就是React组件, 可以灵活组合
  24. // 另外,你也可以使用懒加载,直到用的时候才产生开销
  25. function MyCustomAutoFocusPlugin() {
  26. const [editor] = useLexicalComposerContext();
  27. useEffect(() => {
  28. // 编辑器聚焦
  29. editor.focus();
  30. }, [editor]);
  31. return null;
  32. }
  33. // 在编辑器更新过程中,捕获错误
  34. // 如果不捕获错误,Lexical会试图恢复,而不会丢失数据
  35. function onError(error) {
  36. console.error(error);
  37. }
  38. function Editor() {
  39. const initialConfig = {
  40. theme,
  41. onError,
  42. };
  43. return (
  44. <LexicalComposer initialConfig={initialConfig}>
  45. <LexicalPlainTextPlugin
  46. contentEditable={<LexicalContentEditable />}
  47. placeholder={<div>Enter some text...</div>}
  48. />
  49. <LexicalOnChangePlugin onChange={onChange} />
  50. <HistoryPlugin />
  51. <MyCustomAutoFocusPlugin />
  52. </LexicalComposer>
  53. );
  54. }

核心概念

实例(Editor instances )

编辑器实例是连接一切的核心。您可以将 contenteditable DOM 元素附加进编辑器实例,还可以注册侦听器(listeners )和命令(commands)。最重要的是,编辑器允许更新状态(EditorState). 您可以使用 createEditor()创建实例,一般情况下,使用像@lexical/react的框架时不用担心太多问题。

状态(Editor States)

状态是表示需要显示的内容的底层数据模型。它包含两部分:

  • 节点树 node tree
  • 选择对象 selection object

编辑器状态一旦创建就不可更改,触发更新可以使用 editor.update(() => {...}), 您也可以使用节点转换node transforms)或命令command)钩子更新——它们会在现有更新工作流的一部分被调用,并防止复杂更新(级联/瀑布)。另外,可以使用editor.getEditorState()获取状态。

编辑器状态可以通过 editor.parseEditorState()转为 JSON,方便保存或传输

更新(Editor Updates)

想要更改内容时,必须通过更新来完成,比如editor.update(() => {...}),这个闭包回调很重要。这里是拥有完整的编辑器状态上下文的地方,并且可以访问到所有状态节点树。推荐在这种情况下使用$作为函数名前缀,比较醒目。需要注意的是如果在这个更新回调之外,使用这些函数会报错。对于熟悉 React Hooks 的人,你可以认为它们是类似的(除了$函数可以按任意顺序使用)。

协调器 (DOM Reconciler)

Lexical 有自己的 DOM 协调器,它包含一组编辑器状态(“current”和“pending”)。通过对比差异,只修改差异部分。您可以将其视为一种虚拟 DOM,性能优化,并且能够自动确保 LTR 和 RTL 语言的一致(有些语言是左对齐和右对齐的)。

监听器(Listeners), 节点转换(Node Transforms )和命令( Commands)

除了调用更新editor.update(() => {...})之外,其实大部分工作是通过侦听器、节点转换和命令完成的。但使用前需要注册(register),并且所有注册方法都返回一个函数用来方便取消注册。
下面是更新监听器的例子

  1. const unregisterListener = editor.registerUpdateListener(({editorState}) => {
  2. // 更新发生了!
  3. console.log(editorState);
  4. });
  5. // 随时可以移除注册的监听器
  6. unregisterListener();

命令是用于和内容通信的。可以使用createCommand() 创建自定义命令,并使用editor.dispatchCommand(command, payload)执行。比如当按键被触发或其他情况出现时,内部可以执行命令。命令通过editor.registerCommand(handler, priority)注册,priority代表优先级。命令按优先级传播,直到处理完成(类似浏览器中的事件传播方式)。

开始使用

在React 中使用 Lexical ,建议 查看[@lexical/react](https://github.com/facebook/lexical/tree/main/packages/lexical-react/src).

创建编辑器

创建编辑器实例,并接受一个允许主题(theme)和其他可选配置对象:

  1. import {createEditor} from 'lexical';
  2. const config = {
  3. theme: {
  4. ...
  5. },
  6. };
  7. const editor = createEditor(config);

然后将实例与DOM元素关联:

  1. const contentEditableElement = document.getElementById('editor');
  2. editor.setRootElement(contentEditableElement);

如果要清除编辑器实例,可以设置setRootElement(null) . 或者如果您需要切换到另一个元素setRootElement(newElement).

更新编辑器状态

对于 Lexical,数据源不是DOM,而是底层状态模型。您可以通过调用editor.getEditorState()获取最新的编辑器状态。
编辑器状态可转为 JSON,并且还提供editor.parseEditorState把JSON字符串直接转为编辑器状态。

  1. const stringifiedEditorState = JSON.stringify(editor.getEditorState().toJSON());
  2. const newEditorState = editor.parseEditorState(stringifiedEditorState);

更新编辑器

4种方法更新实例:

  • 通过editor.update()触发更新
  • 通过editor.setEditorState()设置编辑器状态
  • 通过editor.registerNodeTransform()更新
  • 通过editor.registerCommand(EXAMPLE_COMMAND, () => {...}, priority) 命令

最常用的是editor.update(), 需要传一个回调函数来修改状态。开始更新时,当前状态会被被克隆并用作起点。从技术角度来看,这意味着 L在更新期间使用了一种称为双缓冲的技术。有一个当前内容的状态,还有另一个正在更新的未来状态。
创建更新通常是一个异步过程,它允许 Lexical 在一次更新中批量处理多个更新——从而提高性能。当 Lexical 准备好更新 DOM 时,正在更新中的底层状态将形成一个新的不可修改的状态。但你可以随时调用editor.getEditorState()返回最新的编辑器状态。
以下是如何更新实例的示例:

  1. import {$getRoot, $getSelection, $createParagraphNode} from 'lexical';
  2. // 在`editor.update`中,可以使用 $前缀的辅助函数。
  3. // 这些函数不能在这个闭包之外使用,会出错。
  4. //(如果你熟悉 React,就像在 React 函数组件之外使用钩子)。
  5. editor.update(() => {
  6. // 获取根节点
  7. const root = $getRoot();
  8. // 获取光标和选择
  9. const selection = $getSelection();
  10. // 创建段落
  11. const paragraphNode = $createParagraphNode();
  12. // 创建文本
  13. const textNode = $createTextNode('Hello world');
  14. // 把文本添加到段落
  15. paragraphNode.append(textNode);
  16. // 最后,添加到根节点
  17. root.append(paragraphNode);
  18. });

如果您想知道编辑器何时更新以便对更改做出反应,可以向编辑器添加更新侦听器,如下所示:

  1. editor.registerUpdateListener(({editorState}) => {
  2. // 读取最新的 EditorState 内容
  3. editorState.read(() => {
  4. // 和 editor.update() 一样,可以使用以 $ 为前缀的辅助函数
  5. });
  6. });

创建自定义节点

本地调试

  1. 克隆此仓库
  2. 安装依赖
    • npm install
  3. 启动本地服务器并运行测试
    • npm run start
    • npm run test-e2e:chromium 仅运行 e2e 测试
      • 这个服务需要为 e2e 测试运行

npm run start 将同时启动开发服务器和协作服务器。
如果您不需要协作,使用npm run dev仅启动开发服务器.

可选但推荐,使用 VSCode 进行开发

  1. 下载并安装 VSCode
    • 下载 (建议使用未修改的版本)
  2. 安装扩展
    • 流(Flow )语言支持
      • Make sure to follow the setup steps in the README
    • Prettier
      • 将 prettier 设置为默认格式化 editor.defaultFormatter
      • 自动保存格式化 editor.formatOnSave
    • ESlint

      文档

  • How Lexical was designed
  • Testing

    浏览器支持

  • Firefox 52+

  • Chrome 49+
  • Edge 79+ (when Edge switched to Chromium)
  • Safari 11+
  • iOS 11+ (Safari)
  • iPad OS 13+ (Safari)
  • Android Chrome 72+

注意:Lexical 不支持 Internet Explorer 或 Edge 的旧版本。

贡献

  1. Create a new branch
    • git checkout -b my-new-branch
  2. Commit your changes
    • git commit -a -m 'Description of the changes'
      • There are many ways of doing this and this is just a suggestion
  3. Push your branch to GitHub
    • git push origin my-new-branch
  4. Go to the repository page in GitHub and click on “Compare & pull request”
    • The GitHub CLI allows you to skip the web interface for this step (and much more)