有道技术团队

有道技术团队 查看完整档案

北京编辑  |  填写毕业院校网易有道  |  技术团队 编辑 shared.youdao.com/www/about.html 编辑
编辑

公众号:有道技术团队

网易有道是中国领先的智能学习公司,致力于提供100%以用户为导向的学习产品和服务。
旗下有网易有道词典、有道精品课、有道云笔记、有道翻译官等多款深受用户喜爱的产品。

个人动态

有道技术团队 发布了文章 · 4月8日

团队协作中,如何写出让同事赞不绝口的代码

团队中的每个人都会用不同的视角来’审视‘你的”作品“,那么我们如何拿出一份像艺术品一样的项目代码,然后赢得得同事们的赞许呢?

作者/ 琼虎(安增平)

编辑/ hjy

00 前言

在加入了拥有较高技术底蕴的有道精品课团队后,发现自己在前面的职业生涯中养成的一些‘作坊’习惯必须得到纠正。

在日常工作中,研发同学只在coding阶段中不需要别人关心自己的代码,其他需要将自己的产出展示给别人的场景变得十分常见。

简单举几个例子:

  • feature准入后,同产品业务线的同事需要trans-review

  • mentor每个季度要Lint-review

  • 测试二轮后要diff-review

  • ......

团队中的每个人都会用不同的视角来“审视”你的”作品“,那么我们如何拿出一份像艺术品一样的项目代码,然后获得同事们的称赞呢?

保持在项目中做到以下几点,便可收获殿堂级的艺术代码。

以下几点是在接手销售转化系统及质检系统等几个项目后,针对自己的不a足和团队成员交流得出的结论。

01 使用meaningful的变量命名

在声明一个变量的时候,尽可能的将其作用和充当的角色注入其中:

声明一个函数,使用组合动词而非名词:

声明一个集合内部包含多项内容的时候,要记得使用复数形式:

在使用数学计算公式的时候尽量提前声明好常量,常量的注入有助于提升你在维护代码阶段的可读性:

在回调函数或者函数声明的形参中,尽量保持形参的语义化,避免后期维护过程中看到前面随意声明的i,j,k后,又要折返到原回调处进行查看,影响开发效率:

(同时在使用TS的过程中也尽量避免使用any类型,使用这种类型在codeReview过程中可能会被灵魂拷问)同时在声明boolean类型的时候要以is作为开头:

做到以上这些,在codeReview中就可以保持一个自信的状态去接受同事们领导们的审阅,因为没有犯低级错误可以让查看你代码的人保持心情愉悦,同时这种心情可以对你产生正反馈。

02 每个函数只做一件事

每个函数尽量保持其职责的单一性,不要出现一个非常强健的函数做了很多事情:

And这种单词本身就不是函数的一部分,他会导致添加过多的业务依赖或职责到当前的函数中,从长远的角度看这绝对是弊大于利的。

03 让函数保持"纯洁"

在函数外的任何东西,任何变量都不是他的业务,所以好的函数应该和函数外的任何变量保持好隔离。

下面这段代码可能只有刚入门的新手才会写出来,但是这种混乱的逻辑在业务复杂了之后,很可能会混入‘你’的代码中:

上面的例子可以改成下面这样:

当然在ES6的使用过程中上述问题普遍已经不存在了,但纯函数的思想需要时刻谨记。

04 模块化业务逻辑

当你在创建了一些函数之后,发现他们在当前的业务中做了一些比较类似的行为。例如,验证用户登陆的用户名和密码,那么我们最好可以将其归类为一个模块中。

这里我们可以称之为验证模块,而不是简单的使用一个util或者server将其集中起来就完事了:

05 简化条件逻辑

如果一个业务中出现了大量的if else这种内容,想必开发人员看到会十分头痛。

举个简单的例子:

仔细看下这里的else其实是不需要的,我们可以通过提前返回来remove掉:

06 enrich u Error log

当我们浏览某个App或网站时,经常会在点击某个按钮弹出“An Error Occurs”这种提示,这种提示很不友好,我们无法排查到底出现了什么原因,用户更是一头雾水,但是假如在出现这种错误的时候将描述信息填充的完整些,对用户或是技术支持都会有一个很棒的使用体验。

例如:当用户在表单中没有输入信息:

当用户此时网络出现了故障:

对开发者而言,一个详尽的提示能让你轻松定位到问题,节省了大量的时间:

包含但不限于这几种错误格式,还有showMessage等方法可以提供......

07 利用好编辑器中的插件

在VSCode下开发的同学,可以通过安装prettier来保持漂亮的代码。同时借助ESLint可以让你在开发时注重缩进、空格这些格式化的内容。

假如在开发过程中注入了TS,那么开启typescript-eslint会帮助你规范自己的类型定义,塑造一个风格严谨的代码style。

借助这些插件让我们的代码格式化时间大大降低,从而我们可以将更多的时间放在提升代码质量上。

08 总结

以上列举的几个例子较为简单。通过这些通俗易懂的例子,大家在工作中根据自己的理解举一反三的运用起来。那便是起到了作用。

在开发中切勿眼高手低,在编码上做到一丝不苟,对我们技术的成长会有很大帮助。

唯有持之以恒,几十年如一日的训练才能见证技术圈的匠人诞生。共勉!

-END-

查看原文

赞 9 收藏 1 评论 1

有道技术团队 发布了文章 · 3月25日

报名倒计时 | 有道技术沙龙,聊聊明星语音背后的故事

自上周有道技术沙龙开始招募以来

已经收到了众多小伙伴的报名

给爱学习的同学们点个赞!

目前,有道技术沙龙仍在报名中

欢迎来有道技术沙龙现场交流

到场观众赠送网易纪念款帆布包

严选好物抽奖送不停!

IMG20210325_114102.png
同时

多位无法到现场的同学咨询直播问题

有道技术团队决定联合 CSDN

开启线上直播

全程直播技术分享环节!

直播间地址请点击 https://live.csdn.net/room/wl...

有道技术沙龙等你来~


彩蛋一枚:明星语音功能全新升级 有道词典首位女声明星上线

IMG20210324_175104.png


640.png
-END-

查看原文

赞 0 收藏 0 评论 0

有道技术团队 发布了文章 · 3月25日

