04月09日
CODING 技术小馆 | WebIDE 前端札记

本文为 CODING 资深前端工程师 杨臻 在 CODING 技术小馆·上海站 的演讲内容整理

大家好,今天和大家分享的是“那些年我们一起踩过的坑——WebIDE 前端札记”。IDE 项目刚开始是 14 年的 V1 版 backbone + CoffeeScript + underscore + bower + npm + grunt (ace, sh.js),最早的 backbone 库不知道有没有人知道,感觉几年之后之前的技术再也没有人提起,它的风头完全被 React 取代了。WebIDE 后面的版本也都是以 React 为主。

所以说,前端最大的坑是什么?就是永远赶不上时代,技术永远在变。我们开头两年,每年都需要把 IDE 项目重新写一次,以至于 V2 版改版的时候,第一次一年就可以写完,第二次一年写不完,V2 版已经下线,很多用户发现我们新版好像没有 V2 好。我们的人和精力都有限,这就是最大的坑。

WebIDE 前端历程

图片

第一版用了 CoffeeScript,因为写起来简便,有各种优势,所以当 V2 改版时,我们开始用 React 的时候就想继续使用 CoffeeScript,工具库换到 lodash,框架用的是 flux,打包用 Webpack。当时有一个坑,大家知道 React 如果用 CoffeeScrip 写出来没有办法看,这不是 React 提倡的方式,所以到第三版时我们直接换掉了 CoffeeScript。

到 15 年下半年的时候,我们有一个在线看代码的项目,当时想在这个版本上做一些技术探索,编辑器换成了 CodeMirror,框架转为 Redux,加了一个 immutable.js 做搭配,因为 Ace 编辑器 是 c9 开源出来的,而 c9 是我们的竞品,用他们的开源产品似乎不太好,后面我们会稍微做一下比较。在这个项目里面用 CodeMirror ,在某些方面比 Ace 要好。在去年上半年的时候,用户打开我们 WebIDE 的时候可以发现,在右上角可以切换到新版,这个新版其实就是 V3 版,我们从这个版本开始把编辑器切换到了 CodeMirror,在 CodeMirror 上做了很多二次开发,花费的代价其实也很大。

图片

图片

这是我们 V2 版的框架,这个组合现在可能过时了,谈论的也不多了。V3 版也经过了一轮变动,一开始用的是 Redux,用起来不顺手,因为完全按照它的规范来走的话会发现,其实用起来很不方便,Redux 只维护一个 state,整个项目完全基于这一个 state 来展现,它用纯函数替代 dispatcher 来修改 state,它的理念来自于函数式编程。我们在经过一段时间的尝试之后,从去年开始又进行了替换,把 Redux 换到 MobX,现在这两个在项目里面是并存的。其实 Redux 和 MobX 可以并存,下面我会对这两个库做一下比较。

关于状态管理

图片

首先这两个库都是用来做状态管理的,不知道大家有没有思考过,状态管理到底解决什么问题?最开始学 React 的时候,看官网上的例子,其实并不需要 Redux 和 MobX。那么我们为什么需要一个状态管理呢?最主要的原因就是一个项目里面有不同的组件,不同的组件会互相影响,互相调用,某个组件上做的操作,反应结果是另外一个组件发生变化。状态管理就是怎样更好的管理组件之间的通讯。到一定程度时,推算应用的状态将会变得越来越困难。它就会变成一个有很多状态对象并且在组件层级上互相修改状态的混乱应用。在大部分情况下,状态对象和状态的修改并没有必要绑定在一些组件上。

所以,解决方案是引入状态管理库,比如:MobX 或 Redux。它提供工具在某个地方保存状态、修改状态和更新状态。你可以从一个地方获得状态,一个地方修改它,一个地方得到它的更新。它遵循单一数据源的原则。这让我们更容易推断状态的值和状态的修改,因为它们与我们的组件是解耦的。反过来说,如果某些状态根本就不会被别的组件用到,那么用本地状态也就足够了,简单方便,其实我们的项目里就是。比如一些表单,一堆的 onChange 事件,如果用 Redux 那就是一堆的 reduce,其实只有最后确认的结果是用的,那么完成时传最终结果就好。

像 Redux 和 MobX 这类状态管理库一般都有附带的工具,例如在 React 中使用的有 React-Redux 和 MobX-React,它们使你的组件能够获得状态。一般情况下,这些组件被叫做容器组件(container components),或者说的更加确切的话,就是连接组件(connected components)。只要你将组件升级成连接组件,你就可以在组件层级的任何地方得到和更改状态。

另外它们并不一定要跟 React 绑定在一起,它们也可以在 AngularJs 和 VueJs 这些其他库里使用。但它们与 React 的理念结合得非常好。如果你选择了其中一个状态管理方案,你不会感到被它锁定了。因为你可以在任何时候切换到另一个解决方案。你可以从 MobX 换成 Redux 或从 Redux 换成 MobX。

