acm-header
登录

ACM通信

实践

陶醉于约束


软件开发/木偶的形象

插图:约翰·赫西

回到顶部

从最初用于验证HTML表单的JavaScript代码片段开始,web向交互性发展的轨迹最近真的开始加速了。一种新的Web应用程序开始出现,它们采用越来越交互式的用户界面,基于通过越来越多的JavaScript直接操作浏览器文档对象模型(DOM)。谷歌Wave于2009年5月在旧金山的谷歌I/O开发者大会上首次公开演示,它是这种新型Web应用程序的典范。Wave不是由服务器呈现的一系列独立的HTML“页面”来实现的,而是可以被描述为一个客户机/服务器应用程序,其中客户机是执行JavaScript应用程序的浏览器,而服务器是“云”。

负责启用新一代Web应用程序的关键浏览器技术并不是特别新:JavaScript在浏览器中运行,以操纵浏览器DOM作为实际呈现UI和响应用户事件的手段;CSS(层叠样式表)用于控制UI的视觉样式;XHR (XmlHttpRequest)子系统允许JavaScript应用程序代码与Web服务器异步通信,而不需要刷新整个页面,从而使增量UI更新成为可能。还有许多浏览器技术读起来像字母汤:XML、VML、SVG、JSON、XHTML、DTD……这样的例子不胜枚举。

奇怪的是,这些浏览器技术已经有很多年了,但是直到现在主流开发人员才将它们拼凑在一起来创建引人注目的交互式Web应用程序。为什么?当然,谷歌Web Toolkit团队的观点可以被无休止地争论,即主要的障碍实际上是实现细节。要把它们都编码起来,使它们能够在各种可用的浏览器上提供快速而可靠的性能,这实在太难了。

我们的对策是设计谷歌Web Toolkit (GWT),以允许开发人员使用Java语言而不是JavaScript将大部分时间用于编写和调试应用程序代码。使用Java意味着开发人员可以利用Java ide(集成开发环境)的生产力。一旦他们对自己的Java代码感到满意,开发人员就可以使用GWT的交叉编译器将Java源代码转换为功能等效的、优化的JavaScript。交叉编译的想法往往会引起人们的怀疑,我们已经听到了很多对此的怀疑,所以让我们退一步来描述我们是如何决定采用这种方法的以及事情是如何实际运行的。

GWT最初是一个原型,由谷歌软件工程师Joel Webber和我开发,作为解决Web开发过度约束问题的一种方法。主要由于谷歌地图和Gmail的成功,我们同时清楚地认识到以下几点:

  • 最终用户确实喜欢并需要基于浏览器的应用程序。
  • 丰富的客户端交互性(例如,Maps和Gmail)使此类应用程序比典型的Web 1.0一次页面应用程序的响应性和可用性都要高得多,因此也更加引人注目。
  • 每个主要的浏览器在技术上都能够支持这种交互式应用程序。它们都可以运行JavaScript并支持动态HTML,但bug、不一致和专有api和行为阻止任何单个JavaScript程序在大多数现代浏览器上一致和有效地工作。
  • 对JavaScript语言本身的支持,也就是对“纯”语言语法和核心JS库的支持,不包括DOM api,让我们惊讶的是,它在浏览器之间是相当一致和可靠的。

换句话说,浏览器(尤其是XHR、JavaScript和dom)为交付应用程序提供了一个功能强大(尽管令人沮丧)的平台。

回到顶部

Javascript预订

与此同时,我们对JavaScript是否是编写业务关键型应用程序的好语言有疑问。一方面,JavaScript是一种灵活的、动态类型的语言,它使某些类型的代码易于编写并且简洁。另一方面,同样的灵活性会使JavaScript在团队环境中更难以使用,因为没有一种简单的方法可以在整个代码库中自动强制使用一致的约定。确实,通过大量的额外工作,JavaScript团队可以坚持使用额外的元数据(例如JSDoc)来增强所有脚本,然后使用额外的工具来验证所有脚本是否符合商定的约定。这也必然将开发人员限制在可静态分析的JavaScript子集中,因为JavaScript中一些最动态的constructseval()和with语句是彻底击败静态分析的好例子。所有这些额外的东西——元数据和验证工具——看起来非常像一个特别的静态类型系统和编译器前端。

此外,我们迫切需要一个IDE。我们的经验使我们完全相信ide对生产力、质量和维护都有好处。现代Java ide中存在的一些特性,如代码完成、调试、集成单元测试、重构和语法感知搜索,在JavaScript中几乎不存在。这样做的原因与JavaScript的动态性有关。例如,在一般情况下,不可能在JavaScript编辑器中提供健全的代码完成,因为不同的运行时代码路径可能为相同的符号产生不同的含义。考虑以下合法的JavaScript:

ins01.gif