活动 | Apache Pulsar Meetup 欢迎报名

由 StreamNative 发起

有道技术沙龙联合主办的

Apache Pulsar Meetup 北京站

走进网易北京研发中心!

Apache Pulsar PMC 成员、StreamNative CTO 翟佳

有道精品课实时数仓负责人李荣谦等多位业内实践者

将带来精彩的分享

时间:2021年3月27日13:30-17:30

地点:网易北京研发中心 B座1层报告厅

欢迎扫描图中二维码或点击https://www.huodongxing.com/e... 报名!

IMG20210325_113910.png

  • END -
查看原文

赞 0 收藏 0 评论 0

有道技术团队 发布了文章 · 3月25日

有道技术岗大揭秘!这么幸福的生活,真的是熬夜掉发Top1的职业吗?

1.webp
2.webp
3.webp
4.webp
5.webp
6.webp
7.webp
8.webp
9.webp
10.webp

招聘信息

点击 https://hr.163.com/product.ht... 查看详情

社招岗位

  • 技术专家
  • JAVA工程师
  • 直播技术支持
  • 高级前端工程师
  • Android 工程师
  • 前端开发工程师
  • 安卓开发工程师
  • WEB前端工程师
  • 嵌入式软件工程师
  • C++/Qt开发工程师
  • 高级安卓开发工程师
  • Windows开发工程师
  • 高级Android工程师
  • 资深U3D开发工程师
  • 高级前端开发工程师
  • 高级测试开发工程师
  • web前端研发工程师
  • 高级JAVA开发工程师
  • 流媒体传输算法工程师
  • Java服务端开发工程师
  • 流媒体服务端研发工程师
  • iOS开发工程师(词典产品部)
  • iOS开发工程师(有道精品课)
  • 高级Windows C++ 开发工程师
  • Android Framework系统工程师

校招岗位

  • 测试工程师
  • 测试开发工程师
  • iOS开发工程师
  • Java开发工程师
  • C++开发工程师
  • 前端开发工程师
  • NLP算法工程师
  • 文本挖掘工程师
  • 语音算法工程师
  • 视频算法工程师
  • 推荐算法工程师
  • 运维开发工程师
  • 大数据开发工程师
  • Android开发工程师
  • 计算机视觉算法工程师
  • 高性能计算工程师(C++)
  • 技术支持(教学直播及场景优化)

实习生岗位

  • 测试实习生
  • 运维开发实习生
  • 测试开发实习生
  • NLP算法实习生
  • 语音算法实习生
  • C++开发实习生
  • 算法工程实习生
  • iOS开发实习生
  • 推荐算法实习生
  • Java开发实习生
  • 前端开发实习生
  • 围棋AI算法实习生
  • 高性能计算实习生
  • 嵌入式开发实习生
  • 大数据开发实习生
  • 音视频算法实习生
  • Android开发实习生
  • 计算机视觉算法实习生

-END-

查看原文

赞 0 收藏 0 评论 0

有道技术团队 发布了文章 · 3月17日

有道技术沙龙 | AI 语音交互技术在语言学习场景的实践

经过数月的精心筹备
有道技术沙龙即将正式启动!
首先感谢:思否、将门-TechBeat、掘金技术社区合作伙伴的大力帮助

首期我们将和大家聊聊
语音交互技术的那些事儿
明星爱豆的声音是如何生成的?

一根笔如何教会小朋友读绘本?
选择题也可以用语音来回答吗?
答案尽在 有道技术沙龙 第一期!

除了干货满满的技术分享
当然不能少了福利
吃好喝好必不可少
还有精美伴手礼到场即送
抽奖环节拼欧气抽严选好物
前30位到场的早鸟同学
还可兑换网易易间咖啡馆饮品一杯!

活动场地:网易易间咖啡厅

更重要的是:

作为首期有道技术沙龙的听众
有道将奉送 面试直通车 专属福利!
跳过重重难关
直接和终极 Boss 面对面!

活动信息:

时间:2021年3月27日 14:00-17:00

地点:网易北京研发中心-B1F 易间

【温馨提示】疫情防控不能松懈!到场需出示北京市健康码,以及报名手机号码接收的访客信息。

欢迎扫描上方长图二维码或点击有道技术沙龙第一期-报名表报名!

有道技术沙龙等你来!
如有疑问
欢迎添加有道小助手微信:ydtech01


相关岗位热招中!欢迎投递简历!

  • 语音合成算法专家
  • 语音识别&口语评测算法专家
  • 声学前端信号处理算法专家
  • 对话&语义理解算法专家
  • 语音引擎技术专家
  • 语音服务后台技术专家

详情点击:网易招聘

查看原文

赞 0 收藏 0 评论 0

有道技术团队 发布了文章 · 3月11日

有道 Kubernetes 容器API监控系统设计和实践

本期文章,我们将给大家分享有道容器服务API监控方案,这个方案同时具有轻量级和灵活性的特点,很好地体现了k8s集群化管理的优势,解决了静态配置的监控不满足容器服务监控的需求。并做了易用性和误报消减、可视化面板等一系列优化,目前已经超过80%的容器服务已经接入了该监控系统。

来源/ 有道技术团队微信公众号
作者/ 郭超容 王伟静
编辑/ hjy

1.背景

Kubernetes 已经成为事实上的编排平台的领导者、下一代分布式架构的代表,其在自动化部署、监控、扩展性、以及管理容器化的应用中已经体现出独特的优势。

在k8s容器相关的监控上, 我们主要做了几块工作,分别是基于prometheus的node、pod、k8s资源对象监控,容器服务API监控以及基于grafana的业务流量等指标监控。

在物理机时代,我们做了分级的接口功能监控——域名级别接口监控和机器级别监控,以便在某个机器出现问题时,我们就能快速发现问题。

上图中,左边是物理机时代对应的功能监控,包括域名级别接口监控和3台物理机器监控。右边是对应的k8s环境,一个service的流量会由k8s负载均衡到pod1,pod2,pod3中,我们分别需要添加的是service和各个pod的监控。

由于K8s中的一切资源都是动态的,随着服务版本升级,生成的都是全新的pod,并且pod的ip和原来是不一样的。

