Lexical 富文本编辑器 中文文档 翻译

3,902 阅读6分钟

Lexical

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

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

和React一起使用

注意:不仅限于 React。可以支持任何基于 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 LexicalPlainTextPlugin from '@lexical/react/LexicalPlainTextPlugin';
import LexicalContentEditable from '@lexical/react/LexicalContentEditable';
import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';
import LexicalOnChangePlugin from '@lexical/react/LexicalOnChangePlugin';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';

const theme = {
  // 主题样式放在这里
  ...
}

// 当编辑器变化时,你可以通过LexicalOnChangePlugin组件收到通知
// LexicalOnChangePlugin!
function onChange(editorState) {
  editorState.read(() => {
    // Read the contents of the EditorState here.
    const root = $getRoot();
    const selection = $getSelection();

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

// 插件(plugins)就是React组件, 可以灵活组合
// 另外,你也可以使用懒加载,直到用的时候才产生开销
function MyCustomAutoFocusPlugin() {
  const [editor] = useLexicalComposerContext();

  useEffect(() => {
    // 编辑器聚焦
    editor.focus();
  }, [editor]);

  return null;
}
// 在编辑器更新过程中,捕获错误
// 如果不捕获错误,Lexical会试图恢复,而不会丢失数据
function onError(error) {
  console.error(error);
}

function Editor() {
  const initialConfig = {
    theme,
    onError,
  };

  return (
    <LexicalComposer initialConfig={initialConfig}>
      <LexicalPlainTextPlugin
        contentEditable={<LexicalContentEditable />}
        placeholder={<div>Enter some text...</div>}
      />
      <LexicalOnChangePlugin onChange={onChange} />
      <HistoryPlugin />
      <MyCustomAutoFocusPlugin />
    </LexicalComposer>
  );
}

核心概念

实例(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),并且所有注册方法都返回一个函数用来方便取消注册。 下面是更新监听器的例子

const unregisterListener = editor.registerUpdateListener(({editorState}) => {
  // 更新发生了!
  console.log(editorState);
});

// 随时可以移除注册的监听器
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)和其他可选配置对象:

import {createEditor} from 'lexical';

const config = {
  theme: {
    ...
  },
};

const editor = createEditor(config);

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

const contentEditableElement = document.getElementById('editor');

editor.setRootElement(contentEditableElement);

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

更新编辑器状态

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

const stringifiedEditorState = JSON.stringify(editor.getEditorState().toJSON());

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()返回最新的编辑器状态。 以下是如何更新实例的示例:

import {$getRoot, $getSelection, $createParagraphNode} from 'lexical';
// 在`editor.update`中,可以使用 $前缀的辅助函数。
// 这些函数不能在这个闭包之外使用,会出错。
//(如果你熟悉 React,就像在 React 函数组件之外使用钩子)。

editor.update(() => {
  // 获取根节点
  const root = $getRoot();

  // 获取光标和选择
  const selection = $getSelection();

  // 创建段落
  const paragraphNode = $createParagraphNode();

  // 创建文本
  const textNode = $createTextNode('Hello world');

  // 把文本添加到段落
  paragraphNode.append(textNode);

  // 最后,添加到根节点
  root.append(paragraphNode);
});

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

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

创建自定义节点

本地调试

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

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

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

  1. 下载并安装 VSCode
  • 下载 (建议使用未修改的版本)
  1. 安装扩展

文档

浏览器支持

  • 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)