本文为 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