综上所述,传统的物理机API不能满足容器服务的监控需求,并且物理机功能监控需要手动运维管理,为此我们期望设计一套适配容器的接口功能监控系统,并且能够高度自动化管理监控信息,实现pod API自动监控。

2.技术选型

为了满足以上需求,我们初期考虑了以下几个方案。

1. 手动维护各个service 和pod 监控到目前物理机使用的podmonitor开源监控系统。

2. 重新制定一个包含k8s目录树结构的系统,在这个系统里面看到的所有信息都是最新的, 在这个系统里面,可以做我们的目录树中指定服务的发布、功能监控、测试演练等。

3. 沿用podmonitor框架,支持动态获取k8s集群中最新的服务和pod信息,并更新到监控系统中。

+方案分析+

针对方案一,考虑我们服务上线的频率比较高,并且k8s设计思想便是可随时自动用新生成的pod(环境)顶替原来不好用的pod,手动维护pod监控效率太低,该方案不可行。

第二个方案应该是比较系统的解决办法,但需要的工作量会比较大,这种思路基本全自己开发,不能很好的利用已有的功能监控系统,迁移成本大。

于是我们选择了方案三,既能兼容我们物理机的接口功能监控方案,又能动态生成和维护pod监控。

3.整体设计思路

k8s监控包括以下几个部分:

其中API功能监控,是我们保证业务功能正确性的重要监控手段。

通常业务监控系统都会包含监控配置、数据存储、信息展示,告警这几个模块,我们的API功能监控系统也不例外。

我们沿用apimonitor框架功能,并结合了容器服务功能监控特点,和已有的告警体系,形成了我们容器API功能监控系统结构:

首先介绍下目前我们物理机使用的apimonitor监控:一个开源的框架
https://gitee.com/ecar_team/a...

可以模拟探测http接口、http页面,通过请求耗时和响应结果来判断系统接口的可用性和正确性。支持单个API和多个API调用链的探测。

如下图所示,第一行监控里面监控的是图片翻译服务域名的地址,后边的是各台物理机的ip:端口。

点开每条监控

我们沿用apimonitor框架的大部分功能,其中主要的适配和优化包括:

1. 监控配置和存储部分:一是制定容器服务service级别监控命名规则:集群.项目.命名空间.服务;(和k8s集群目录树保持一致,方便根据service生成pod监控),二是根据service监控和k8s集群信息动态生成pod级别监控,

2. 监控执行调度器部分不用改动

3. 信息展示部分,增加了趋势图和错误汇总图表

4. 告警部分,和其它告警使用统一告警组。

4.具体实践操作

4.1 添加service 级别API监控告警

需要为待监控服务,配置一个固定的容service级别监控。

service级别监控命名规则:集群.项目.命名空间.服务

以词典查词服务为例,我们配置一条service级别的多API监控(也可以是单API监控)

· 单API:一个服务只需要加一条case用

· 多API:如果一个服务需要加多条功能case

其中“所属系统” 是服务所属的告警组,支持电话、短信、popo群、邮件等告警方式(和其它监控告警通用)

任务名称:取名规则,rancher中k8s集群名字.项目名字.命名空间名字.service名字(一共四段)

告警消息的字段含义:

docker-dict:告警组,订阅后会收到告警消息

k8s-prod-th:集群

dict: 项目

dict:命名空间

data-server:workload名字

data-server-5b7d996f94-sfjwn:pod名字

{} :接口返回内容, 即:response.content

http://dockermonitor.xxx.youdao.com/monitorLog?guid=61bbe71eadf849558344ad57d843409c&name=k8s-prod-th.dict.dict.data-server.data-server-5b7d996f94-sfjwn : 告警详细链接

4.2 自动生成pod API监控

自动生成下面三行监控任务:(第一行监控是按上面方法配置的容器service ip监控,后边三行是自动生成pod监控任务 )

监控service级别是单API,则自动生成的是单API,service级别是多API,则自动生成的是多API监控。

自动生成的pod级别监控,除了最后两行标红处(ip: port)和service级别不一样,其他都一样。

实现pod自动生成的方法

1.给podmonitor(改框架是java语言编写的),增加一个java模块,用来同步k8s信息到podmonitor中。考虑到修改podmonitor中数据这个行为,本身是可以独立于框架的,可以不修改框架任何一行代码就能实现数据动态更新。

2.对比podmonitor数据库和k8s集群中的信息,不一致的数据,通过增删改查db,增加pod的监控。由于数据之间存在关联性,有些任务添加完没有例行运行,故采用了方法三。

3.对比podmonitor数据库和k8s集群中的信息,不一致的数据,通过调用podmonitor内部接口添加/删除一项监控,然后调接口enable /disable job等。按照可操作性难易, 我们选择了方法三

针对于k8s集群中查到的一条pod信息:总共有三种情况:

1.对于表中存在要插入pod的监控信息记录,并且enable状态为1。则认为该pod的监控不需要改变

2.对于表中存在要插入pod的监控信息记录(删除操作并不会删除源数据信息),并且enable状态为0。则认为该pod的监控已被删除或者被停止。调用删除操作, 清空QRTZ (例行任务插件)表中的响应内容, 调用delete db操作清出监控信息相关表中的内容(使得监控记录不至于一直在增长)

3.对于表中不存在pod相关信息记录, 则需要新增加的一个pod。调用post 创建监控任务接口(根据service 监控配置), 并调用get请求设置接口为监控enabled状态。

另外对于已经在物理机podmonitor中添加了监控的服务,提供了一个小脚本,用于导出物理机podmonitor域名级别监控到docker monitor监控中。

5.难点和重点问题解决

5.1 误报消减

5.1.1上线告警抑制

由于服务重启期间,会有removing状态和未ready状态的pod,在dockermonitor系统中存在记录,会引起误报。
我们的解决方法是提供一个通用脚本,根据k8s服务的存活检查时间,计算容器服务的发布更新时间,确定再自动开启服务监控的时机。实现在服务重启时间段,停止该服务的接口功能告警;存活检查时间过了之后,自动开启监控。
如下如所示,即Health Check中的Liveness Check检查时间。

在我们上线发布平台上衔接了该告警抑制功能。

5.1.2弹性扩缩容告警抑制

