最近咱们微信读书将写想法换成了基于webview的富文本编辑器,遇到了很多问题,这里我将简单的介绍一下咱们在开发过程当中踩到的坑。css
实现富文本编辑器有两个基本思路:html
基于native实现:好比coretext或者textkitnode
基于uiwebview实现android
第一种方案,你须要本身去实现不少在webview已经很成熟的效果,好比连接,字体加粗,标题,引用样式,列表样式等等,这些的工做量都比较可观,并且还有ios/android两个端的对齐问题。还有一个问题,这个多是咱们项目相关的问题,咱们在原来尚未不少富文本要求的状况下,在textview上作了一些咱们对连接的处理工做,仅仅这一个方面,当时就以为不是很方便。ios
第二种方案,你能够借助webview省掉不少在第一种方案里面提到的工做,同时webview相对而言,开源的可供参考的项目也更多一点,不过webview也会存在光标的控制,css的冲突处理以及兼容性的问题,不过在最终选择方案的时候,咱们几经权衡,最终选择了webview的方案git
uiwebview实现富文本编辑器,一个大麻烦在于光标的处理,另外一个大麻烦就是css样式的兼容,具体体现为以下几个方面:github
如何保持光标在可见区域。web
插入表情的时候uiwebview会失焦问题。api
原生命令会有bug,须要本身处理。微信
样式的兼容性。
at以及话题的连接处理。
这里有不少状况,如咱们在当前可见区域的最后一行的时候,再进行换行时,光标会跑到可见区域的下面,webview不会把光标所在位置自动滚动到可见区域,须要手动触发webview的滚动机制.
解决这个问题有两个思路,一种思路就是hack native的滚动逻辑,对滚动进行修正处理,这样作,能使得webview滚动表现的像native同样,可是须要hack的地方会比较多,实现起来可能会踩吭。另一种思路就是直接在js里面操做scrollview,这样相对比较简单。咱们使用的是后者的作法,经过监听光标位置的变化,来进行修正,代码以下:
document.addEventListener("selectionchange", function(e) { RE.calculateEditorHeightWithCaretPosition();//矫正位置 });
这里矫正的具体逻辑是,每次光标位置发生变化时首先计算当前光标的位置,而后判断当前光标是否在可见区域范围内,若是不在,那么执行window.scrollTo滚动到响应区域。这里有个小点须要注意,就是在判断光标位置的时候,顶部以及底部的判断有稍微的区别,若是判断光标是在可见区域上面,须要判断光标的顶部是否在可见区域范围内,若是断光标是在可见区域下面,须要判断光标的底部是否在可见区域范围内。
这里还有一个问题,在最后一行,换行到新的一行进行输入的时候,若是是汉字输入,会产生联想输入条,在尚未肯定输入内容的时候,uiwebview是不知道你须要的高度的,这个时候,因为触发了selectionchange,会致使输入时候,整个界面不断的抖动,所以在这里使用了一个奇技淫巧的方案。咱们监听input的输入,并在在webview的最后面,强制插入一个空白的div,使得输入始终是在已有的区域范围内的。
另外切记不要在js,以及native两端都由scroll的操做,这样会致使滚动的逻辑很混乱。
在咱们的场景中,插入表情会弹出一个表情面板(这应该也是主流的作法),这个表情面板会覆盖键盘,这样会致使uiwebview失焦。这就意味着,咱们在插入表情的时候,不能直接使用Inserthtml命令插入,由于在失焦状态下这个命令会失效,所以在这里须要手动找到正在操做的node,手动执行Insertnode的操做,而且要记录光标的变化位置,以便退出面板退出后,从新foucus找到正确的光标位置。简单的伪代码以下代码
// 这时候光标直接parent是editor,须要判断光标所在的node的nodeIndex,而后将imgNode 插入到parent[nodeIndex]以前 if(currentOffset == currentNode.childNodes.length){ // 光标在editor最后或者editor内容为空,直接append currentNode.appendChild(imgNode); }else{ currentNode.insertBefore(imgNode, currentNode.childNodes[currentOffset]); }
而后更新一下当前selection,同时在表情面板删除表情的时候,也须要作相似的操做。
表情还有一个问题,就是在webview里面插入表情以后,传给后台的时候,由于ios/android两个平台的表情本地文件名,样式有所区别,所以要转化成[发呆]这个格式。
前面在提到webview的优势的时候说过,webview很吸引人的一个地方在于其提供了不少原生的接口来实现同样样式,可是,真正操做起来才发现,套路远比咱们想象的要深。
举一个简单的例子你要实现加粗以及取消加粗,那么一个命令就能搞定。
document.execCommand('bold', false, null);
一样的,按照文档的介绍,若是若是你想要操做quote格式,改为一下命令就行了
document.execCommand('formatBlock', false, "<blockquote>")
然而上面这个命令只能让你添加引用格式,若是要取消引用格式,你会发现然而并无卵用,就须要本身另外想办法了。这里有一个哥们对于quote的悲催经历 ,不过咱们最后的处理方案和他略有不一样,咱们是经过判断当前光标所在的Node是否是有blockquote标签或者是否是含有blockquote标签的Node的子node来决定是添加引用仍是取消引用,伪代码以下:
if (inQuoteBlock.is) { document.execCommand('formatBlock', false, "<div>") } else { document.execCommand('formatBlock', false, "<blockquote>") }
一样的还有标题设置等等都有这样的问题。
这个问题发生在多个样式并存的状况,好比引用、标题、序列格式、高亮连接混在一块儿用会发现各类奇奇怪怪的问题。
以咱们碰到的一个引用和高亮连接混用的例子来讲明。咱们先使用引用格式 blockquote,而后在引用文字里面插入一个a标签,接下来再次输入其余文字的时候,会发现系统帮咱们偷偷的加了一个span标签,这个标签有它本身的style,致使后面样式跟前面的不一致了。
再举一个例子,咱们开发的时候测试发现一个bug,就是当引用、标题、序列格式同时运用到一段文字的时候,会发现系统默默的帮你插入了一个div标签,这个也会致使一些莫名奇妙的格式问题。
固然还有一些其余奇奇怪怪的问题,这些实际上是css样式的问题,对于这些问题的处理,要么本身维护一套css格式库,而后不要使用系统的document.execCommand命令,本身去封装,这个固然是最完全的,可是也是最费工做量的,另一个方法就是去限定某些组合的可能性,或者对某些场景的场景进行特殊处理,固然这个只是不补救的方案啦,具体怎么作,取决于使用场景,毕竟咱们不是作一个word,因此未必须要考虑的那么全面。
其实这里就是对webview的连接处理问题了。以@为例,咱们的需求要求点击@以后,生成一个搜索框,可以搜索想要@的用户,若是使用textview,咱们彻底可以在textView的delegate里面,根据当前的位置以及输入的内容进行搜索,可是webview你是很难去获取用户一段时间输入的内容的,所以,咱们直接使用连接代替,当输入一个@以后就生成一个连接,而后搜索操做就在连接中进行(这里有个小技巧,若是只是输入一个@字符而后将其变成一个Link,那么光标默认的会处在link的外面,所以接下来的输入,不会成为连接的一部分,所以在这里咱们生成的是一个 "@ "加一个空格的连接,并把光标手动移动到@以后)。不过这里还有一个光标的坑,由于咱们选择一个用户以后须要替换掉已经输入的部分,也就是将link内容替换掉,会发现光标会移动到link的最前面去,光标又乱跳了!因此其实这里还须要本身去移动光标!
另外这里在进行搜索的时候还有个问题,就是在使用系统输入法输入中文的时候,会出现联想输入条(quicktype),若是这个时候,用户没有选择输入条的内容,而是直接选择了用户名进行替换,那么咱们会自动将当前的link替换成选择后的内容,并将光标移动到Link的后面,可是这个时候,其实系统输入法的联想输入还没结束,所以当用户再次点击输入的时候,系统会默认找原来开始联想输入时候的Node位置,可是因为这个已经被咱们替换掉了,会找不到,从而使得光标跑到webview的外面去,所以咱们还须要在这里经过监听compositionupdate,进行修正光标的位置
总得来讲,基于webview的富文本,虽然系统帮咱们作了不少事情,可是真正实践起来仍是会发现问题远比咱们想象的多,因此永远不要怀疑word开发那么多年的工做!另外要基于webview作富文本编辑器,那么必定要对Js有必定的了解,要否则会发现很鬼头痛!不过对于大多数app而言,其实咱们的要求是没那么高的,因此找一个适合本身的webview的开源方案仍是能很大的减小本身的工做量。