使用Slate.js创建富文本编辑器
首先,
作为前端工程师,我首次参与了一个丰富的文本编辑器开发项目,我在这个项目中开始接触了一个叫做Slate.js的库。
我将会谈谈在开发过程中的一些经验以及对Slate.js的感受。
Slate.js 是什么?
官方 GitHub 请访问 https://github.com/ianstormtaylor/slate
官方文档请访问 https://docs.slatejs.org
Slate是一种基于React和Immutable构建的框架,可以使用React来开发WYSIWYG编辑器和Markdown编辑器等富文本编辑器。
由于 Slate.js 目前仍处于测试版阶段,存在可能进行破坏性更改的可能性。
斯莱特的优点
-
- 公式のexamplesが豊富
-
- history 管理がラク
-
- Markdown エディタも作れる
- 型を定義して開発するから安全に作れる(TypeScript の場合)
WYSIWYG(即所见即所得)是什么意思?
在计算机用户界面术语中,指的是通过在显示器上呈现与实际处理内容(特别是打印结果)相一致的技术。它是从What You See Is What You Get(所见即所得)的首字母缩写中衍生而来,也有时被称为去掉了”is”的WYSWYG(或称为“表示为所见”)。
与Markdown编辑器的对比
与在 Qiita 的编辑器和 README.md 文件中使用的 Markdown 格式不同,直接将输入的值显示在屏幕上的编辑器被称为所见即所得(WYSIWYG)编辑器。
使用Markdown语法给文档添加样式需要一定的学习成本,但一旦熟悉了使用方法,它易上手且受到许多工程师的喜爱。
与此相反,WYSIWYG 是一个直观易用的功能,它可以将输入的内容直接作为预览显示,并且还可以使用工具栏和快捷键进行装饰。不仅在Microsoft Word和Mac的备忘应用程序中使用,非技术人员也可以直观地使用它。
我特意进行了比较,发现许多所谓的所见即所得编辑器也使用了一些类似于Markdown的语法。
我能做什么
在HTML的textarea标签中,我们可以使用键盘输入将内容显示在屏幕上。然而,由于基本上是将其作为字符串处理,所以很难对其进行部分装饰。
如果是WYSIWYG编辑器,则可以使用工具栏或快捷键来部分加粗、斜体、用代码块包围文本、创建超链接等进行装饰。
此外,还可以根据输入的特定字符添加功能,例如添加#标签或@提及等。
此文稿中的装饰遵循了Qiita的Markdown语法进行记录。
可编辑的内容
如果在HTML的属性中添加contenteditable=”true”,无论是在div标签还是在p标签中,都可以进行编辑。在Slate.js中,可以基于contenteditable开发所见即所得的编辑器。
实践部分
我們將根據公式教程,用Slate.js開發一個編輯器。
完成状态

具有以下功能。
Ctrl + b のショートカットキーで文字を太くする
ツールバーの b(太字)をクリックすると文字を太くする
Ctrl + バッククウォート のショートカットキーでコードブロックを作成する
ツールバーの <>(コードブロック)をクリックするとコードブロックを作成する
Slate的版本
石板: 0.65.x
石板反应: 0.65.x
步骤一:你好世界
软件包安装
yarn add slate slate-react
yarn add -D @types/slate @types/slate-react
创建类型定义文件
如果使用TypeScript编写,必须创建类型定义文件。
touch src/custom-types.d.ts
import { BaseEditor } from "slate";
import { ReactEditor } from "slate-react";
type CustomElement = { type: "paragraph"; children: CustomText[] };
type CustomText = { text: string };
declare module "slate" {
interface CustomTypes {
Editor: BaseEditor & ReactEditor;
Element: CustomElement;
Text: CustomText;
}
}
创建Editor组件
创建一个用于编辑器用户界面的组件。
import React, { useMemo, useState } from "react";
import { createEditor, Descendant } from "slate";
import { Slate, Editable, withReact } from "slate-react";
export const Editor: React.FC = () => {
const editor = useMemo(() => withReact(createEditor()), []);
const [value, setValue] = useState<Descendant[]>(initialValue);
const handleChange = (value: Descendant[]) => {
setValue(value);
};
return (
<div>
<Slate value={value} editor={editor} onChange={handleChange}>
<Editable />
</Slate>
</div>
);
};
const initialValue: Descendant[] = [
{
type: "paragraph",
children: [{ text: "Hello World!" }],
},
];
在Slate组件中,需要三个props,分别为value、editor和onChange。
value中存储的值将在屏幕上显示,并且可以通过onChange进行更改。
我会建立一个开发服务器进行确认。