原来我们通过查询rancher的 API接口得到集群中全量信息,在我们服务越来越多之后, 查询一次全量信息需要的时间越来越长,基本需要5min左右。在这个过程中,存在docker-monitor和k8s集群中的信息不一致的情况。一开始试图通过按照业务分组,并行调用rancher接口得到业务k8s集群信息。时间从5min缩短到1min多钟。误报有一定的减少, 但从高峰期到低谷期时间段, 仍然会有若干pod在k8s集群中缩掉了, 但docker-monitor中仍有相应的告警。

在调研了一些方案之后,我们通过k8s增量事件(如pod增加、删除)的机制,拿到集群中最新的信息,pod的任何变更,3s钟之内就能拿到。

通过es的查询接口,使用 filebeat-system索引的日志, 把pod带有关键字Releasing address using workloadID (更及时),或kube-system索引的日志: Deleted pod: xx delete 。

通过这个方案,已经基本没有误报。

5.2策略优化

为了适配一些API允许一定的容错率,我们在apimonitor框架中增加了重试策略(单API和多API方式均增加该功能)

为了适配各类不同业务,允许设置自定义超时时间

5.3易用性

增加复制等功能,打开一个已有的告警配置,修改后点击复制, 则可创建一个新的告警项
使用场景: 在多套环境(预发、灰度和全量)监控,以及从一个相似API接口微调得到新API监控

5.4业务适配

精品课对服务的容器化部署中使用了接口映射机制,使用自定义的监听端口来映射源端口,将service的监听端口作为服务的入口port供外部访问,如下图所示。当service的监听端口收到请求时,会将请求报文分发到pod的源端口,因此对pod级别的监控,需要找到pod的源端口。

我们分析了rancher提供的服务API文件后发现,在端口的配置信息中,port.containerPort为服务的监听端口,port.sourcePort为pod的监听端口,port.name包含port.containerPo
-rt和port.sourcePort的信息,由此找到了pod的源端口与service监听端口的关键联系,从而实现了对精品课服务接入本平台的支持。

6.上线效果

1.容器服务API监控统一,形成一定的规范,帮助快速发现和定位问题。
通过该容器API监控系统,拦截的典型线上问题有:

· xx上线误操作
· 依赖服务xxxlib版本库问题
· dns server解析问题
· xxx服务OOM问题
· xxx服务堆内存分配不足问题
· xx线上压测问题
· 多个业务服务日志写满磁盘问题
· 各类功能不可用问题
·

2.同时增加了API延时趋势图标方便评估服务性能:

错误统计表方便排查问题:

结合我们k8s资源对象监控,和grafana的业务流量等指标监控,线上故障率显著减少,几个业务的容器服务0故障。

7.总结与展望

7.1总结

本期文章中我们介绍了基于静态API监控和K8s集群化管理方案,设计了实时的自动容器API监控系统。

通过上述方案,我们能够在业务迁移容器后,很快地从物理机监控迁移到容器监控。统一的监控系统,使得我们线上服务问题暴露更及时、故障率也明显减少

7.2展望

1.自动同步k8s服务健康检查到docker-monitor系统,保证每一个服务都有监控。

2.集成到容器监控大盘中,可以利用大盘中k8s资源目录树,更快查找指定服务,以及关联服务的grafana指标等监控。

3.自动恢复服务,比如在上线指定时间内,发生API监控告警,则自动回滚到上一版本,我们希望监控不仅能发现问题,还能解决问题。

监只是手段,控才是目标。

8.结语

Docker技术将部署过程代码化和持续集成,能保持跨环境的一致性,在应用开发运维的世界中具有极大的吸引力。

而k8s做了docker的集群化管理技术,它从诞生时就自带的平台属性,以及良好的架构设计,使得基于K8s容器可以构建起一整套可以解决上述问题的“云原生”技术体系,也降低了我们做持续集成测试、发布、监控、故障演练等统一规划和平台的难度。目前有道业务服务基本都上线到容器,后续我们将陆续迁移基础服务,实现整体的容器化。

我们也会不断积极拥抱开源,借鉴业界成功案例,寻找适合我们当前业务发展需要的理想选型。

-END-

查看原文

赞 8 收藏 6 评论 0

有道技术团队 发布了文章 · 3月3日

有道写作浏览器扩展实践

有道写作浏览器扩展作为一款为网页增加英文语法批改的辅助工具,允许用户在任意网页上绝大部分的富文本编辑器、多行文本输入框中编辑英文文本,可实时得到批改结果反馈,并自行接受建议自动修改,实现完美写作
来源/ 有道技术团队公众号
作者/ 李靖雯
编辑/ 刘振宇

一、背景介绍

有道写作服务是有道出品的写作智能批改产品,为用户提供优质的作文拼写、语法、样式方面的批改服务。有道写作不仅仅支持浏览器扩展形式,还支持在其他平台使用:例如有道词典 APP-作文批改、Web 在线端、Word 插件、PC 词典内。欢迎各位体验。

http://write.youdao.com/

浏览器插件在浏览器里面的称呼是 Browser Extension,也就是浏览器扩展,是一个扩展网页浏览器功能的插件。它主要基于 HTML、JavaScript、CSS 开发,同时由于是扩展特性,可以利用浏览器本身提供的底层 API 接口进行开发,可以给所用页面或者特定页面添加一些特殊功能而不影响原本页面逻辑。

每个支持扩展的浏览器有自己下载扩展的应用商店,可以直接在应用商店下载。有些产品自己提供浏览器扩展的 .crx 文件让用户下载并安装。

二、适配浏览器

有道写作在 Windows/Mac 系统都可安装,适配 Chrome、360安全浏览器、360极速浏览器、Edge 新版浏览器等,在以上浏览器商店中搜索有道写作,点击安装按钮即可。

三、功能介绍&效果展示

在介绍开发思路与实践之前,我们先来直观地看一下有道写作浏览器扩展的实际效果,并对其功能进行简单的介绍。

3.1 表现方式

视觉效果就是,给错误的文本字符下面画一条横线,在 hover 的时候,可以给文本增加一个高亮的效果。在选接受建议的时候,可以替换成我们想要的文本数据。

image

3.2 适用场景

>>> 在线邮件编辑:

163邮箱

Outlook 邮箱

Gmail

>>> 社交动态、评论:

Facebook

微博动态

评论

