一、why?
如果你有自己创建一个编辑器的需求,重新开发一个编辑器,从效率上将显然不是最佳的方案。随着编辑器不断你的发展,无头编辑器也在大力的发展。
什么只听说过 Headless浏览器,其实本质上还真有相似的地方,无头浏览器,使用代码控制,一般需要展示界面。Headless 编辑器也是没有界面。
在开始之前我们还是了解一下富文本基础知识和历史发展吧。
二、基础
基础的富文本实现方案有很多种,以下是实现富文本实现的普遍方案:
编辑
- iframe
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>iframe designMode Example</title>
<style>
#editor {
width: 100%;
height: 300px;
border: 1px solid #ccc;
}
</style>
</head>
<body>
<h1>Editable iframe Example</h1>
<iframe id="editor"></iframe>
<script>
iframe.onload = function() {
const iframe = document.getElementById('editor');
iframe.contentDocument.designMode = "on";
iframe.contentDocument.body.innerHTML = `
<h2>Editable Content</h2>
<p>You can edit this content directly in the iframe.</p>
`;
};
iframe.srcdoc = '<html><body></body></html>';
</script>
</body>
</html>
- textarea
- contenteditable 属性
- canvas
- Range 和 Selection
```sh
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Selection and Range Example</title>
<style>
#editable {
border: 1px solid #ccc;
padding: 10px;
width: 300px;
height: 150px;
overflow: auto;
}
</style>
</head>
<body>
<div id="editable" contenteditable="true">
This is some editable content. Try selecting some text and then clicking the button below.
</div>
<button id="replaceButton">Replace Selected Text</button>
<script>
document.getElementById('replaceButton').addEventListener('click', function() {
let selection = window.getSelection();
if (selection.rangeCount > 0) {
let range = selection.getRangeAt(0);
let newNode = document.createTextNode('Replaced Text');
range.deleteContents();
range.insertNode(newNode);
selection.removeAllRanges();
let newRange = document.createRange();
newRange.selectNode(newNode);
selection.addRange(newRange);
}
});
</script>
</body>
</html>
contenteditable 中配Selection 和 Range API 能实现选择和操作选区工作。
这里在 selection 获取选区,然后在使用 selection 中的 range 对象做 crud 操作。
交互
- document.execCommand(已经不推荐)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Simple Rich Text Editor</title>
<style>
#editor {
border: 1px solid #ccc;
padding: 10px;
min-height: 200px;
margin-bottom: 10px;
}
.toolbar button {
margin-right: 5px;
}
</style>
</head>
<body>
<div class="toolbar">
<button onclick="execCmd('bold')"><b>B</b></button>
<button onclick="execCmd('italic')"><i>I</i></button>
<button onclick="execCmd('underline')"><u>U</u></button>
<button onclick="execCmd('strikeThrough')"><s>S</s></button>
</div>
<div id="editor" contenteditable="true"></div>
<script>
function execCmd(command, value = null) {
document.execCommand(command, false, value);
}
</script>
</body>
</html>
document.execCommand 虽然不推荐了使用,但是它还是你入门对编辑器思考的重要 api 之一。
数据驱动
数据启动就是不直接操作 DOM,使用数据变化描述 DOM。
Schema
- Schema 约束
数据库模型抽象常用的一些词汇。如果你熟悉 Mongoose 的第一件事就是写 Schema, 用 Schema 来约束数据表,富文本编辑器也类似,很多边与用户操作的界情况需要考虑,这大概也是富文本最的难点吧。
三、发展
不知道什时候,富文本编辑器也有了等级,通常会被分为以下几种等级
L0
L0 级的主要特点是依赖浏览器 DOM api(contenteditable, document.execCommand), 操作的是 dom 结构,没有将数据单独的抽象出来。像 UEditor、TinyMCE 等。L0 方案开发门槛低,与原生浏览器交互直接流畅,但是存在浏览器不同表现不同,同时不支持一些高级功能(协同等)。
L1
L1 级的主要特点是在 L0 上做了数据的抽象,数据模型描述富文本编辑器内容,不再需要维护 DOM 结构。带有数据驱动意思,与前端主流框架意图相似。但是它们依然依赖 DOM api。目前社区主流编辑器大多都是这种方案,他们有 Slate.js、ProseMirror、Quill 等等。
L2 级
L2 基本上就是放弃了 dom 操作, 有的甚至使用 Canvas 全面自己写排版、自己实现内容编辑和交互。基于 Canvas 的编辑器最大的问题就是开发难度大,适合拥有资源的大公司。
对于一般的用户其实 L1 级就够用了。拥有了数据抽象,
四、Headless Editor?
无头编辑器是不含预设用户界面的用户界面文本编辑器框架。他有以下的特点:
- 没有绑定 UI: 无头编辑器不关注集具体的 UI。
- 高度可定化:无头编辑器是要用户的需求自己定制外观和交互。
- 专注于核心功能:专注于文本处理,数据结构,操作和时间处理等
- API驱动:无头编辑器对外暴露 API,方便开发者进行扩展和集成。
- 插件支持:无头编辑器使用插件进行支持。
五、社区中的开源方案
L1 级的编辑器到目前开发成本和实用性最合理,以下是社区中常见的富文本编辑器:
- ProseMirror
- Tiptap
- Slate.js
- Plate.js
- Lexical
- Quill
- ...
在这些编辑器中,其实都处在 L1 级别,其实对一般使用场景就已经够用了,这些编辑器一般都可以实现:
- 自定义工具条
- 浮动菜单
- 斜线菜单
- 侧边菜单
等一些常用功能,下面我们以 Tiptap 为例在 Next.js 中使用,事件
六、tiptap
Tiptap 替我们屏蔽了底层的知识点,使得使用 Tiptap 非常简单,但是我们要明白底层原理。
tiptap 基于 ProseMirror 目前支持主流前端框架。
以 Next.js 为例,很简单的做一个编辑器功能。tiptap 由核心包和插件组成
npm install @tiptap/react @tiptap/pm @tiptap/starter-kit
Tiptap 在 Next.js 中组成一个编辑器,useEditor 钩子函数和 EditorContent 就可以组合成一个简单的编辑器。
'use client'
import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
const Tiptap = () => {
const editor = useEditor({
extensions: [StarterKit],
content: '<p>Hello World! 🌎️</p>',
onUpdate({ editor }) {
const html = editor.getHTML();
onChange?.(html);
},
})
return <EditorContent editor={editor} />
}
export default Tiptap
useEditor 中配置扩展和内容,当然也能监听编辑器变化需要在 useEditor 的 onUpdata 属性中,使用 editor 的getHTML 获取到 html。
当然 Tiptap 的功能远如此。你还可以更加自己的 UI 或者 UI 框架设计符合自己需求的富文本编辑器,甚至实现 AI 聊天功能。。。
七、小结
本文以流行的 headless 编辑器基本讲解富文本编辑器相关知识,包含底层 API, 富文本发展简单历程,headless 浏览器,以及 Tiptap 在 Next.js 中简单实现。当然也有基于 Canvas 底层渲染内容都要自己实现的编辑器,这种虽然最好,但是开发确实需要精兵强将,性价比没有 L1 级别高。