execCommand 有什么问题?不用 execCommand 如何实现富文本,大概的思路是什么?

1,688 阅读4分钟

L0 阶段的富文本(第一代传统编辑器)

L0 阶段也就是最原始的阶段,核心实现完全是依靠着execCommand来实现操作的能力,在搭配着contenteditable = true 来给元素提供可编辑的能力、range, selection 提供选区。 这样就形成了初代富文本。

什么是execCommand

文档 developer.mozilla.org/zh-CN/docs/…

document.execCommand() 作用就是当一个HTML文档切换到设计模式时,document 暴露 execCommand 方法,该方法允许运行命令来操纵可编辑内容区域的元素。

语法: bool = document.execCommand(aCommandName, aShowDefaultUI, aValueArgument)

参数:aCommandName 一个 DOMString ,命令的名称。可用命令列表请参阅 命令 。 aShowDefaultUI 一个 Boolean, 是否展示用户界面,一般为 false。Mozilla 没有实现。 aValueArgument 一些命令(例如insertImage)需要额外的参数(insertImage需要提供插入image的url),默认为null。

命令:背景颜色粗体字体大小拷贝字体颜色等等,命令有很多,可以根据自己需要的功能自己选择

execCommand 的优缺点

可以分两点来说

  1. 自定义命令: 性能稍差,代码量相对较大。为什么说性能稍差代码量较大呢,那是因为execCommand 本身自己实现的命令也就哪一些,并没有覆盖完全,比如我要实现一个创建表格的功能,是没有这个功能的,那怎么办呢,只能靠自己实现了

首先创建表格添加行删除行添加列删除列设置表头取消表头删除表格等等操作,一个功能就要去实现一块代码,具体多少代码得看个人实现的方式了,当然,自己实现功能也就意味着bug可控(想怎么做就怎么做),也可自定义样式、兼容性可控。

  1. 原生命令:

性能佳,bug不可控,兼容性差,实现的功能有限,只接受有限的 commands 。下图就是浏览器兼容性

image.png

execCommand 提供的命令可能在谷歌中有用,但是到了ie上面并不支持,出现了bug也并不能去兼容他,浏览器的特性也是有很大的关系,execCommand 提供的命令是也是存在bug的, 在加上浏览器实现也不一样,出现问题就要去兼容它。但是呢用了原生命令功能也代表这代码量少,毕竟都不用自己实现,浏览器已经知道我们要执行什么命令了。

原生命令并不算太多,对于只要求文本编辑的话那确实是够用,但是要是想带一些复杂的功能呢。还有一个问题就是如果使用contenteditable 特性自带的undo/redo, 那么, 对内容(html)的所有操作都必须使用document.execCommand() 完成, 否则就会破坏内部的undo/redo栈, undo/redo 功能就会不正常。而且, document.execCommand() 能做的事情太少,也就意味着需要自己拓展,还要自己实现undo/redo

总结:

  • 对浏览器差异的屏蔽,和bug的规避,成本巨大,而且不稳定,时不时发现一些新问题(作为wangEditor的一员深知这个问题……);

  • 对有限的命令集进行扩充,但不是基于execCommand() 进行扩展,而是自行封装实现效果,通过工具栏调用;

  • 只是能力扩充,但并没有提供通用扩展接口,开发者无法自定义一种符合业务需要的格式,随着功能的越多,大小也随之变大; 总结一下,最大的问题是:缺乏扩展性。

不用execCommand,该如何去实现富文本?

个人想法(还没有参考别的富文本)

不使用document.execCommand()API去实现一个富文本,首先我想到的是使用JavaScript去封装功能实现。

举个例子,做一个标题 (h1) 的功能

// 代码示例
<div contenteditable = 'true'>
    <p>
        阿斯顿发斯蒂芬
    </p>
</div>

图片效果

image.png

选中文字

image.png

然后通过选区selection来获取选中的dom (p)节点,在生成替换的节点。

const selection = window.getSelection ? window.getSelection() : document.getSelection()
// 获取焦点所在的容器 此时获取到的是text 
const dom = selection.getRangeAt(0).commonAncestorContainer;
// 获取它的父级元素 p
const parentNode = dom.parentNode;
// 然后删除文本节点
parentNode.removeChild(dom)
// 在生成替换的html字符串
parentNode.innerHTML = `<h1>${dom.textContent}</h1>`;
// 最后的效果
<div contenteditable = 'true'>
    <p>
        <h1>阿斯顿发斯蒂芬</h1>
    </p>
</div>

下面看图片的前后对比

image.png

image.png

以上就是整个思路,但并不代表最后的实现代码,只是描述一下思路