Coding WebIDE 前端架构变迁
发布于 1 个月前 作者 zengliqi 254 次浏览 来自 分享

本文为 2016 年 9 月 28 日,Coding WebIDE 项目负责人杜万在『前端之巅』微信群在线分享活动总结整理而成。

发展历程和现状

2014 年 10 月,我结束了一份 7 年的工作,加入了 Coding,当时的 Coding 只有 20 几个人。作为上海分公司第一个员工,我一边开始为 WebIDE 指定产品计划和架构选型,一边开始张罗办公地点,公司地点和团队招募的事情。

当时 docker 虽然很新,但是已经开始广受关注。第一感觉是一种更轻量级的虚拟机,抱着对新技术充满好奇的心态,快速的阅读了一些 docker 资料以后,在架构选型会议上成功的说服了大家。

另外一个影响深远的决定是后端服务的 daemon 采用嵌入到 container 内部的方式,还是外置共享的方式。当时考虑嵌入的方式难以升级,也容易和用户自己的程序产生干扰,另外也担心早期版本的 java 程序被反编译。但外置共享的方式,无疑要更了解 docker,需要在自己的程序里调度 container。一番取舍之后,最终选择了外置。就目前来看,我觉得这个决定不一定是最好的,但肯定是可行的,至少没有因为死在路上。

接下来的 2 个月,我们制定了一个以 2 周为一个迭代周期, 4 个周期的研发计划。第一版本的产品计划里涵盖了:代码编辑器、Docker环境、布局框架、文件树和Web 终端模拟器五个部分作为一个初始的 IDE 原型。

Coding 2014 年年度会议上,我们内部向同事展示了一个满是 Bug,设计粗糙的 IDE 原型。记得当时在去深圳的飞机上我仍然在解决用 Gruntjs 编译 ace 的问题。

2015 年愚人节那天,WebIDE 正式发布。记得分享开发环境的需求是发布日前两周,老板想出来的好主意。我们为此打乱了原定的计划,发布前的一周团队 5 个人都玩命加班。发布前夜我们一起奋战,直到见到了早上的太阳才勉强搞定。次日,就有一个同事提出辞职,理由很简单,太累。

WebIDE 第一版的前端是我用 Backbonejs 写的,水平有限和时间紧迫,所以并不满意。上线以后新来的前端大牛提议要 Reactjs 重写,我马上支持道,”好,你来写,其他人做新需求“,没想到一个月后他果然写出来了,这人是刘辉,后来他去“深JS”大会分享了这次重写的心得。

2015 年 8 月,WebIDE 有了自己的独立入口——https://ide.coding.net,于此同时我们开发了 dashboard 界面,提供更多的 workspace 管理功能。

2016 年 3 月,WebIDE 成为了 Coding 内部第一个支持国际化,有英文版本的产品。5 月份,我们推出了企业版,相比于在线的平台版,企业版主要是针对于需要做私有部署的企业客户。

2016 年 9 月 12 日,WebIDE 搞了一个程序员节解谜活动,我们开发了一个很 Geek 的活动页面——https://ide.coding.net/256 , 在 WebIDE 的 web terminal 里用 Elixir 写了一个命令行的解谜游戏。游戏的奖励是获得排名和隐藏在游戏里我们提前放出的 WebIDE 源码。次日,就正式宣布开源了。

上面和大家回顾了 2 两年来 WebIDE 研发过程的一些关键时刻和轶事。接下来和大家分享一下 WebIDE 前端的发展情况。

前端两度重写

创业的路上充满了新奇的想法和难料的意外,所以研发计划变得太快,工作节奏也随之紊乱。代码的字里行间也势必会受到影响,急着写业务,来不及重构的事情相信大家都有遇到。WebIDE 也是一个做得很急的项目,从接到任务到上线也就花了 6 个月。

第一版 Coffeescript / BackboneJS / Grunt

最初做前端选型的时候,先选定了 Coffeescript,它从 Ruby 和 Python 那里吸收了很多语法糖,用起来很带劲。一门好的语言能提升开发者的表达能力。

框架方面当时很想选 Angular 1.x,因为当时 coding.net 使用 Angular 1.x 开发的。很费劲的把《Angular 权威指南》啃完以后放弃了,选用了门槛比较低的 BackboneJS + jQuery.