>>> 工具、笔记类:

有道翻译

Google 翻译

石墨文档

3.3 功能介绍

>>> 实时批改:

支持一边修改一边实时提供批改反馈,展示批改错误数量。

>>> 语法检测:

image

>>> 增强编辑框:

可以查看每一个错误反馈详细内容,并可分错误类型过滤查看结果。

>>> 接受建议:

点击接受建议时候替换正确文本。

image

四、开发思路

需求:扩展需要针对页面上的可输入文本的编辑框赋予批改的功能

4.1 适配编辑器

那么,网页中可输入文本的编辑框都有哪些呢?

通常我们常见可输入编辑框有:

  • 基于 Web 的表单可以输入文本控件:input、textarea
<input value="123"/>
<textarea>123456</textarea>
  • 可编辑属性的元素:contenteditable
<blockquote contenteditable="true">
    <p>Edit this content to add your own quote</p>
</blockquote> 

Input 元素通常是一行且输入范围较短的内容,考虑到批改交互的功能,我们的扩展针对以下可输入较多文本的编辑器进行兼容:

  • contenteditable 富文本编辑器
  • textarea
  • 其他文档编辑器

4.2 富文本编辑器

我们常见基于 contenteditable 实现的富文本编辑器有百度编辑器、draft.js、 有道云笔记(旧版)等等。

相比 textarea,富文本编辑器可以包含很多不同标签,可以以用来渲染成不同字体颜色的文本、图片、附件、视频、音频等等元素。

实现基于浏览器的富文本编辑器的四要素

四代编辑器的技术选型

  • 第一代编辑器主要是通过有限的 execCommand 指令对 html 文档进行操作。
  • 第二代则是在 execCommand 基础上,添加更多自定义指令甚至自己实现指令方式修改 html 文档。
  • 第三代是引入数据模型(json/xml),绑定自定义实现指令从而渲染html文档。
  • 第四代主要是直接抛弃整个 contenteditbale,单独制定选区和监听输入事件。

更多关于编辑器的介绍,可参考有道技术团队之前发布的文章:

为什么要介绍富文本编辑器内容呢,因为了解多这些编辑器实现方式和保存机制可以帮助后面实现并优化扩展的功能。

4.3 初想

一开始的想法是,将原始编辑器的纯文本内容提取并发送到服务端,然后根据服务端返回的数据进行重新的拼接,在错误节点位置使用特殊标记标签进行标注。

以有道写作 Web 端为例:

使用这种方法实现批改效果的还有 163 邮箱英文智能检查、Gmail 自带写信语法检测功能等。这种方法适合我们自定义的编辑器,可以自己控制文本的渲染和指令。

但由于浏览器扩展是基于别人写的编辑器上进行的辅助工具,不能随意修改其文本格式和样式。比如复制带有划线的文本进行粘贴,会出现冗余的划线(除非原本的编辑器有做粘贴文本的标签过滤),但是不能寄希望于别人写的编辑器都有这个功能。

4.4 实现

需要分别从两个部分进行考虑:

  1. 如何定位画线
  2. 如何接受建议替换正确文本

如何定位画线,并且可以给予其高亮的效果呢?

需要解决的问题是:需要在不影响原编辑器的格式以及功能前提下,将结果划线部分加入到界面上。

>>>contenteditabe:

  • 第一步:虚拟辅助器边框

虚拟一个元素(大小相同,位置相对)在原始编辑器之上,将结果划线标注在这个元素之上,我称之为辅助器。

因为是覆盖在原始编辑器上,需要禁止辅助器的鼠标响应行为,在 hover 的时候需要将鼠标位置同步到辅助器之上。

辅助器需要和原编辑器相同,才能定位准确,需要获得原编辑器以下属性。

  • 第二步:找准定位

问题:如果单纯提取元素的 innerHTML/InnerText/textContent 作为批改请求的参数,无法实现准确定位。

原因:服务端返回结果是根据纯文本定位,网页上的编辑器格式是HTML文档格式,包含不同字体不同格式的标签。

举个例子:html 中有两个块级元素,分别展示两句话,差异只在于两句话 font-size 不一样。

通过 element.textContent 提取出来的内容都是相同的,校验返回的错误标志结果也相同,如下:

因为无法从纯文本的角度得到两种情况差异,难点就在于:需要解析 html 格式,将服务端数据转换到实际格式位置中。

要知道,这相当于要在一个空白的白板元素里添加一个多个绝对定位的高亮元素。需要知道每个错误相对原编辑框的相对位移,和自身宽高。

下面是一个反向推敲的过程:

  1. 我需要得到的是 hightlightElement : { top, letf, width, height };
  2. 通过 range.getClientRects() 可以获得我们想要的数据。
  3. 于是需要知道如何获取一个错误节点对应的 range。

  1. 需要找到对应的开头节点和它的相对位移、以及结束节点和它的相对位移。range: { startNode, stratOffest, endNode, endOffest}。方法就是通过错误节点在纯文本的开始(fixedposStart)跟结束位置(fixedposEnd)通过遍历全文每一个文本节点(textNodes)的数据长度(textNodes.nodeValue)进行计算得出。
  2. fixedposStart、fixedposEnd根据服务端返回数据经过稍微计算可得出。全文每一个文本节点(textNodes)需要通过过滤某些标签得到。
  3. 所以需要先思考如何处理 html 中各种标签问题。

所以划线的原理是:提取其纯文本的 textnode 节点,根据结果 position 匹配开始的节点和位移、结束节点和位移,获取其文本片段 range 对应编辑器的 x,y,height,width,画出高亮区域。

具体步骤如下:

a. 根据原文所有 html 标签加工过滤,提取纯文本和加工后的文本节点集合:

html 内有各种标签节点,需要根据这些标签不同意义,对标签内的文本进行加工。比如针对 p 标签,通常是表示段落,需要将其包裹的内容后面添加一个换行符。

p 标签处理例子:
问题: 这个换行符是一起发给服务端的,服务端返回来的数据定位也算上了这个换行符。
解决方案: 过滤标签的同时记录文本处理过的位置,在后面的计算反向处理。同时还需要注意字符的转义问题,尤其注意零宽字符的处理。

b. 提取纯文本节点:

(上图文本内容根据标签内容分成5个纯文本节点)

