有道写作浏览器扩展作为一款为网页增加英文语法批改的辅助工具,允许用户在任意网页上绝大部分的富文本编辑器、多行文本输入框中编辑英文文本,可实时得到批改结果反馈,并自行接受建议自动修改,实现完美写作。
来源/ 有道技术团队公众号
作者/ 李靖雯
编辑/ 刘振宇
有道写作服务是有道出品的写作智能批改产品,为用户提供优质的作文拼写、语法、样式方面的批改服务。有道写作不仅仅支持浏览器扩展形式,还支持在其他平台使用:例如有道词典 APP-作文批改、Web 在线端、Word 插件、PC 词典内。欢迎各位体验。
http://write.youdao.com/
浏览器插件在浏览器里面的称呼是 Browser Extension,也就是浏览器扩展,是一个扩展网页浏览器功能的插件。它主要基于 HTML、JavaScript、CSS 开发,同时由于是扩展特性,可以利用浏览器本身提供的底层 API 接口进行开发,可以给所用页面或者特定页面添加一些特殊功能而不影响原本页面逻辑。
每个支持扩展的浏览器有自己下载扩展的应用商店,可以直接在应用商店下载。有些产品自己提供浏览器扩展的 .crx 文件让用户下载并安装。
有道写作在 Windows/Mac 系统都可安装,适配 Chrome、360安全浏览器、360极速浏览器、Edge 新版浏览器等,在以上浏览器商店中搜索有道写作,点击安装按钮即可。
在介绍开发思路与实践之前,我们先来直观地看一下有道写作浏览器扩展的实际效果,并对其功能进行简单的介绍。
视觉效果就是,给错误的文本字符下面画一条横线,在 hover 的时候,可以给文本增加一个高亮的效果。在选接受建议的时候,可以替换成我们想要的文本数据。
>>> 在线邮件编辑:
163邮箱
Outlook 邮箱
Gmail
>>> 社交动态、评论:
微博动态
评论
>>> 工具、笔记类:
有道翻译
Google 翻译
石墨文档
>>> 实时批改:
支持一边修改一边实时提供批改反馈,展示批改错误数量。
>>> 语法检测:
![上传中...]()
>>> 增强编辑框:
可以查看每一个错误反馈详细内容,并可分错误类型过滤查看结果。
>>> 接受建议:
点击接受建议时候替换正确文本。
![上传中...]()
需求:扩展需要针对页面上的可输入文本的编辑框赋予批改的功能
那么,网页中可输入文本的编辑框都有哪些呢?
通常我们常见可输入编辑框有:
<input value="123"/> <textarea>123456</textarea>
<blockquote contenteditable="true"> <p>Edit this content to add your own quote</p> </blockquote>
Input 元素通常是一行且输入范围较短的内容,考虑到批改交互的功能,我们的扩展针对以下可输入较多文本的编辑器进行兼容:
我们常见基于 contenteditable 实现的富文本编辑器有百度编辑器、draft.js、 有道云笔记(旧版)等等。
相比 textarea,富文本编辑器可以包含很多不同标签,可以以用来渲染成不同字体颜色的文本、图片、附件、视频、音频等等元素。
实现基于浏览器的富文本编辑器的四要素
四代编辑器的技术选型
更多关于编辑器的介绍,可参考有道技术团队之前发布的文章:
为什么要介绍富文本编辑器内容呢,因为了解多这些编辑器实现方式和保存机制可以帮助后面实现并优化扩展的功能。
一开始的想法是,将原始编辑器的纯文本内容提取并发送到服务端,然后根据服务端返回的数据进行重新的拼接,在错误节点位置使用特殊标记标签进行标注。
以有道写作 Web 端为例:
使用这种方法实现批改效果的还有 163 邮箱英文智能检查、Gmail 自带写信语法检测功能等。这种方法适合我们自定义的编辑器,可以自己控制文本的渲染和指令。
但由于浏览器扩展是基于别人写的编辑器上进行的辅助工具,不能随意修改其文本格式和样式。比如复制带有划线的文本进行粘贴,会出现冗余的划线(除非原本的编辑器有做粘贴文本的标签过滤),但是不能寄希望于别人写的编辑器都有这个功能。
需要分别从两个部分进行考虑:
如何定位画线,并且可以给予其高亮的效果呢?
需要解决的问题是:需要在不影响原编辑器的格式以及功能前提下,将结果划线部分加入到界面上。
>>> contenteditabe:
虚拟一个元素(大小相同,位置相对)在原始编辑器之上,将结果划线标注在这个元素之上,我称之为辅助器。
因为是覆盖在原始编辑器上,需要禁止辅助器的鼠标响应行为,在 hover 的时候需要将鼠标位置同步到辅助器之上。
辅助器需要和原编辑器相同,才能定位准确,需要获得原编辑器以下属性。
问题:如果单纯提取元素的 innerHTML/InnerText/textContent 作为批改请求的参数,无法实现准确定位。
原因:服务端返回结果是根据纯文本定位,网页上的编辑器格式是HTML文档格式,包含不同字体不同格式的标签。
举个例子:html 中有两个块级元素,分别展示两句话,差异只在于两句话 font-size 不一样。
通过 element.textContent 提取出来的内容都是相同的,校验返回的错误标志结果也相同,如下:
因为无法从纯文本的角度得到两种情况差异,难点就在于:需要解析 html 格式,将服务端数据转换到实际格式位置中。
要知道,这相当于要在一个空白的白板元素里添加一个多个绝对定位的高亮元素。需要知道每个错误相对原编辑框的相对位移,和自身宽高。
下面是一个反向推敲的过程:
所以划线的原理是:提取其纯文本的 textnode 节点,根据结果 position 匹配开始的节点和位移、结束节点和位移,获取其文本片段 range 对应编辑器的 x,y,height,width,画出高亮区域。
具体步骤如下:
a. 根据原文所有 html 标签加工过滤,提取纯文本和加工后的文本节点集合:
html 内有各种标签节点,需要根据这些标签不同意义,对标签内的文本进行加工。比如针对 p 标签,通常是表示段落,需要将其包裹的内容后面添加一个换行符。
p 标签处理例子:
问题: 这个换行符是一起发给服务端的,服务端返回来的数据定位也算上了这个换行符。
解决方案: 过滤标签的同时记录文本处理过的位置,在后面的计算反向处理。同时还需要注意字符的转义问题,尤其注意零宽字符的处理。
b. 提取纯文本节点:
(上图文本内容根据标签内容分成5个纯文本节点)
c. 结合服务端数据计算每个错误全文定位:
比如 has 错误对应的错误节点信息。
d. 根据定位获取每个错误节点文本片段:
e. 通过文本片段获取相对视口的位置:
划线步骤图
contenteditable 集合辅助器工作的流程图
>>> textarea:
textarea 本身是无法获取其 textnode 的,它相当于只有一个节点。考虑将其转换成文本节点:
this.textarea.addEventListener('input',this.mirror.update);
>>> 关于突变:
编辑器其实就是一个普通的元素,以下编辑器的交互会引起我们页面内文本节点的变化:
这些变化也就影响我们定位的变化,称之为突变。需要处理每一个突变引起的重新定位问题(重点难点)。
同时,需要监听原始编辑器的输入、字体变化、编辑器尺寸变化等等触发 assist 的重新定位方法。
// 通过ResizeObserver监听编辑器尺寸变化 objResizeObserver = new ResizeObserver((entries) => { var entry = entries[0]; this.elementResizeHandler(entry.target) });
ResizeObserver 兼容性问题需要通过 polyfill 库文件解决。
重新定位方法(mutation):
如何接受一个建议,替换文本:
替换文本意味要修改原编辑器的数据甚至格式,就会造成刚才说的对部分编辑器会引起格式错乱和保存失败的情况。
难点:不影响原始数据存储格式,不影响原始编辑器撤回操作,同时还能触发原编辑器保存机制。
解决方法:不直接用脚本修改 dom 节点,模拟用户修改数据的方式:选中文字,替换内容。
以新版有道云笔记为例子:
入口在点击右下角按钮或者 hover 出现结果卡片时候,点击详细建议进入
>>> 增强编辑框的作用:
增强编辑框是一个特殊的 contenteditable 编辑器。
>>> 初始化、关闭赋值:
在初始化增强编辑器的时候,直接获取原编辑器数据,这里忽略了原编辑器的一些样式、图片,只使用 html 数据部分。
在增强编辑器中编辑后返回原编辑器时候,需要将新数据返回赋值。
>>> 通信:
增强编辑框是嵌入页面的 iframe,只在顶层页面出现。与原来页面的通信是通过postMessage 方式。
(注意:postMessage 不能传递 html 元素和过于复杂的 json object)
如果是原本编辑器是 iframe,需要找到最上层 window.top,利用 window.top 和增强编辑框进行通信。
上图为有道写作浏览器扩展从注入到浏览器页面,以及运行的大致流程。
为了在不影响用户操作前提下,扩展脚本只会在当前页面空闲时候加载,并且批改功能只在部分被用户点击 focus 的编辑器中激活。
以上是开发有道写作浏览器扩展过程中的开发思路和部分技术实现细节,借此机会分享给大家,欢迎与有道技术团队一起探讨更多关于前端、浏览器扩展的知识问题。