36 KiB
am-editor
一个支持协同编辑的富文本编辑器,可以自由的使用React、Vue 等前端常用库扩展定义插件。
English · Demo · 文档 · 插件 · QQ群 907664876 ·
广告
:科学上网,方便、快捷的上网冲浪 稳定、可靠,访问 Github 或者其它外网资源很方便。
Vue2
案例 https://github.com/zb201307/am-editor-vue2
Vue3
案例 https://github.com/yanmao-cc/am-editor/tree/master/examples/vue
React
案例 https://github.com/yanmao-cc/am-editor/tree/master/examples/react
Vue2 DEMO
https://github.com/yanmao-cc/am-editor-demo-vue2
Vue2 Nuxt DEMO
https://github.com/yanmao-cc/am-editor-nuxt
基本原理
使用浏览器提供的 contenteditable
属性让一个 DOM 节点具有可编辑能力:
<div contenteditable="true"></div>
所以它的值看起来像是这样的:
<div data-element="root" contenteditable="true">
<p>Hello world!</p>
<p><br /></p>
</div>
当然,有些场景下为了方便操作,也提供了转换为 JSON 类型值的 API:
[
'div', // 节点名称
// 节点所有的属性
{
'data-element': 'root',
contenteditable: 'true',
},
// 子节点1
[
// 子节点名称
'p',
// 子节点属性
{},
// 字节点的子节点
'Hello world!',
],
// 子节点2
['p', {}, ['br', {}]],
];
编辑器依赖 contenteditable 属性提供的输入能力以及光标的控制能力。因此,它拥有所有的默认浏览器行为,但是浏览器的默认行为在不同的浏览器厂商实现下存在不同的处理方式,所以我们其大部分默认行为进行了拦截并进行自定义的处理。
比如输入的过程中 beforeinput
input
, 删除、回车以及快捷键涉及到的 mousedown
mouseup
click
等事件都会被拦截,并进行自定义的处理。
在对事件进行接管后,编辑器所做的事情就是管理好基于 contenteditable
属性根节点下的所有子节点了,比如插入文本、删除文本、插入图片等等。
综上所述,编辑中的数据结构是一个 DOM 树结构,所有的操作都是对 DOM 树直接进行操作,不是典型的以数据模型驱动视图渲染的 MVC 模式。
节点约束
为了更方便的管理节点,降低复杂性。编辑器抽象化了节点属性和功能,制定了 mark
inline
block
card
4 种类型节点,他们由不同的属性、样式或 html
结构组成,并统一使用 schema
对它们进行约束。
一个简单的 schema
看起来像是这样:
{
name: 'p', // 节点名称
type: 'block' // 节点类型
}
除此之外,还可以描述属性、样式等,比如:
{
name: 'span', // 节点名称
type: 'mark', // 节点类型
attributes: {
// 节点有一个 style 属性
style: {
// 必须包含一个color的样式
color: {
required: true, // 必须包含
value: '@color' // 值是一个符合css规范的颜色值,@color 是编辑器内部定义的颜色效验,此处也可以使用方法、正则表达式去判断是否符合需要的规则
}
},
// 可选的包含一个 test 属性,他的值可以是任意的,但不是必须的
test: '*'
}
}
下面这几种节点都符合上面的规则:
<span style="color:#fff"></span>
<span style="color:#fff" test="test123" test1="test1"></span>
<span style="color:#fff;background-color:#000;"></span>
<span style="color:#fff;background-color:#000;" test="test123"></span>
但是除了在 color 和 test 已经在 schema
中定义外,其它的属性(background-color、test1)在处理时都会被编辑器过滤掉。
可编辑器区域内的节点通过 schema
规则,制定了 mark
inline
block
card
4 种组合节点,他们由不同的属性、样式或 html
结构组成,并对它们的嵌套进行了一定的约束。
特性
- 开箱即用,提供几十种丰富的插件来满足大部分需求
- 高扩展性,除了
mark
inline
block
类型基础插件外,我们还提供card
组件结合React
Vue
等前端库渲染插件 UI - 丰富的多媒体支持,不仅支持图片和音视频,更支持插入嵌入式多媒体内容
- 支持 Markdown 语法
- 支持国际化
- 引擎纯 JavaScript 编写,不依赖任何前端库,插件可以使用
React
Vue
等前端库渲染。复杂架构轻松应对 - 内置协同编辑方案,轻量配置即可使用
- 兼容大部分最新移动端浏览器
插件
快速上手
安装
编辑器由 引擎
、工具栏
、插件
组成。引擎
为我们提供了核心的编辑能力。
使用 npm 或者 yarn 安装引擎包
$ npm install @aomao/engine
# or
$ yarn add @aomao/engine
使用
我们按照惯例先输出一个Hello word!
import React, { useEffect, useRef, useState } from 'react';
import Engine, { EngineInterface } from '@aomao/engine';
const EngineDemo = () => {
//编辑器容器
const ref = useRef<HTMLDivElement | null>(null);
//引擎实例
const [engine, setEngine] = useState<EngineInterface>();
//编辑器内容
const [content, setContent] = useState<string>('<p>Hello word!</p>');
useEffect(() => {
if (!ref.current) return;
//实例化引擎
const engine = new Engine(ref.current);
//设置编辑器值
engine.setValue(content);
//监听编辑器值改变事件
engine.on('change', () => {
const value = engine.getValue();
setContent(value);
console.log(`value:${value}`);
});
//设置引擎实例
setEngine(engine);
}, []);
return <div ref={ref} />;
};
export default EngineDemo;
插件
引入 @aomao/plugin-bold
加粗插件
import Bold from '@aomao/plugin-bold';
把 Bold
插件加入引擎
//实例化引擎
const engine = new Engine(ref.current, {
plugins: [Bold],
});
卡片
卡片是编辑器中单独划分的一个区域,其 UI 以及逻辑在卡片内部可以使用 React、Vue 或其它前端库自定义渲染内容,最后再挂载到编辑器上。
引入 @aomao/plugin-codeblock
代码块插件,这个插件的 语言下拉框
使用 React
渲染,所以有区分。 Vue3
使用 @aomao/plugin-codeblock-vue
import CodeBlock, { CodeBlockComponent } from '@aomao/plugin-codeblock';
把 CodeBlock
插件和 CodeBlockComponent
卡片组件加入引擎
//实例化引擎
const engine = new Engine(ref.current, {
plugins: [CodeBlock],
cards: [CodeBlockComponent],
});
CodeBlock
插件默认支持 markdown
,在编辑器一行开头位置输入代码块语法```javascript
回车后即可触发。
工具栏
引入 @aomao/toolbar
工具栏,工具栏由于交互复杂,基本上都是使用 React
+ Antd
UI 组件渲染,Vue3
使用 @aomao/toolbar-vue
工具栏除了 UI 交互外,大部分工作只是对不同的按钮事件触发后调用了引擎执行对应的插件命令,在需求比较复杂或需要重新定制 UI 的情况下,Fork 后修改起来也比较容易。
import Toolbar, { ToolbarPlugin, ToolbarComponent } from '@aomao/toolbar';
把 ToolbarPlugin
插件和 ToolbarComponent
卡片组件加入引擎,它可以让我们在编辑器中可以使用快捷键 /
唤醒出卡片工具栏
//实例化引擎
const engine = new Engine(ref.current, {
plugins: [ToolbarPlugin],
cards: [ToolbarComponent],
});
渲染工具栏,工具栏已配置好所有插件,这里我们只需要传入插件名称即可
return (
...
{
engine && (
<Toolbar
engine={engine}
items={[
['collapse'],
[
'bold',
],
]}
/>
)
}
...
)
更复杂的工具栏配置请查看文档 https://editor.aomao.com/zh-CN/config/toolbar
协同编辑
通过 MutationObserver
监听编辑区域(contenteditable 根节点)内的 html
结构的突变反推 OT。通过Websocket
与 ShareDB 连接,然后使用命令对 ShareDB 保存的数据进行增、删、改、查。
交互模式
每位编辑者作为 客户端 通过 WebSocket
与 服务端 通信交换由编辑器生成的 json0
格式的数据。
服务端会保留一份 json
格式的 html
结构数据,接收到来自客户端的指令后,再去修改这份数据,最后再转发到每个客户端。
服务端是 NodeJs
环境,使用 express
+ WebSocket
搭建的网络服务。
案例
案例中我们已经一份比较基础的客户端代码
//实例化协作编辑客户端,传入当前编辑器引擎实例
const otClient = new OTClient(engine);
//连接到协作服务端,`demo` 与服务端文档ID相同
otClient.connect(
`ws://127.0.0.1:8080${currentMember ? '?uid=' + currentMember.id : ''}`,
'demo',
);
项目图标
开发
React
需要在 `am-editor 安装依赖
//依赖安装好后,只需要在根目录执行以下命令
yarn start
packages
引擎和工具栏plugins
所有的插件api
支持一些插件所需要的 api 访问,默认使用 https://editor.aomao.com 作为 api 服务ot-server
协同服务端。启动:yarn dev
启动后访问 localhost:7001
Vue
只需要进入 examples/vue 目录安装依赖
//依赖安装好后,在 examples/vue 目录执行以下命令
yarn serve
在 Vue 运行环境中,默认是安装的已发布到 npm 上的代码。如果需要修改引擎或者插件的代码后立即看到效果,我们需要做以下步骤:
- 删除 examples/vue/node_modules/@aomao 文件夹
- 删除 examples/vue/node_modules/vue 文件夹。因为有插件依赖了 Vue,所以 Vue 的包会在项目根目录中安装。如果不删除 examples/vue 中的 Vue 包,和插件的 Vue 包不在一个环境中,就无法加载插件
- 在 am-editor 根目录下执行安装所有依赖命令,例如:
yarn
- 最后在 examples/vue 中重新启动
Vue
案例中没有配置任何后端 API,具体可以参考 React
和 api
设置反向代理
贡献
感谢 pleasedmi、Elena211314、zb201307 的捐赠
如果您愿意,可以在这里留下你的名字。