# Redux 与 MobX 的比较

图片

如果你用 Redux,state 的格式是像数据库一样标准化的。实体之间只靠 ID 互相引用,比如说两个对象,中间要包含一个子对象,你只写它的 ID,用这种关联,这是最佳实践。这种关联很像数据库的指向,看着好像很好,它的理念是假设有很多对象都包含一个 author,很多东西都引用它,只要改单个就行。概念没错,但实际操作起来真的比较难读。

MobX 则是受到面向对象编程和响应式编程的影响。它将 state 包装成可观察的对象,因此你的 state 就有了 Observable 的所有能力。state 数据可以只有普通的 setter 和 getter,但 observable 让我们能在数据改变的时候得到更新的值。我个人比较推荐 MobX,可能有些朋友觉得是倒退,用起来跟很多年前的框架差不多,但是真的用起来非常方便。

总而言之这是一个习惯问题,因为大家也知道,编程的大趋势是从面向过程到面向对象,然后大家觉得下一个就是所谓的函数式编程,Redux 走的就是函数式编程这套理念。

编辑器

接下来介绍我们采用的编辑器。我们 IDE 的编辑器一开始用的是 ace,我们在上面做了很多的定制,实现了代码比较的 diff view,merge view,Java 的代码提示等。CodeMirror 上自带了 diff view,merge view,在 CodeMirror 上我们又重新把 Java 的代码提示写了一遍。之前有用户向我们建议使用 monaco 的编辑器,但是更换编辑器会有很多细节需要处理,如果接下来有精力我们可能会换到 monaco。换编辑器也需要很大的工作量,这是一个很头疼的问题。

我们的 V3 版新加入了协同编辑的功能,用户可以邀请其他 CODING 用户作为协作者,邀请进来后会显示在线状态,打开同一个文件可以显示其他的用户正在干什么,正在写哪一段代码;我们还内置了一个简单的聊天工具。整个协同功能其实很强大,但是从数据上来说使用的人不多。

协同编辑

图片

我们整个协同是基于 OT (Operational Transformation)协议,讲到这个协议,大家有没有听说过 Google 10 年前有一个叫 Wave 的项目,那个项目的理念当时非常的惊艳,也给业界提供了很多想法和创意。

图片

Google wave 项目野心很大,是综合了邮件、聊天工具、文档的存在,在界面上比较像一个邮件界面,只是可以同步协作,加到协作列表里面的用户可以评论,现在很多软件,比如 slack上面的功能,Wave 里面早就已经有了。当年这个项目只运营一年就被谷歌干掉,可能是理念太超前,超前到用户接受不了。那个项目给大家留下很多的东西,比如说这一套 OT 协议就是他们定出来的,我们也是参照这个协议。这套协议本身有很多操作,这里远远没有列全,可以想象有多复杂。它定义了一系列的 operation,要实现文本的协同编辑,其实我们只实现了开头两个。

图片

图片

图片

基本原理是你要实现一个方法,把用户的操作传进去,得到两个互补的操作,等于像打补丁一样的。这个可能说的有点抽象,比如说一个用户在第二个位置插入一个 A 字符,另一个用户也在第二个位置插入一个 B 字符,假设 A 字符先传到服务器,那么通过计算,第一个用户得到的补丁操作应该是在第三个位置插入 B 字符,而第二个用户得到的是在第二个位置插入 A,最后使得两个人得到的结果可以达到同步的,这是比较简单的操作,通过组合可以实现很复杂的协同。OT 部分简单讲到这儿,想要更详细了解的同学可以搜一下文档。

终端模拟器

图片

再讲一下终端模拟器,最开始调研的时候 xterm 并不完善,同样都需要做定制,于是我们选择了 sh.js。当时上线最紧急的问题是不支持中文输入,中文字符显示有问题,显示以字符数来计算,一个中文算一个字符,但是它占两个位置,就会导致每一行到最后的位置不对,返回的内容也会换掉。因此我们对 sh.js 做了改进,实现了宽字符显示支持,处理换行,包括解决了对齐的问题。

之后用户又提出文字对齐还是有问题,当时我们以为是文字符算错了,其实不是的,道理很简单,就是字体问题,一个中文其实不是两个字符那么宽。当时我们用的 Ace 编辑器,处理中文就很好,我们参考了它的解决方案。方案其实很简单,每个中文套一个标签,算出来两个英文字符的宽度,标签设到那个宽度;我们打开终端的时候先什么也不做,输入 20 个大写的 X,然后除一下,得到字符宽度,给每个中文套一个标签。改完以后终于对齐没有问题了。