从 BackboneJS 的角度去看 Angular 1.x,还是蛮震撼的,声明式的编程、双向数据绑定,路由、模块化,依赖注入,还有单元测试。但是感觉驾驭不了,做复杂 DOM 修改不容易。Angular 成也绑定,败也绑定。绑定语法简单,简单到甚至可以不写 JS,但是绑多了页面刷新太慢。感觉 Angular 其实更适合于对性能要求不高的企业管理后台开发场景。为了一套框架还得掌握一套自律的开发规范和调优技术未必值得啊。最挫败的是当时我想搞一个递归的文件树,想了半天不知道如何做才合适。

第一版的编译工具选的是 Grunt,那时 Gulp 也开始流行了,因为 Grunt 在之前项目用熟悉了,也就先用着了。那年 bower 也还在维护,不过也确实觉得 bower 和 npm 搞两套有点多余。

第二版 Coffeescript / React & Flux / Webpack & Gulp

第一版写着写着,慢慢的不爽了,计划重构一次,老板很不理解。好吧,那就不重构了,我们重写一个吧!

刘辉提议重写第二版的时候,我表示支持,但是得用我喜欢的 Coffeescript,估计那会他那时没有预料到 Coffeescript 不支持 JSX 的影响,也就欣然接受了。

React 作为一个 View 层的框架引入了虚拟 DOM,让 WebIDE 感觉上流畅了很多。但是打开速度比以前快是肯定的,这要归功于 Gulp 和 Webpack 的组合。React 项目逻辑变得越来越复杂的时候,将很难理清 state 跟 view 之间的对应关系。所以引入 Flux 来统一管理 React 中引起 state 变化的情况。

Gulp 相对于 Grunt,职责更单一,而且输入输出和管道的概念,感觉很符合 Unix 的设计哲学。而 Webpack 作为 Glup 的组件被引入让模块化变得更容易,可以采用相对一致的方式处理 JS 和 CSS 模块。

第二版的前端架构还是有些不如意的地方,比如说 Coffeescript 相对于 ES6,失去了JSX 这个好东西。Coffeescript 采用缩进的方式来写 React 的 Component,有点像 Jade,写的时候挺酷,读的时候不如 HTML Tag 来得直观。

另外在代码结构上也处理得不好,代码是按照 Store、View、Action 来归类的,往往开发一个业务需要到不同的目录去找文件。当然也有好的地方,比如说出现了前端控件插件化的结构雏形。

第三版 ES6 / React & Redux / Webpack

去年春节假期,终于有空去拔掉那颗横着长的智齿。含着一口鲜血,一边挂着点滴,一边拜读了阮一峰老师的《ECMAScript 6入门》。当时就感觉 Coffeescript 的使命完成了,暗暗地埋下了第三次重写的种子。

碰巧 IDE 要搞开源版本,那得给用户准备一份上得了厅堂的代码。有减法和加法两套方案,我提议做加法,新来的前端张烁同学表示支持。好吧,那就由你来完成,谁让你支持我。

这一版我们用上了 ES6,那 babel 肯定是必不可少的。babel 通过语法转换和 Polyfill 解决了 ES6 的向下兼容问题,同时也可以编译 JSX.

当前最火的框架无疑是 React 和 Vue.js. 由于 React 社区评价一直不错,而我们又熟悉,那就没有必要去折腾了。数据层的框架由 Flux 换成了 Redux. Redux 作为 Flux 的改进版,解决了 Store 过多问题。State 树也是个不错的改进。引入的 Reducer 作为一个纯函数,被表述为(previousState, action) => newState,颇有几分 Erlang/OTP 的味道。

这次也解决掉了文件结构没有按照业务分类的问题,更符合领域驱动设计中对内聚的要求。

做最好的 Web Terminal

一个完整的 IDE 需要具备很多功能,比如文件管理、版本管理、编辑器、编译器、执行环境等等。初次上线的最小功能集合里,我们认为 Web IDE 区别于 Web Editor 的一个功能亮点就是 Web Terminal。

Web Terminal 和 SSH 的工作原理类似,通过架设在 TCP 之上的应用层协议实现对主机的远程控制。相信大多数开发者都有 SSH 的使用经验,理解其工作原理的仅占少数。开始研究之初,我们也和大多数人一样搞不清楚 terminal、tty、pty、shell、bash 之间的区别,所以先来理理概念。

什么是 Terminal ?

从用户的角度来看,Terminal 是键盘和显示器的组合,也称为 TTY(电传打字机的缩写)。键盘输入字符,显示器显示字符。从进程的角度来看,终端是字符设备,可以通过 read、write、ioctl 等系统调用来读写和控制该设备。

TTY 早已进入了博物馆,桌面系统上字符界面基本被 GUI 界面替代。取而代之是一个称之为 Terminal Emulator(终端模拟器)的窗口程序,该程序显示的字符界面就是曾经物理显示器里的完整内容。