c. 结合服务端数据计算每个错误全文定位:

比如 has 错误对应的错误节点信息。

d. 根据定位获取每个错误节点文本片段:

e. 通过文本片段获取相对视口的位置:

划线步骤图

  • 第三步:在assist范围内画出线和高亮

contenteditable 集合辅助器工作的流程图

>>> textarea:

textarea 本身是无法获取其 textnode 的,它相当于只有一个节点。考虑将其转换成文本节点:

  • 创建一个隐形 mirror,这个 mirror 具备与原始编辑器相同边框大小、可编辑区域。

  • textarea 任何文本变动同步到 mirror
this.textarea.addEventListener('input',this.mirror.update);
  • 再为这个 mirror 创建一个 assist,同理上面处理 contenteditbale 的流程相一致。

>>>关于突变:

编辑器其实就是一个普通的元素,以下编辑器的交互会引起我们页面内文本节点的变化:

  • 文本内容变化
  • 尺寸变化(窗口变大变小)
  • 位置变化
  • 字体大小变化(加粗,居中)
  • 滚动

这些变化也就影响我们定位的变化,称之为突变。需要处理每一个突变引起的重新定位问题(重点难点)。

同时,需要监听原始编辑器的输入、字体变化、编辑器尺寸变化等等触发 assist 的重新定位方法。

// 通过ResizeObserver监听编辑器尺寸变化 objResizeObserver = new ResizeObserver((entries) => {
    var entry = entries[0];
    this.elementResizeHandler(entry.target)
}); 

ResizeObserver 兼容性问题需要通过 polyfill 库文件解决。

重新定位方法(mutation):

  • 通过新旧 textnode array 比对,正向遍历节点集合和反向遍历节点集合,得到被修改的 textnode 是哪一个段文本节点 textnode 集合。
  • 只需要处理被影响的 textnode 所对应的错误节点集合根据移动的 offest 计算后面影响的节点位移。

  • 其他错误相对自己 textnode 的位移是不会改变的。

如何接受一个建议,替换文本:

替换文本意味要修改原编辑器的数据甚至格式,就会造成刚才说的对部分编辑器会引起格式错乱和保存失败的情况。

难点:不影响原始数据存储格式,不影响原始编辑器撤回操作,同时还能触发原编辑器保存机制。

解决方法:不直接用脚本修改 dom 节点,模拟用户修改数据的方式:选中文字,替换内容。

以新版有道云笔记为例子:

  1. 通过之前复杂计算得到结果片段,根据结果片段计算出对于可视窗口的位置,得到 {top, left, height, width}。
  2. 模拟鼠标从左向右滑动的操作事件加在内容区域。

  1. 找到自定义的自绘区域。
  2. 一个错误结果中可能涉及不同的样式,我们仅获取当前节点第一个片段的字体样式,模拟一个粘贴事件。

  1. 在自绘区域触发自定义粘贴事件。

4.5 增强编辑框

入口在点击右下角按钮或者 hover 出现结果卡片时候,点击详细建议进入

>>>增强编辑框的作用:

  • 提供更大的编辑空间
  • 查看详细的批改结果

增强编辑框是一个特殊的 contenteditable 编辑器。

>>> 初始化、关闭赋值:

在初始化增强编辑器的时候,直接获取原编辑器数据,这里忽略了原编辑器的一些样式、图片,只使用 html 数据部分。

在增强编辑器中编辑后返回原编辑器时候,需要将新数据返回赋值。

>>> 通信:

增强编辑框是嵌入页面的 iframe,只在顶层页面出现。与原来页面的通信是通过postMessage 方式。

(注意:postMessage 不能传递 html 元素和过于复杂的 json object)

如果是原本编辑器是 iframe,需要找到最上层 window.top,利用 window.top 和增强编辑框进行通信。

五、整体流程

上图为有道写作浏览器扩展从注入到浏览器页面,以及运行的大致流程。

为了在不影响用户操作前提下,扩展脚本只会在当前页面空闲时候加载,并且批改功能只在部分被用户点击 focus 的编辑器中激活。

以上是开发有道写作浏览器扩展过程中的开发思路和部分技术实现细节,借此机会分享给大家,欢迎与有道技术团队一起探讨更多关于前端、浏览器扩展的知识问题。

查看原文

赞 17 收藏 9 评论 2

有道技术团队 发布了文章 · 2月26日

校招 | 有道2021春招正式启动,技术研发、人工智能等近50个岗位等你来投!

网易有道 2021 春季校园招聘

正式启动!

人工智能、技术研发等五大类

近50个岗位等你来投!

image

欢迎加入有道技术团队!

查看原文

赞 0 收藏 0 评论 0

有道技术团队 发布了文章 · 1月27日

有道云笔记新版编辑器架构设计(下)

image

上期文章,我们从整体上介绍了富文本编辑器的背景,并分享了有道云笔记新版编辑器技术选型中的模型渲染部分。
本期文章,我们将继续分享技术选型中的编辑指令部分内容,并详细解读有道云笔记编辑器的分层架构设计。

作者/ 金鑫
编辑/ 刘振宇

二、云笔记新版编辑器技术选型

2.3 编辑

由于 contentEditable 会产生不受控事件,导致很多 bug,例如,一开始数据是 abc,对应渲染出的视图是一个 span,内容是 abc。由于需要提供可编辑,span abc 是一个 contentEditable 的元素。

正常情况下,当编辑 span abc 时,例如输入了 d,我们拦截 keyup 事件,在处理函数中将事件 preventDefault,这一步是不让 contentEditable 元素自己修改 span abc为abcd,然后我们在处理函数里调用自定义的 insertText 指令,修改数据 abc 变为 abcd,再用新的数据进行渲染,修改 span abc 为 span abcd。

但是,一旦出现 span abc 上的事件没有被拦截或拦截了但没有正常处理,就会出现 bug。

例如我们旧版的编辑器就没有拦截 ctrl + delete 的事件,如果在 abc 这一行按 ctrl + delete 就没有对应的事件处理函数修改数据模型,数据模型还是 abc,但是由于 span abc 是 contentEditable 的,ctrl + delete 事件会直接修改 span abc 将 abc 整行删除,这样数据模型和视图上就出现了不一致。后续如果再输入 d,则会将数据模型修改成 abcd,这时候视图会根据新数据渲染为 span abcd,表现为已经删除的 abc 再次出现,对用户的使用造成困扰。