步骤2. 创建“代码块”。
在类型定义文件中添加一个名为“代码块”的内容
我会添加纯文本元素和代码块元素的类型。
export type ParagraphElement = { type: "paragraph"; children: CustomText[] };
export type CodeElement = { type: "code"; children: CustomText[] };
type CustomElement = ParagraphElement | CodeElement;
type CustomText = { text: string };
declare module "slate" {
interface CustomTypes {
Editor: BaseEditor & ReactEditor;
Element: CustomElement;
Text: CustomText;
}
}
创建“普通文本”和“代码块”元素
创建一个组件来判断是明文还是代码块,并返回结果。
import { RenderElementProps } from "slate-react";
const ParagraphElement: React.FC<RenderElementProps> = (props) => {
return <p {...props.attributes}>{props.children}</p>;
};
const CodeElement: React.FC<RenderElementProps> = (props) => {
return (
<pre {...props.attributes}>
<code>{props.children}</code>
</pre>
);
};
export const Element: React.FC<RenderElementProps> = (props) => {
switch (props.element.type) {
case "code": {
return <CodeElement {...props} />;
}
default: {
return <ParagraphElement {...props} />;
}
}
};
从编辑器组件中调用元素
将 使用 useCallback 进行 memo 化的 Element 传递给可编辑的 renderElement。
export const Editor: React.FC = () => {
const editor = useMemo(() => withReact(createEditor()), []);
const renderElement = useCallback(
(props: RenderElementProps) => <Element {...props} />,
[]
);
const [value, setValue] = useState<Descendant[]>(initialValue);
const handleChange = (value: Descendant[]) => {
setValue(value);
};
return (
<div>
<Slate value={value} editor={editor} onChange={handleChange}>
<Editable renderElement={renderElement} />
</Slate>
</div>
);
};
// 省略
我会检查代码块是否可行。
我将将 Editor.tsx 中 initialState 的 ‘paragraph’ 更改为 ‘code’。

