引言
富文本编辑器大概是最复杂、使用场景却极广的组件了。
可以说富文本编辑器让Web数据录入充满了无限的想象空间,如果只有文本框、下拉框这些纯文本的数据录入组件,那么Web的数据录入能力将极大地受限。我们将无法在网页上插入图片、视频这些富文本内容,更无法插入自定义的内容。
富文本编辑器让Web内容编辑变得更轻松、更高效,我们几乎可以在富文本编辑器中插入任何你想插入的内容,图片、视频、超链接、公式、代码块,都不在话下,甚至还可以插入表格、PPT、思维导图,甚至3D模型这种超复杂的自定义内容。
富文本编辑器的场景在Web上也是随处可见,写文章、写评论、意见反馈、录需求单,都需要使用到富文本。
今天的主角 Lexical,是 facebook 开源的一款富文本编辑器,我们来看它的介绍
Lexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.
看起来好像很强,而且这个仓库的 star 数已有 8.7k,但是往下一看
⚠️ Lexical is currently in early development and APIs and packages are likely to change quite often.
确实,目前版本还只是 0.2.5,我查了查这个仓库最早的创建时间是 2020-12-03,热知识,其实 facebook 早在 2016-02-19 就开源一个一款富文本编辑器叫 draft-js,那为何又要再开源 Lexical 呢,让我们来一探究竟。
启动
我们先看看他的官网,直接有配合 react 的例子,果然不愧是 Facebook 出品的,话不多说 ctrl c v run 然后看下界面
虽然我知道现在很多流行的富文本编辑器都不是开箱即用的,但是我 ctrl c v 了好歹几十行代码,这出来的界面也太丰富了吧,跟 ProseMirror 可有丶像。
加点东西
那我们看看给它加点东西,先看看官网的 demo ,demo 里的功能很丰富,而且代码就在 Lexical 仓库里,那不如 ᶘ ͡°ᴥ͡°ᶅ
当我打开代码一看,什么,居然用的是 flow 而不是 ts,都这个年代了,真是成也 Facebook 败也 Facebook。不过不重要,这并不能阻拦我 copy 的决心,我准备先给编辑器加个插入图片的功能。
我们先分析一下我们想要的效果,需要一个 button,按下 button,插入一个 img 标签到富文本里,这其中需要一个类似菜单的 button,需要富文本支持 img 标签,还需要点击 button 后有个插入的操作。
如何让 Lexical 支持 img 标签呢,来看看官网怎么说
Nodes are a core concept in Lexical. Not only do they form the visual editor view, as part of the EditorState, but they also represent the underlying data model for what is stored in the editor at any given time.
看来我们先写一个 ImageNode ,ctrl c v,行了我们现在有了。
import {
DecoratorNode,
} from 'lexical';
export class ImageNode extends DecoratorNode {
__src;
__altText;
__width;
__height;
static getType() {
return 'image';
}
static clone(node) {
return new ImageNode(
node.__src,
node.__altText,
node.__width,
node.__height,
);
}
constructor(
src,
altText,
width,
height,
key,
) {
super(key);
this.__src = src;
this.__altText = altText;
this.__width = width || 'inherit';
this.__height = height || 'inherit';
}
createDOM(config) {
const span = document.createElement('span');
const theme = config.theme;
const className = theme.image;
if (className !== undefined) {
span.className = className;
}
return span;
}
updateDOM() {
return false;
}
decorate() {
return (
<img
src={this.__src}
alt={this.__altText}
width={this.__width}
height={this.__height}
/>
);
}
}
export function $createImageNode(
src,
altText,
maxWidth,
){
return new ImageNode(src, altText, maxWidth);
}
然后自己写一个 button,接下来应该要写插入的事件了,但是我发现了一个有意思的事,在 Lexical 中,这样的操作叫 Commands,需要自己创建,于是我们 ctrl c v,有了这么一个 ImagePlugin 组件。
import {useEffect} from 'react';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {
$getSelection,
$isRangeSelection,
$isRootNode,
COMMAND_PRIORITY_EDITOR,
createCommand,
} from 'lexical';
import {$createImageNode, ImageNode} from '../nodes/ImageNode';
export const INSERT_IMAGE_COMMAND =
createCommand();
export default function ImagesPlugin() {
const [editor] = useLexicalComposerContext();
useEffect(() => {
if (!editor.hasNodes([ImageNode])) {
throw new Error('ImagesPlugin: ImageNode not registered on editor');
}
return editor.registerCommand(
INSERT_IMAGE_COMMAND,
(payload) => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
if ($isRootNode(selection.anchor.getNode())) {
selection.insertParagraph();
}
const imageNode = $createImageNode(payload.src, payload.altText, 200);
selection.insertNodes([imageNode]);
}
return true;
},
COMMAND_PRIORITY_EDITOR,
);
}, [editor]);
return null;
}
可以看到,imagePlugin 里自定义了一个 Commands,里面携带的逻辑就使用刚才的 ImageNode 初始化一个实例然后插入编辑器,这里有一个小细节,这边使用的 editor
是通过 useLexicalComposerContext
获取的,所以这个 ImagePlugin 组件必须要放在 LexicalComposer
组件内,其实包括刚才说的 button,也都会放在这里,LexicalComposer
是富文本的核心。
button 的代码就随意写了
<button onClick={() => {
editor.dispatchCommand(INSERT_IMAGE_COMMAND, {
altText: 'Yellow flower in tilt shift lens',
src: jay,
});
}}>insert image</button>
我们来看下效果
点击 button,插入图片(我的照片),完成。
搞点难度
我们回到我取的这个标题,为什么要扯上 Quill呢(当然不是因为我只用过 Quill 拉),因为 Quill 是目前开源的富文本编辑器里面 star 数最高,而且模块化支持的也比较友好,不过 Quill 比较难以接受的缺点就是对嵌套结构的 DOM 不够友好,例如在 table 标签里面 加 ul li 标签,这样的解构 Quill 需要自己开发相关的插件来支持,成本不小,那么我们看看 Lexical 如何呢。
又是一通 ctrl c v,看下效果
虽然看起来有些小问题,但是对嵌套结构是比较友好的。
总结
前面都只是一些尝试,我不太想做官网的搬运工,我来直接说一下我对 Lexical 的理解。
Lexical 本身是比较切合 React 的,这个是天生自带的优势
每一个 DOM 类型,都需要有 对应的 node,当然 Lexical 会有内置一些比较基础的 node,table、list、link 等。
每一个操作 Commands,也需要自己注册,当然也会内置一些 Commands,KEY_ENTER_COMMAND、CLICK_COMMAND 等。
刚才我们插入图片的逻辑是怎么完成的呢,先写好一个 ImageNode 描述 img 标签,初始化的时候带上,再写好一个 ImagePlugin,注册这个插入图片的操作,然后在 button 的点击事件里触发这个操作。
Lexical 自身的数据结构使用它自定义的 state 来描述,里面的核心有两个,一个是 node 树,一个是 selection,node 树我们可以理解,一个 DOM 对应一个 node,那么 DOM 树当然对应 node 树,顺带提一句,树型结构也利于做嵌套处理,selection 就是表示选区了,在富文本里也是一个常见概念,表示鼠标选取中的区域。
但是我们平常怎么使用呢,比如富文本一通操作后得出一个 state,我们总不能把这个 state 提交给后端接口吧,Lexical 当然有给我们提供了方法。
Editor states are serializable to JSON, and the editor instance provides a useful method to deserialize stringified editor states.
const stringifiedEditorState = JSON.stringify(editor.getEditorState().toJSON());
const newEditorState = editor.parseEditorState(stringifiedEditorState);
意思是,把 Lexical 自定义的数据结构 state 序列化成 JSON 字符串,传给后端,初始化的时候,把这个 JSON 字符串反序列化成 state。
说实在的,我并不是很喜欢这种方法,这意味着,和当前的富文本编辑器强绑定,如果万一后期我想换另一个富文本编辑器,无法实现,因为其他富文本编辑器怎么会支持这套 state。
当然 Lexical 自己也有说
With Lexical, the source of truth is not the DOM, but rather an underlying state model that Lexical maintains and associates with an editor instance.
意味着我们就不能直接用 html 字符串和 Lexical 进行交互了,我觉得是8太行的,当然,也有黑科技可以实现,比如我们粘贴复制一段富文本到 Lexical 也是会生成相应的 state 的,那么你就可以参照 lexical-clipboard 的处理逻辑写一个 htmlToState ?,这么麻烦,淦,还玩个🔨。
当然我看了 Lexical 相关 issue,发现其实在 draft-js 就有类似问题,官方也会在后续版本出一些解决方案,我是认为这个问题必须要解决,不然跟 draft-js 不是没差了。
后续还有不少的关键概念比如 Listeners Selection,我没有深入去看,因为 Lexical 还在开发阶段,现在看明白了后期也可能会大变样,那么这次的探索就到这里,下边是我之前 demo 的代码,里面移除了 flow,而且我加了不少的功能,想要玩玩的可以看看。
好的,结束,拜拜 !