在[1]上,不可能静态地判断foo是函数还是变量,因此IDE代码完成只能提供“潜在正确”的建议,这是一种乐观的说法,即您必须反复检查IDE的代码完成建议,这反过来可能会减少从JavaScript IDE中实现的许多潜在生产力收益。出于类似的原因,JavaScript的自动重构工具很少见到,尽管这种工具在Java世界中随处可见。这些观察结果使得JavaScript作为编写大型应用程序的语言显得不那么有吸引力。

我们终于意识到我们想这么做开发我们的Java语言源代码部署它是纯JavaScript。通过选择Java语言作为初始语言,我们可以立即利用Java工具的伟大生态系统,特别是伟大的Java ide。唯一的问题是如何从Java源输入生成JavaScript。我们的答案是构建一个从java到JavaScript的编译器——一个优化编译器,事实上,因为我们认为既然我们无论如何都要编写一个编译器,为什么不确保它产生小的,高效的JavaScript呢?此外,我们还发现,因为Java有一个静态类型系统,它允许许多编译时优化,而动态类型的javascript做不到这一点。

作为一个例子,考虑内联和去虚拟化之间的交互(即在方法调用中删除多态性)。在JavaScript中,开发人员通常模拟面向对象结构,如多态性。例如,框1说明了如何使用GWT用JavaScript和Java语言编写一个简单的Shape层次结构。

的用法之外,这两个示例的源代码看起来几乎相同@Override(这有助于防止错误),以及散布在字段、方法和局部变量上的显式类型名的存在。

由于额外的类型信息,GWT编译器能够执行一些优化。未经修饰的GWT编译器输出大致如下所示:

ins02.gif

注意,在[1]和[2]中,进行了级联优化。

首先,编译器内联了对displayArea ()方法。这被证明是有帮助的,因为它消除了为该方法生成代码的需要。的确,displayArea ()在编译后的脚本中完全没有,这导致了较小的大小减小。更好的是,内联代码可以在特定于使用的上下文中进一步优化,在这种上下文中优化器可以获得更多信息。

接下来,优化器注意到shape1而且shape2可以“收紧”为比原始声明更具体的类型。换句话说,尽管shape1被声明为一个形状,编译器看到它实际上是一个圆。类似地,类型shape2被收紧为方形。因此,调用getArea ()在[1]和[2]中更具体。前者成了Circle的电话getArea (),后者变成了给Square的电话getArea ().因此,所有的方法调用都被静态绑定,所有的多态性都被删除了。

最后,在删除了所有多态性之后,优化器内联了Circle的getArea ()变成[1]和Square'sgetArea ()[2]。这两个getArea ()方法在编译后的脚本中不存在,因为已经内联了。数学。π是一个编译时常数,也被简单地内联到[1]中。

所有这些优化的好处是速度。GWT编译器生成的脚本执行得更快,因为它消除了多层函数调用。

由于显而易见的原因,编写大型代码库时往往强调清晰性和可维护性,而不仅仅是纯粹的性能。说到可维护性,抽象、重用和模块化是绝对的基石。然而,在前面的例子中,可维护性和性能发生了直接冲突:内联代码更快,但没有软件工程师会这样写。当然,“可维护性与性能”的二分法并不是Java代码独有的。同样,编写模块化的、可维护的JavaScript往往会产生比人们所希望的更慢、更大的脚本。因此,所有构建复杂Web应用程序的开发人员都必须面对这种取舍的现实。关键的问题似乎是,一旦编写了代码库,它对优化的适应性有多大。在这方面,Java类型系统提供了很大的优势,这就是GWT编译器能够包含许多类似于这里所示的优化的原因,以帮助减轻在设计良好的面向对象代码库中可能最终不得不付出的“抽象代价”。

回到顶部

融合两个世界

当然,创建允许开发人员用Java构建基于浏览器的应用程序的环境只解决了开发周期的一部分。与大多数开发人员一样,我们不能生成完美的代码,因此我们知道还必须解决调试GWT程序所涉及的问题。

第一次听说GWT时,人们通常认为您以以下方式使用它:

  1. 编写Java源代码。
  2. 用GWT的编译器编译成JavaScript。
  3. 在浏览器中运行和调试JavaScript。

事实上,这根本不是GWT的工作方式。您的大部分时间都花在GWT上托管模式,它允许您在普通的Java调试器(例如Eclipse)中运行和调试Java代码,就像您习惯做的那样。只有在编写并调试了应用程序之后,才需要将其编译成JavaScript。因此,每个人对永远无法理解和调试编译后的JavaScript的条件反射性恐惧被证明是没有根据的。