你的代码已经被封装起来了,看起来很顺利。
步骤三:设置快捷键。
用 ctrl + 反引号键对代码进行块状化处理。
创建一个将函数块化的函数。
import { Editor, Transforms, Element } from "slate";
export function toggleCodeElement(editor: Editor): void {
const [match] = Editor.nodes(editor, {
match: (n) =>
!Editor.isEditor(n) && Element.isElement(n) && n.type === "code",
});
Transforms.setNodes(
editor,
{ type: match ? "paragraph" : "code" },
{ match: (n) => Editor.isBlock(editor, n) }
);
}
我们将在可编辑的 onKeyDown 元素中编写根据键盘输入的处理。
export const Editor: React.FC = () => {
// 省略
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.ctrlKey) {
switch (event.key) {
// コードブロック
case "`": {
event.preventDefault();
toggleCodeElement(editor);
return;
}
}
}
};
return (
<div>
<Slate value={value} editor={editor} onChange={handleChange}>
<Editable renderElement={renderElement} onKeyDown={handleKeyDown} />
</Slate>
</div>
);
};
现在可以通过按下ctrl + 反引号键来创建一个代码块了。
第四步:建立工具栏。
创建工具栏并创建代码块化按钮。由于代码块化的处理与按下 ctrl + 反引号时相同,因此将 editor 作为 props 传递。
export const Toolbar: React.FC<Props> = ({ editor }) => {
const handleClickCodeButton = () => {
toggleCodeElement(editor);
};
return (
<div>
<button onClick={handleClickCodeButton}>{"<>"}</button>
</div>
);
};
步骤5:添加“太字”。
接下来我们将添加一个将文字变为粗体的功能。基本上,只需要按照第3和4步的方式编写代码即可。在第3步中,我们将表现为块级元素的code标签添加到名为Element的组件中,而将要变为粗体的strong标签是内联元素,所以我们将其添加到名为Leaf的组件中。
创建并调用一个叶子组件。
import { RenderLeafProps } from "slate-react";
export const Leaf: React.FC<RenderLeafProps> = ({
attributes,
children,
leaf,
}) => {
return <span {...attributes}>{children}</span>;
};
export const Editor: React.FC = () => {
// 省略
const renderLeaf = useCallback(
(props: RenderLeafProps) => <Leaf {...props} />,
[]
);
// 省略
return (
<div>
<Slate value={value} editor={editor} onChange={handleChange}>
<Toolbar editor={editor} />
<Editable
renderElement={renderElement}
renderLeaf={renderLeaf}
onKeyDown={handleKeyDown}
/>
</Slate>
</div>
);
};
接下来,在 custom-types.d.ts 文件中,向 CustomText 类型中添加一个属性来表示文本是否为粗体。
type CustomText = { text: string; bold?: boolean };
下面的代码是为了使 Leaf 组件中的文本加粗而编写的。
在某个处理过程中,根据文本的布尔值进行更改,如果为 true,则在文本中添加 strong 标签。
import { RenderLeafProps } from "slate-react";
export const Leaf: React.FC<RenderLeafProps> = ({
attributes,
children,
leaf,
}) => {
if (leaf.bold) {
children = <strong>{children}</strong>;
}
return <span {...attributes}>{children}</span>;
};
我会创建一个将文字变为粗体的处理过程。
import { Editor } from "slate";
export function toggleMark(editor: Editor): void {
const marks = Editor.marks(editor);
const isActive = marks ? marks["bold"] === true : false;
if (isActive) {
Editor.removeMark(editor, "bold");
} else {
Editor.addMark(editor, "bold", true);
}
}
使用快捷键和工具栏调用在上面创建的处理程序。
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.ctrlKey) {
switch (event.key) {
// コードブロック
case "`": {
event.preventDefault();
toggleCodeElement(editor);
return;
}
// 太字
case "b": {
event.preventDefault();
toggleMark(editor);
return;
}
}
}
};
import React from "react";
import { Editor } from "slate";
import { toggleCodeElement } from "./toggleCodeElement";
import { toggleMark } from "./toggleMark";
type Props = {
editor: Editor;
};
export const Toolbar: React.FC<Props> = ({ editor }) => {
const handleClickCodeButton = () => {
toggleCodeElement(editor);
};
const handleClickBoldButton = () => {
toggleMark(editor);
};
return (
<div>
<button onClick={handleClickCodeButton}>{"<>"}</button>
<button onClick={handleClickBoldButton}>b</button>
</div>
);
};
以上是以 Hands-on 形式介绍和开发 Slate.js 的结束。
我们创建的演示应用程序的存储库在这里。
最后
透過Slate.js可以在各種應用中輸入@提及、#標籤和URL,即使不使用Slate.js,也能實現連結功能。我了解到這種方法。
这次文章的反省之处是,由于我个人对HTML的知识(诸如DOM和Node等)了解不深,所以无法很好地用语言来描述和解释许多地方,我在撰写文章时一直感到沮丧。
由于我在前端学习方面还有很长的路要走,所以很可能会发布错误的信息。如果您发现了错误,请不吝指出或补充,我将非常感激。