针对 contentEditable 的问题,我们决定将完全抛弃它,由此带来两个问题:

  • 没有可编辑的元素,不会触发输入事件
  • 没有可编辑的元素,无法使用浏览器自带的光标

触发输入事件:

我们采用了在用户光标位置后画一个隐藏的 Input 组件,Input 组件中有一个 textarea 来接受用户的输入,触发输入相关的事件,如下图所示:

自绘光标/选区:

由于不能使用浏览器默认的光标,我们只能自绘光标。

我们参考浏览器的 Selection 的结构,设计了类似的Selection模型,并用Selection组件渲染 Selection 模型,在屏幕上用绝对定位画出用户的光标,同时用户拖蓝时产生的选区,也可以用这种办法绘制,由于光标和选区本质上一样,我们就放弃了浏览器的选区绘制,也改为自己绘制选区。具体流程如下:

我们用 anchor 表示用户开始托选的位置,focus 表示用户结束托选的位置。anchor 和focus 都包含了 nodeId 和 offset 两个属性,nodeId 表示位置所在的文本节点,offset 表示位置相对于文本节点开始的偏移量,以字符为单位。

当用户点击鼠标开始拖选时,找到鼠标所在的 dom 节点对应模型上的文本节点,我们拿到 id 存储在 anchor 的 nodeId 中,再计算鼠标在 dom 节点上的位置,转换为数据模型上相对文本节点开始处的偏移量,存储在 anchor 的 offset 中。

当用户移动鼠标或者抬起鼠标时,我们用类似的办法更新 focus 数据,将 anchor 和 focus 数据组合成为一个区域(Range)放入 Selection 模型中。这样我们就可以根据用户的点击/拖蓝操作构造出用户的光标/选区对应的Selection模型了。

然后,我们开发 Selection 组件渲染 Selection 模型。当 anchor 和 focus 在同一个位置时,Selection 组件将 Selection 模型渲染为一个闪烁的短线,表示用户的光标,当 anchor 和 focus 不在同一个位置时,Selection 组件将 Selection 模型渲染为一个从 anchor 位置到 focus 位置的一个或者多个矩形区域,表示用户的选区。

总结这一节,我们用 Input 组件触发用户输入事件,构造 Selection 模型和 Selection 组件用于自己绘制光标和选区,最终我们的模型层和视图层如下图所示:

2.4 指令

新版编辑器实现了丰富的自定义的富文本编辑指令,自己实现了 execCommand 方法来执行指令。

下面以输入文字的指令作为例子说明指令是如何生成的。

输入文字:

输入文字的指令名称是 'insertText',它需要传入以下四个参数:

  • nodeId: 插入到的文本节点的 id
  • offset: 插入的位置相对文本节点起始位置的偏移量
  • text: 插入的文本内容
  • marks: 插入文本的行内样式

在下面的例子中,我们想在 'This is a text' 的位置10处插入一个红色的 'rich' 变为 'This is arichtext',需要生成的指令如图所示:

生成指令上述的指令,需要我们将光标定位到 ‘This is a ’ 之后,然后点击工具栏的颜色按钮设置颜色为红色,再输入字符串 'rich ',根据用户操作生成指令的过程如下图所示:

在用户点击将光标定位到 ‘This is a ’ 之后时,我们更新了 Selection 模型,它的 anchor 和 focus 中的 nodeId 变为当前文本的 id,而 offset 变为 10。

然后,用户点击了红色按钮,这时候我们在Selection模型上记录用户当前设置的行内样式,将在下一次输入时生效(如果点击其他地方,这个行内样式将会重置为新光标前面一个字符的行内样式)。

最后,在用户按下按键输入文字时,我们拦截用户的keydown事件,从event.data中拿到需要插入的文本r,再根据当前的Selection模型,拿到anchor节点的nodeId和offset,以及存储在Selection模型上的行内样式,根据这几个参数就可以生成insertText指令了。

指令的组合:

指令直接会有一些公用的逻辑,为了指令逻辑的复用,我们将一些公用逻辑也封装成指令。简单的指令(Operation)可以组合成复杂的命令(Command)。例如选中一块区域并输入文字,理想的表现是删除区域内的所有文字,再插入输入的文字,如下图所示:

这个复杂的命令我们将它定义为 insertTextAtRange,它实际上是由三步组成:

第一步,先删除选区中的所有文字,这里我们用 deleteByRange 命令实现。而要删除选区中所有的内容,因为选区跨了三个段落,我们需要首先将第一个段落中的 ‘world’ 删除,用到了 deleteText 指令;然后将第二个段落节点 ‘hello javascript’ 整个删除,这里用的是 deleteNode 指令;最后我们还需要将最后一段中的 hello 删除,这里用的也是 deleteText 指令。所以一个 deleteByRange 命令又由多个 deleteText 和 deleteNode 指令组成。

第二步,删除完选区所有文字之后,我们需要插入 editor 到第一段的 hello 之后,用到了上面提到的 insertText 指令。

第三步,我们发现 hello editor 和2020都是文本段落,按照需求我们要将他们合并到一起变为'hello editor 2020',就用到了mergeNode 的指令。

由此可见一个复杂操作对于的命令是有多个指令和命令共同组成的,这种方式能充分解耦和复用的指令,让每个指令只关注于实现一类对数据模型的修改。

撤销重做:

将对数据模型的修改抽象成指令之后,撤销重做就变得比较好实现。

我们规定指令都是成对出现的,每个指令都有对应的逆指令,例如 insertText 的指令它的逆指令是 deleteText,文档模型 Document 在 insertText 指令的修改下变为了Document',那么根据 insertText 指令构造出的逆指令 deleteText 就可以修改Document‘ 让它恢复成 Document,这就是实现撤销重做的基础。

对于复杂的命令,我们会在他执行的时候收集执行的所有简单指令。在撤销时,根据指令的执行顺序,反向的执行所有收集到的指令的逆指令。在重做时,则只需要正向的执行所有收集到的指令。

2.5 小结

本章从模型、渲染、编辑、指令四个角度中的前两个说明了新编辑器的技术选型。

