lexical

Lexical

GitHub Workflow Status Visit the NPM page Add yourself to our Discord Follow us on X

Lexical 是一个可扩展的 JavaScript Web 文本编辑器框架,它侧重于可靠性、可访问性和性能。 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 = {
  // Theme styling goes here
  // ...
}

// When the editor changes, you can get notified via the
// LexicalOnChangePlugin!
function onChange(editorState) {
  editorState.read(() => {
    // Read the contents of the EditorState here.
    const root = $getRoot();
    const selection = $getSelection();

    console.log(root, selection);
  });
}

// Lexical React plugins are React components, which makes them
// highly composable. Furthermore, you can lazy load plugins if
// desired, so you don't pay the cost for plugins until you
// actually use them.
function MyCustomAutoFocusPlugin() {
  const [editor] = useLexicalComposerContext();

  useEffect(() => {
    // Focus the editor when the effect fires!
    editor.focus();
  }, [editor]);

  return null;
}

// Catch any errors that occur during Lexical updates and log them
// or throw them as needed. If you don't throw them, Lexical will
// try to recover gracefully without losing user data.
function onError(error) {
  console.error(error);
}

function Editor() {
  const initialConfig = {
    namespace: 'MyEditor',
    theme,
    onError,
  };

  return (
    <LexicalComposer initialConfig={initialConfig}>
      <PlainTextPlugin
        contentEditable={
          <ContentEditable
            aria-placeholder={'Enter some text...'}
            placeholder={<div>Enter some text...</div>}
          />
        }
        ErrorBoundary={LexicalErrorBoundary}
      />
      <OnChangePlugin onChange={onChange} />
      <HistoryPlugin />
      <MyCustomAutoFocusPlugin />
    </LexicalComposer>
  );
}

Lexical 是一个框架

Lexical 的核心是一个不依赖任何库的文本编辑器框架,它允许开发人员构建强大、简单和复杂的编辑器界面。 Lexical 有一些值得探索的概念

编辑器实例

编辑器实例是将所有内容连接在一起的核心。 您可以将 contenteditable DOM 元素附加到编辑器实例,还可以注册侦听器和命令。 最重要的是,编辑器允许更新其 EditorState。 您可以使用 createEditor() API 创建编辑器实例,但是当使用诸如 @lexical/react 之类的框架绑定时,通常不必担心,因为这会为您处理。

编辑器状态

编辑器状态是表示您要在 DOM 上显示的内容的底层数据模型。 编辑器状态包含两个部分

编辑器状态一旦创建就不可变,为了创建它,您必须通过 editor.update(() => {...}) 来完成。 但是,您还可以使用节点转换或命令处理程序“挂钩”到现有更新中,这些节点转换或命令处理程序作为现有更新工作流程的一部分被调用,以防止更新的级联/瀑布效应。 您可以使用 editor.getEditorState() 检索当前的编辑器状态。

编辑器状态也可以完全序列化为 JSON,并且可以使用 editor.parseEditorState() 轻松地序列化回编辑器中。

读取和更新编辑器状态

当您要读取和/或更新 Lexical 节点树时,必须通过 editor.update(() => {...}) 来完成。 您还可以通过 editor.read(() => {...})editor.getEditorState().read(() => {...}) 对编辑器状态执行只读操作。 传递给更新或读取调用的闭包非常重要,并且必须是同步的。 这是您拥有活动编辑器状态的完整“词法”上下文的唯一位置,并为您提供对编辑器状态的节点树的访问权限。 我们提倡使用以 $ 为前缀的函数(例如 $getRoot())的约定,以传达这些函数必须在此上下文中调用。 尝试在读取或更新之外使用它们会触发运行时错误。

对于那些熟悉 React Hooks 的人,您可以将这些 $ 函数视为具有类似的功能:| *功能* | React Hooks | Lexical $functions | | – | – | – | | 命名约定 | useFunction | $function | | 需要上下文 | 只能在渲染时调用 | 只能在更新或读取时调用 | | 可以组合 | Hooks 可以调用其他 hooks | $functions 可以调用其他 $functions | | 必须同步 | ✅ | ✅ | | 其他规则 | ❌ 必须以相同的顺序无条件调用 | ✅ 无 |

节点转换和命令侦听器是在隐式 editor.update(() => {...}) 上下文中调用的。

允许进行嵌套更新或嵌套读取,但不应将更新嵌套在读取中,反之亦然。 例如,允许 editor.update(() => editor.update(() => {...}))。 允许在 editor.update 的末尾嵌套一个 editor.read,但这会立即刷新更新,并且该回调中的任何其他更新都会引发错误。

