前言
近期在github上看到一些编辑器相关的库,就顺势利用一点点时间内探索了下一些编辑器和document.execCommand
这个原生api(如果有对这个api不熟悉的可以看下相应的文档-MDN),简单的概括下就是允许运行命令来操纵可编辑内容区域(contenteditable)的元素。 本文主要介绍下这个api的不足和替代的方案。
思考的方向
- document.execCommand的缺陷
- 当前主流编辑器对于document.execCommand的使用情况
- 探索自定义命令的编辑器
execCommand存在的问题
想必都知道现在市面上很多编辑器(富文本编辑器)其核心的编辑能力都是基于这个api,但是这个api在我了解下存在两个比较大的问题:兼容性问题 和 扩展性问题。
兼容性问题
document.execCommand,MDN明确声明,这是一个Obsolete的特性,浏览器厂商可以不再支持(是一个已经废弃的api)。
并且各个浏览器目前的兼容性都不好。下面是caniuse的兼容性:
从上面的兼容性来看,目前各大浏览器的兼容性也很差。比如现在市面上比较流行的编辑器Quill,UEditor, wangEditor都是基于这个api去实现的。如果要将编辑器做到更高的层次,就必须突破这个节点。
扩展性问题
针对于document.execCommand所能提供的能力还是有点局限,不够已经能够满足大部分需求。但是如果用户需要扩展,就比较棘手,比如用户自定义一些行为等等。
编辑器的能力分析
现在主流的编辑器总的来说对应的是三种类型,不同的类型其展现的能力、可扩展性、复杂性都各不相同。如下表展示的,从L0 -> L1 -> L2也可以说是:站在浏览器的站在浏览器的头上、站在浏览器的肩上和站在浏览器的脚上,逐渐的脱离浏览器,并且向现代化靠近。典型的就是draft.js、slate。
类型 | 描述 | 代表 | 优劣 | L0 | 1. 基于浏览器的contenteditable富文本输入框 1. 使用document.execCommand操作命令 | 轻量级编辑器 典型代表:wangEditor | 优:短时间内快速研发劣:可定制空间非常有限 | L1 | 1. 基于浏览器的contentditable富文本输入框 1. 自主实现操作命令 | 典型代表:draft.js(初始化了一个编辑区)、TinyMCE等 | 优:在浏览器的基础上,能满足大部分业务劣:无法突破浏览器本身的排版效果 | L2 | 1. 自主实现富文本输入框 1. 只依赖少量浏览器API | Google Doc、其他的还有(Office Word Online、WPS文字在线版) | 优:都自己实现,可控度都掌握在开发者劣:技术难度大 |
---|
Google Doc(可以参考:drive.googleblog.com/2010/05/wha…),由contentEditable转到了监听用户交互同时在DOM上通过div等标签绘制的方案。
document.execCommand使用案例
针对于document.execCommand
的使用情况,以wangEditor为例子来分析。wangEditor核心的文件是command.ts来做命令的封装,下面展示一些关键的伪代码:
/**
* 执行操作的命令
* @param name name
* @param value value
*/
public do(name: string, value?: string | DomElement): void {
// TODO
switch (name) {
case 'insertHTML': // 插入HTML字符串
this.insertHTML(value as string)
break
case 'insertElem': // 插入DOM元素
this.insertElem(value as DomElement)
break
default:
// 默认 command 执行浏览器默认的指令
this.execCommand(name, value as string)
break
}
// TODO
}
/**
* 插入 html
* @param html html 字符串
*/
private insertHTML(html: string): void {
// inserHTML 在IE下是没有的,需要兼容处理
if(isNoIE) {
this.execCommand('insertHTML', html)
} else {
// 通过window.selection获取选取,然后利用insertNode来实现在IE下的insertHTML
range.deleteContents()
range.insertNode()
}
}
在插入元素时做了兼容,其余的都是按原生的浏览器api来实现,但是基于这个层面,如果用户想自定义一些事件或者行为,这个设计就显得很局限了。也就是没有突破编辑器的L0,到达L1的能力。
初步探索
基于上面的一些分析,了解了document.execCommand
的不足和一些使用案例。是不是有人看到这里就会想:既然这个api废弃的并且兼容性不好,有没有可以替代的方案。当然是有的,比如浏览器api: Clipboard, 但是兼容性并不是很好,至少对IE是很不有好的。
那有没有其他方案,答案是肯定的。接下来就看下deckdeckgo
这个库怎么去自主实现命令。带着这个目的一起探索下这个处于L1阶段的编辑器。
deckdeckgo
官方介绍:DeckDeckGo
- 用于演示的开源Web编辑器。以幻灯片的形式展示编辑器,这是一个国外的开源项目,其基于@stencil/core
做的组件渲染、和事件的管理,并且有移动和PC端,基本的一些操作都涵盖了。可以归纳到L1能力的编辑器,其借助contentditable
的能力,加上自定义的命令。
加粗
以加粗为例,下面是一个将加粗源码逻辑抽象出来的简单流程图:
说明下:
- 用户点击加粗按钮,触发component组件action-button内部的原生button点击事件
- 事件通过执行被装饰过的句柄emit外部传进来的props事件(对如何触发web component定义事件的可以自行了解下)
- 接下来就是一样的逻辑,依次往复,直到最外层的inline-editor调用了execCommand, 这个就是里面有两个句柄一个是:execCommandStyle(修改样式的自定义指令),另一个是:execCommandList。
注意
: 这里的事件装饰是由@stencil/core
提供的,感兴趣的可以自行去看下这个库。相比于Web component,@stencil/core
内部JSX,渲染的性能会比较高。
execCommandStyle
这个函数的内容也不多,主要基于两个函数: updateSelection
&& replaceSelection
, 一个是更新选区,一个是替换选区。
updateSelection
这个方法主要做的事情就是: 查找是否有选区是否有容器,容器上是否有对应的样式,有就直接更新样式。 在上面流程图可以看到detail下面有个style属性,会通过这个属性去判。下面是源码:
if (sameSelection && !DeckdeckgoInlineEditorUtils.isContainer(containers, container) && container.style[action.style] !== undefined) {
await updateSelection(container, action, containers);
return;
}
async function updateSelection(container: HTMLElement, action: ExecCommandStyle, containers: string) {
container.style[action.style] = await getStyleValue(container, action, containers);
await cleanChildren(action, container);
}
getStyleValue做的事情很简单,就是获取对应的值。 内部做了判断样式继承的操作。
updateSelection
更新的方式,省略了一直创建dom,这就是document.execCommand会干的事情。
但是如果没有找到,又该怎么处理。就是replaceSelection
的事情了。
replaceSelection
这个方法需要配合选区的range, 具体怎么配合看下,下面的伪代码:
async function replaceSelection(container: HTMLElement, action: ExecCommandStyle, selection: Selection, containers: string) {
const range: Range = selection.getRangeAt(0);
const fragment: DocumentFragment = range.extractContents();
const span: HTMLSpanElement = await createSpan(container, action, containers); // 附加指令
span.appendChild(fragment);
await cleanChildren(action, span); // 如果span下面有子元素,需要将所有子元素对应的action.style清空,因为要实现继承。
await flattenChildren(action, span); // 打平span下,没有样式的子元素
range.insertNode(span);
selection.selectAllChildren(span);
}
replaceSelection将选区的内容利用 range.extractContents() 剪切到文档碎片里,然后创建span,在创建span时,就将 指令 里面的 action.style & action.value设置到span上,再将文档碎片塞到span里,再做一些清理操作,最后insertNode到range里。
总结
期间有稍微了解了下draft.js,它并不是开箱即用的,只是提供了很多工具去创建编辑器。 其基于描述的形式,将html描述成一个数据结构,直接按照 React 的模式去做的,通过拦截光标和键盘等操作,然后更新到内部 immutable 的 state 上面,然后在 render 出来。通过 immutable 来提升渲染性能。个人觉得这个方式,编辑器可能会偏重,复杂度可能也会陡升。
对于tinyMCE看的不是很多,但是这个的思想以block为主,也是类似于draft.js,对一个元素进行抽象描述。
最后,这篇内容可能有点问题,见解可能还比较短浅,就当是抛砖引玉,如果文章写得哪里有问题,希望各位看官指点一二。
参考资料:
- caniuse.com/?search=doc…
- developer.mozilla.org/zh-CN/docs/…
- www.zhihu.com/question/40…
- github.com/deckgo/deck…
- github.com/tinymce/tin…
- github.com/facebook/dr…
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!