总结起来,新编辑器采用典型的 MVC 模式,结合了 React 等前端框架的数据驱动的思想,通过修改数据模型来解决更新视图,由于放弃了 execCommand 和 contentEditable 这两个浏览器的 API,所以自己实现了指令系统、事件拦截和光标绘制。

整个富文本编辑的模块如下图所示:

但是由于富文本编辑器除了需要支持富文本的编辑功能,还需要支持图片、附件、表格、代码块等其他复杂功能,在上述框架内如何扩展支持这些功能,如何实现功能的解耦和可配置,这就是下一节我们讨论的问题。

三、新编辑器的分层架构

首先我们用图片功能为例,说明如何在现有框架下实现。

3.1 实现图片功能

我们先只考虑占据一行的图片,这类图片可以单独当做一个段落,所以是可以放入我们的三层文档模型的第二层,如下图所示:

对应的我们需要开发 Image 组件渲染图片,它与 Paragraph 组件一样,也是 Document 组件的子组件,如下图所示:

点击工具栏按钮,我们需要在文档光标处插入对应的图片,这就需要我们生成 insertImage 命令,用它修改文档模型,生成 insertImage 命令的过程如下:

由上述添加图片功能的做法可以看出,新添加一个功能,我们需要设计实现对应的模型、组件和命令,每个功能都涉及到这三处功能的修改,随着功能越来越多,不同功能之间的代码会互相耦合。

并且,在不同应用场景下,需要不同的功能,例如编辑器 A 只需要图片附件和表格的功能,编辑器 B 需要图片、代办、列表的功能,这种编辑器定制是比较难实现的,之前只能通过屏蔽入口实现,js 包里有很多无用代码。

如何解决这些问题呢?

3.2 分层架构

为了解决编辑器核心功能和业务功能的解耦,我们将云笔记新版编辑器的架构分为了核心层业务层:

  • 核心层 只负责提供富文本的编辑能力,以及多种拓展机制。
  • 业务层 负责实现各种各样的扩展功能,例如图片、附件、表格、代办、列表等。

核心层:

核心层的主要能力是通过第二节的 MVC 框架提供富文本编辑能力。它暴露了以下接口:

  • 首先,是 Editor 组件,Editor 组件提供包含富文本编辑能力的编辑器组件,业务层只需要将它作为一个 React 组件进行调用就可以了。
  • 其次,是 editor 全局对象,它上面挂在了执行编辑命令、撤销、重做等富文本编辑的核心接口。
  • 最后,核心层还提供了丰富的扩展机制,用于业务层对编辑器能力进行扩展。常见的扩展机制包含数据模型扩展、组件扩展、插件扩展、自定义命令等。

业务层:

在核心层提供的富文本编辑器的基础上实现云笔记编辑器的众多复杂的业务功能。大致包含以下几个需要开发模块:

  • 首先,需要将核心层提供的 Editor 组件与业务层开发的工具栏、右键菜单等组件组合成为一个功能完整编辑器。
  • 其次,需要利用核心层的扩展机制开发相互解耦的编辑器特性,扩展编辑器的功能,例如图片、附件、表格、代办、列表等等。
  • 最后,还需要开发与云笔记编辑器与不同平台宿主 WebView 交互的接口层 editorAPI,实现云笔记编辑器的跨平台特性。

所以用如下图这样的分层结构,我们就可以解决编辑器功能耦合和定制化的问题。

编辑器和核心富文本编辑功能和扩展功能之间以及不同的扩展功能之间都是单独开发的,耦合的可能性大大降低。同时针对不同的编辑器定制需求,可以组合不同的编辑器特性进行打包,这样就可以实现按需打包出定制版的编辑器。

3.3 扩展机制

用核心层提供的扩展机制,我们重新实现图片的功能。首先,我们将三层文档模型的第二层由段落泛化为块(Block),块上提供 name 字段表示块的类型,默认类型为表示段落的 paragraph,针对图片类型,name 可以标志位 image。

图片模型中我们需要记录图片的地址,我们在块的模型中添加 data 字段用于存储不同类型块的自定数据,对于图片就可以在 data 中存储 url 字段。

其次,我们在渲染时,针对块用 Block 组件进行渲染。同时在 editor 暴露。registerComponent 接口,针对不同 name 的块,将对应的渲染组件注册进编辑器。Block 组件就可以在渲染数据时,根据 name 选择对应的注册组件进行渲染。例如,针对 name 是 paragraph 的段落数据用 Paragraph 组件进行渲染,针对 name 是 image 的图片组件,则用 Image 组件进行渲染。

最后,我们需要实现 insertImage 的自定义命令,通过 editor 的 registerCommand 注册命令,就可以在点击工具栏插入图片时调用 insertImage 的命令修改数据模型。在实现 insertImage 自定义命令的过程中,我们可能会用到 editor 上保留的编辑器内置命令和指令。

做完这三步,我们就利用编辑器的扩展机制实现了图片的功能。可以看出这样实现的图片功能,有扩展性强、耦合低、可插拔等优点。

四、总结

综上所述,云笔记新版编辑器采用了核心层和业务层的两层架构,如下图所示:

在核心层,舍弃了存在较多问题的 contentEditable 和 execCommand 接口,自定义了数据格式,攻克了光标绘制、事件拦截、命令系统等技术难题,实现了富文本编辑的核心功能。同时还暴露了丰富的扩展机制。

在业务层,通过核心层暴露的扩展机制,我们可以开发各种不同编辑器特性,通过注册机制将它们注册回编辑器丰富编辑器的功能。

在开发有道云笔记的新版编辑器的过程中,我们遇到很多实际问题,愈发感觉到这是一个非常有深度的前端技术领域,所以我们将新版编辑器的技术选型、架构和部分实现细节拿出来分享给大家,希望对大家开发富文本编辑器、做复杂系统的架构设计有一定参考意义。

- END -

查看原文

赞 18 收藏 5 评论 1

有道技术团队 关注了用户 · 1月21日

卤代烃 @skychx

公众号:卤蛋实验室

个人网站:supercodepower.com

关注 226

认证与成就

  • 获得 109 次点赞
  • 获得 0 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 0 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2020-12-09
个人主页被 4.8k 人浏览