使托管模式成为有效调试环境的秘密在于它不仅仅是模拟在Java中调试时浏览器的行为。托管模式直接将真正的Java调试与真正的浏览器UI和事件系统结合在一起。托管模式在概念上很简单,它在单个JVM (Java虚拟机)进程中执行:

  1. 启动一个实际浏览器的实例,内嵌在进程内,它可以通过JNI (Java本机接口)由Java代码控制。我们称之为托管浏览器
  2. 创建一个CCL(编译类装入器)来装入GWT模块的入口点类。
  3. 每当要求CCL获取一个类时,它都会检查该类是否具有JSNI (JavaScript原生接口)方法。如果没有,则可以直接使用该类。如果找到本机方法,则从源代码编译类,并重写JSNI方法。
  4. 运行入口点类的字节码,它将依次请求由CCL加载其他类,CCL将重复步骤3中的过程。

第3步,重写JSNI方法,是这里真正简洁的部分。JSNI是在手写JavaScript中实现本机Java方法的方法,如框2所示。

因此,托管模式的CCL将JSNI方法转换为链接,将其调用重定向到托管浏览器的JavaScript引擎,进而驱动真正的浏览器DOM。

从JVM的角度来看,这里描述的所有内容都是纯Java字节码,因此可以使用Java调试器进行正常调试。从开发人员的角度来看,他或她可以看到真正的浏览器的真实行为是由Java源代码驱动的,而不是首先交叉编译成纯JavaScript。

这可能是关于托管模式最令人兴奋的一点:因为它与Java代码动态工作,不依赖于调用GWT交叉编译器(可能很慢),所以托管模式非常快。这意味着开发人员可以享受直接使用JavaScript时所享受的运行/调整/刷新行为。

因此,GWT成功地将传统优化编译器的优点与动态语言的快速开发周期结合在了一起。尽管编译技术可能看起来很复杂,但它实际上是优化编译器的标准方法。在这一过程中,我们遇到的真正的技术问题围绕着创建UI库的努力,以在不影响大小或速度的情况下,同时考虑到浏览器特有的怪癖。换句话说,我们需要提供许多不同的UI功能实现——Firefox的版本A, Safari的版本B,等等——而不是让编译后的应用程序承担所有变体的合并,从而迫使每个浏览器至少下载一些不相关的代码。我们的解决方案是一种独特的机制延迟绑定,它安排GWT编译器生成的不是一个输出脚本,而是任意数量的输出脚本,每个脚本都针对特定的一组环境进行了优化。

每个编译的输出都是许多不同实现选择的组合,这样每个脚本都有(而且只有)它所需要的代码量。值得一提的是,除了处理浏览器变化之外,延迟绑定还可以在其他方向上专门进行编译。例如,延迟绑定用于创建每个地区的专门化(例如,为什么法语用户必须下载本地化为英语的字符串,反之亦然?)事实上,延迟绑定是完全开放的,因此开发人员可以根据自己的需要添加专门化轴。

这种方法确实会创建大量编译过的脚本,但我们认为这是一种受欢迎的权衡:您最终在许多优化过的脚本上花费了廉价的服务器磁盘空间,因此,应用程序下载和运行速度更快,使最终用户更高兴。

无论如何,我们开发GWT的经验使我们完全相信,没有必要向Web开发的典型约束让步。也就是说,通过一点创造性和一些专门的努力,我们现在知道,在不影响应用程序用户最终享受的体验的情况下,保留更熟悉的开发环境的丰富性确实是可能的。

ACM队列的q戳相关文章
queue.acm.org

案例研究:转向AJAX
杰夫·诺沃克
http://queue.acm.org/detail.cfm?id=1515744

聪明编程:人vs.工具
也斯利
http://queue.acm.org/detail.cfm?id=945135

在生产环境中调试AJAX
Eric Shrock
http://queue.acm.org/detail.cfm?id=1506423

回到顶部

作者

布鲁斯·约翰逊他在亚特兰大成立了谷歌的工程办公室,就在他的母校佐治亚理工学院的隔壁,目标是生产谷歌Web Toolkit和许多相关工具,旨在使Web开发更高效、有效和更有趣。

回到顶部

脚注

DOI: http://doi.acm.org/10.1145/1562164.1562181

回到顶部

数据

UF1数字框1。形成Javascript (a)和Java (b)中可能出现的层次结构。

UF2数字箱2。在手写JavaScript中实现本机Java方法。

回到顶部


©2009 acm 0001-0782/09/0900 $10.00

允许为个人或课堂使用本作品的全部或部分制作数字或硬拷贝,但不得为盈利或商业利益而复制或分发,且副本在首页上附有本通知和完整的引用。以其他方式复制、重新发布、在服务器上发布或重新分发到列表,需要事先获得特定的许可和/或付费。

数字图书馆是由计算机协会出版的。版权所有©2009 ACM, Inc.


没有发现记录

登录为完全访问
»忘记密码? »创建ACM Web帐号
文章内容:
Baidu
map