所有 Lexical 节点都依赖于关联的编辑器状态。 除了少数例外,您应该只在读取或更新调用中调用方法并访问 Lexical 节点的属性(就像 $ 函数一样)。 Lexical 节点上的方法将首先尝试使用节点的唯一键从活动编辑器状态中找到该节点的最新(并且可能是可写的)版本。 逻辑节点的所有版本都具有相同的键。 这些键由编辑器管理,仅在运行时存在(未序列化),并且应被视为随机且不透明(不要编写假定键的硬编码值的测试)。

这样做是因为编辑器状态的节点树在协调后会递归冻结,以支持有效的时间旅行(撤消/重做和类似用例)。 更新节点的方法首先调用 node.getWritable(),它将创建冻结节点的可写克隆。 这通常意味着任何现有引用(例如局部变量)都将引用节点的过时版本,但是让 Lexical 节点始终引用编辑器状态允许更简单且不易出错的数据模型。

:::tip

如果您使用 editor.read(() => { /* callback */ }),它将首先刷新任何挂起的更新,因此您将始终看到一致的状态。 当您处于 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}) => {
  // An update has occurred!
  console.log(editorState);
});

// Ensure we remove the listener later!
unregisterListener();

命令是用于将 Lexical 中的所有内容连接在一起的通信系统。 可以使用 createCommand() 创建自定义命令,并使用 editor.dispatchCommand(command, payload) 将其分派到编辑器。 当触发按键以及发生其他重要信号时,Lexical 会在内部分派命令。 还可以使用 editor.registerCommand(handler, priority) 处理命令,并且传入的命令会按优先级通过所有处理程序传播,直到处理程序停止传播(类似于浏览器中的事件传播)。

使用 Lexical

本节介绍如何独立于任何框架或库使用 Lexical。 对于那些打算在他们的 React 应用程序中使用 Lexical 的人,建议查看 @lexical/react 中提供的钩子的源代码

创建编辑器并使用它

当您使用 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()。 调用此函数需要传入一个函数,该函数将提供访问权限来修改底层编辑器状态。 当开始新的更新时,当前编辑器状态将被克隆并用作起点。 从技术角度来看,这意味着 Lexical 在更新期间利用了一种称为双缓冲的技术。 有一个编辑器状态来表示屏幕上当前的内容,还有另一个正在进行中的编辑器状态来表示未来的更改。

协调更新通常是一个异步过程,允许 Lexical 将编辑器状态的多个同步更新批量处理为对 DOM 的单个更新,从而提高性能。 当 Lexical 准备好将更新提交到 DOM 时,更新批处理中的底层突变和更改将形成一个新的不可变的编辑器状态。 然后调用 editor.getEditorState() 将返回基于更新中的更改的最新编辑器状态。

这是一个如何更新编辑器实例的示例

import {$getRoot, $getSelection, $createParagraphNode} from 'lexical';

// Inside the `editor.update` you can use special $ prefixed helper functions.
// These functions cannot be used outside the closure, and will error if you try.
// (If you're familiar with React, you can imagine these to be a bit like using a hook
// outside of a React function component).
editor.update(() => {
  // Get the RootNode from the EditorState
  const root = $getRoot();

  // Get the selection from the EditorState
  const selection = $getSelection();

  // Create a new ParagraphNode
  const paragraphNode = $createParagraphNode();

  // Create a new TextNode
  const textNode = $createTextNode('Hello world');

  // Append the text node to the paragraph
  paragraphNode.append(textNode);

  // Finally, append the paragraph to the root
  root.append(paragraphNode);
});

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

editor.registerUpdateListener(({editorState}) => {
  // The latest EditorState can be found as `editorState`.
  // To read the contents of the EditorState, use the following API:

  editorState.read(() => {
    // Just like editor.update(), .read() expects a closure where you can use
    // the $ prefixed helper functions.
  });
});

为 Lexical 做贡献

请阅读 CONTRIBUTING.md

  1. 下载并安装 VSCode

    • 这里下载 (推荐使用未修改的版本)
  2. 安装扩展

    • Flow 语言支持
      • 请务必遵循 README 中的设置步骤
    • Prettier
      • editor.defaultFormatter 中将 prettier 设置为默认格式化程序
      • 可选:设置保存时格式化 editor.formatOnSave
    • ESlint

文档

浏览器支持

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

贡献

  1. 创建一个新分支
    • git checkout -b my-new-branch
  2. 提交您的更改
    • git commit -a -m '对更改的描述'
      • 有很多方法可以做到这一点,这只是一个建议
  3. 将您的分支推送到 GitHub
    • git push origin my-new-branch
  4. 转到 GitHub 中的存储库页面,然后单击“Compare & pull request”
    • GitHub CLI 允许您跳过此步骤的网络界面(以及更多)

支持

如果您对 Lexical 有任何疑问,想讨论错误报告,或对新的集成有疑问,请随时加入我们的 Discord 服务器

Lexical 工程师会定期检查此频道。

运行测试

许可证

Lexical 是 MIT 许可的。