但用户还是不太满意,反馈说输中文的时候输入法位置不对,总是在最左上,因为 sh.js 本身隐藏了一个输入框在最上角,解决方法也很简单:让这个输入框的位置永远跟随光标位置,每次光标位置移动,就把输入框的位置移到光标位置。

后来又有用户反馈说复制粘贴不行,因为大家也知道,浏览器上的安全限定比较严格,早期的时候都是用 flash 来解决剪贴板的问题。我们当时用了一个取巧的方法,就是鼠标只要在输入框上面,右键可以出来复制粘贴的,我们让输入框占了整行,因此不管在哪点右键,总能弹出复制粘贴。在我们对 sh.js 做了大量的改进之后,忽然我们又发现不知何时,xterm 又冒了出来。

图片

之前说到调研的时候 xterm 并不完善,但是现在 xterm 搭上 vscode 的顺风车,项目一直在更新维护,并且功能更强大。新版已经是用 canvas 代替 dom 进行绘制。大家都知道用 canvas 绘制效率会高很多,页面会更流畅,更厉害的是 xterm 对中文支持也做的很好,已经没有宽字符和输入法的问题。

所以业界一直有个说法,遇到一个技术难题的时候有两个解决方案,一个是花很多精力和时间把它搞定;另一个是什么都不做,过一段时间发现这个问题自然解决了。我们现在也已经没有太多的精力去维护 sh.js 这个项目,所以最近我们在线上改用了 xterm,虽然也有一些小问题,比如上线以后发现在 FF 下复制粘贴有些问题,我们也用一些取巧的方法进行了修复。

再来说说 canvas 的效率问题。DOM 绘制有很多的劣势,比如速度很慢。浏览器打开网页时,需要解析文档,在内存中生成 DOM 结构,每个 DOM 本身又有很多属性和方法,所以这个过程是很慢的。DOM 还会拖慢 JavaScript,所有的 DOM 操作都是同步的,会堵塞浏览器。JavaScript 操作 DOM 时,必须等前一个操作结束,才能执行后一个操作。只要一个操作有卡顿,整个网页就会短暂失去响应。浏览器重绘网页的频率是 60 FPS(即 16 毫秒/帧),JavaScript 做不到在 16 毫秒内完成 DOM 操作,因此产生了跳帧。用户体验上的不流畅、不连贯就源于此。

网页是单线程的。现在的浏览器对于每个网页,只用一个线程处理。所有工作都在这一个线程上完成,包括布局、渲染、JavaScript 执行、图像解码等等,怎么可能不慢?网页没有硬件加速,都是由 CPU 处理,没用 GPU 进行图形加速。但是 canvas 有,flipboard.com 大概在15年的时候就尝试过,用手机打开 flipboard.com 就是全用 canvas 绘制的页面,体验和原生的没有区别。但是 canvas 也有明显的劣势,因为 canvas 不是自适应的(responsive),文字在哪里断行都要自己计算,而且用户也无法选中文本,实现 UI 要把所有页面元素都实现一遍 (超链,组件,css 效果等)。另外,怎么让搜索引擎检索网页的问题解决起来也不是很容易,所以当时也引起了很大的争议。

关于国际化

说到国际化,不知道什么时候开始前端项目国际化功能是一个标配,我们的 IDE 很费劲的做了一个国际化功能,但是回过头来问,有多少用户知道国际化功能,到底用户会用国际化功能吗?

我们 v2 的时候用的是 yahoo 的 React-intl,其实挺好用,不过比较复杂,很多功能也用不上。到了 v3 的时候精力旺盛的小伙伴觉得那个太重型,想想自己实现一个也不难,就自制了一个国际化的组件,实现了很多有趣的小功能,比如自动生成语言文件,在每一个文本上加了 id 方便后续做自动化测试等等,因为是我们自己做的,上面也可以加一些我们需要的功能。但我这里还是不推荐,如果要用国际化,造轮子这种事,我个人觉得还是少做比较好。

插件体系

图片

v3 版我们实现了一套插件体系,v3 版本身是开源的,开源版单机跑没有问题,因为我们开源项目的后台另外写了一个单机版的后台,只是开源版不像线上版那样有更多的功能。为什么会有功能上的差别?比如说登陆流程,以及界面右边的各种工具都是通过插件的方式加载的,不加载就没有这些功能。插件实现起来不是特别复杂,简单讲一下怎么实现插件:在需要插的位置,我们在这儿插一个 plugin 的组件,它自己有一个ID,说明我是右侧,一开始加载组件的时候,每个组件会声明要加在右侧还是其他地方。这个右侧的 plugin 插件组件会把要显示在右侧的组件全部显示出来。

以上就是我本次的分享内容,谢谢大家。

扫码关注 扣钉CODING 微信号,获得 CODING 技术小馆和产品更新 第一手信息。
图片