Terminal 作为真实的物理设备已经不复存在了,但是为了和面向终端的程序(比如Bash)进行通信,于是就了发明了 pty(Pseudoterminal,伪终端)。pty 是一对 master-slave 设备,master 设备表现得像一个文件,slave 设备表现得像一个终端设备,当 Terminal Emulator 作为一个非面向终端的程序不直接与 pty slave 通讯,而是通过文件读写流与 pty master 通讯,pty master 再将字符输入经过线路规程的转换传送给 slave,slave 进一步传递给 bash。

Bash 是一个命令行的解释器,通常也是进程会话的主进程,其职责是解释执行终端设备(或者伪终端的slave 设备)传递过来的字符串和控制字符,执行命令。

Web Terminal 的工作原理

理解了上面背景知识之后,再看 SSH 的原理图。 图片 SSH 是一个典型的 server-client 模式架构,用户通过终端将字符流传递给 SSH client。SSH client 和 SSH server 之间通过 TCP/IP 协议进行通讯。远端的 server 创建一对 pty,并且 fork+exec 一个 bash 进程,server 进程通过 pty 对与 bash 进行交互。

仿照 SSH 的工作原理,我们在 HTTP 协议之上设计了 Web Terminal,见下图: 图片 真实实现中,Socket.io 是应用层的通讯协议。Terminal Emulator 是一个纯 JS 的实现,Node.js后端使用 pty.js 模块来创建 pty 对。

宽字符兼容问题

Terminal Emulator 的开源 JS 实现网上能找到好几个,但是没有一个完美的支持中文字符问题。做了一些调查,我甚至没有找到判断 unicode 宽字符的 npm 库。深入研究发现,判断 unicode 字符串的宽度并不如想象的那么容易(找出宽字符的码表就 OK 了)。对于 unicode 宽字符有 combining 和 ambiguous 的情况。于是我 port 了一段 C 代码到 JS。具体的实现可以看这个开源项目 https://github.com/vangie/east-asian-width

解决了宽字符判断,算解决了基础,上层的 Terminal Emulator 仍然需要改造。阅读 Terminal Emulator 源码后,才知道它其实一个复杂的状态机。感觉是从 C 程序 翻译过来的。我们另一位前端大牛杨臻,硬生生地在此基础上把宽字符给改出来了,也顺带解决了块模式下一些莫名其妙的 Bug。

感谢 WebIDE 用户的改进建议,经过 2 年时间的打磨,目前 WebIDE 内置的 Terminal 已经达到了一个比较好的产品状态。

开源之路

WebIDE 的开源计划我们酝酿了一段时间。早年和同事一起翻译过《The Apache Way》那篇文章。文章里有谈到一个好的开源项目需要有一个好的代码基。如果我们一开始就宣布开源,呼吁大家一起来搞,肯定没人响应。也是基于这个考虑,我们把代码和 API 做了整理和重写。

为什么要开源

很多朋友问我,Coding 为什么开源,而且还是有竞争力的核心产品。我总结了如下三点。

老板的情怀,提升品牌影响力

Coding 的产品有开源先例的,早些时候就把 Coding 的 iOS 和 Android 客户端开源了。移动端版本 m.coding.net 开源了。Chrome 插件开源了。后来的 IntelliJ IDEA 的插件,我们是通过码市悬赏做的,做完也开源了,Coding 的开源项目,大家可以在 https://github.com/Coding 查看。

吸引用户,推动产品发展

一个好的产品不仅需要好的产品设计和研发团队,也需要大量的用户反馈来打磨和验证。开源以后平台版的访问量上升和来自于 Coding 和 Github 平台的 Issue 数量也印证了我们的想法。

形成开发者社区,提升代码质量

为了开源,我们重写了大量的代码,不是原来的代码不 work,而是拿出去见人的,要体面一点。为了开源,我们开始加入更多的测试,对于第三方的 PR,我们需要有一种方式验证其有效性,而社区实践下来,测试 + Review 的方式运作良好。另外也有写的不好,不够通用的地方,被更好的 PR 来替代,比如不兼容 Windows 的写法,就有用户 PR 了跨平台的写法。

接下来的计划

我们会在 Coding 和 Github 上持续维护社区版,和社区大家一起把 WebIDE 运营好。平台版本也会继续研发。随着社区版本的发展,相信会有越来越多的开发者熟悉 WebIDE。之后会考虑搞一些社区活动,把部分功能模块放到 Coding 的码市平台做悬赏。

Talk is cheap, show you the code. https://coding.net/u/coding/p/WebIDE

回到顶部