下载本文的 PDF 版本 PDF

案例研究:RIA 开发

专题文章:生产环境中调试 AJAX

在缺乏适当浏览器支持的情况下,我们可以采取哪些步骤来调试生产环境中的 AJAX 代码?

Eric Schrock,Sun Microsystems

JavaScript 语言有着一段奇特的历史。最初,它只是一个简单的工具,让 Web 开发人员可以为原本静态的网页添加动态元素,但后来却发展成为交付基于 Web 应用程序的复杂平台的核心。在早期,该语言静默处理失败的能力被视为一种优势。如果图像翻转失败,那么与向用户呈现难看错误对话框相比,最好是保持无缝的 Web 体验。

这种对失败的容忍已成为现代浏览器的核心设计原则,错误会被静默记录到隐藏的错误控制台中。即使当用户知道控制台时,他们也只能找到少量信息,这是基于脚本很小,并且一条指示文件和行号的消息应该足以识别问题来源的假设。

然而,随着复杂的 AJAX 应用程序的激增,这种情况已不再成立,它永久性地改变了 JavaScript 环境的设计中心。

脚本变得庞大而复杂,跨越多个文件,并广泛使用异步、动态实例化的函数。现在,在最好的情况下,脚本执行失败会导致糟糕的用户体验。在最坏的情况下,应用程序停止工作或破坏服务器端状态。默认接受脚本错误不再合适,并且对于识别复杂 AJAX 应用程序中的故障而言,单行数字和消息也不足够。因此,缺乏强大的错误消息和原生堆栈跟踪已成为当今 AJAX 开发的主要困难之一。

问题的严重性取决于调试环境的性质。在开发过程中,工程师拥有几乎无限的自由。他们可以随意重现问题,启动交互式调试器,或快速修改和部署测试代码,从而能够快速形成和测试假设,以确定问题的根本原因。然而,一旦应用程序离开这个避风港进入生产环境,一切都会改变。问题可能无法在用户环境之外重现,并且获得系统访问权限以进行交互式调试通常是不可能的。运行测试代码,即使不需要停机,也可能比问题本身更糟糕。对于这些环境,事后调试问题的能力是必要的。当在生产环境中遇到错误时,必须保留足够的信息,以便能够准确确定根本原因,并且必须以一种可以轻松地从用户传输到工程部门的形式提供此信息。

根据浏览器的不同,JavaScript 拥有一套丰富的工具,用于在开发阶段识别问题根源的错误。诸如 Firebug、Venkman 和内置 DOM(文档对象模型)检查器等工具非常有价值。然而,与大多数语言一样,在生产环境中事情变得更加困难。理想情况下,我们希望能够获得 JavaScript 执行上下文的完整转储,但没有浏览器能够以安全或实用的方式支持这样的功能。这使得错误消息成为我们唯一的希望。这些错误消息必须提供足够的上下文来识别问题的根本原因,并且它们需要集成到应用程序体验中,以便用户可以管理错误流并了解如何将所需信息提供给开发人员以进行进一步分析。

此过程的第一步是提供一种在应用程序中显示错误的方法。虽然仅仅依赖于alert()及其简单的弹出消息似乎很诱人,但与之相关的视觉体验非常突兀。大量文本无法很好地适应弹出窗口,并且大量此类错误可能需要快速连续地重复关闭对话框——有时会使向前推进变得不可能。许多框架为此目的提供了内置控制台,但是一个非常简单的隐藏 DOM 元素,允许我们展开、折叠、清除和隐藏控制台,就可以很好地完成这项工作。借助此集成控制台,我们可以捕获并显示通常会丢失到浏览器错误控制台的错误。在大多数浏览器上,错误可以被顶级的window.onerror()处理程序捕获,该处理程序提供特定于浏览器的消息、文件和行号。

简单地将这些消息转储到用户可见的控制台代表着向前迈进了一大步,但即使是准确的消息、文件和行号,在调试 AJAX 应用程序中的问题时也可能毫无价值。除非错误是一个简单的拼写错误,否则我们需要更好地理解遇到错误的上下文。

面对意外错误,接下来的问题几乎总是:“我们是如何以及为何来到这里的?” 如果我们幸运的话,我们可以只查看源代码并做出一些有根据的猜测。改进此过程最常见的方法是通过堆栈跟踪。生成堆栈跟踪的能力是健壮编程环境的标志,但不幸的是,这也是经常被忽视的一个功能。堆栈跟踪通常被认为太难构建、在生产环境中提供成本太高,或者根本不值得实现。因为它们通常被视为仅在特殊情况下才需要的东西,所以堆栈跟踪的计算成本通常可能很高。然而,随着系统复杂性的增长以及异步性的更大程度的应用,这种观点变得越来越站不住脚。例如,在消息传递系统中,原始消息入队的上下文通常比消息出队后发生故障的上下文更重要。在 AJAX 环境中(其中异步值得在首字母缩略词中占有一席之地),闭包的实例化上下文通常比闭包本身更有用。

可悲的是,JavaScript 对堆栈跟踪的支持严重不足。支持堆栈跟踪的浏览器仅通过抛出的异常提供堆栈跟踪,并且大多数浏览器根本不提供堆栈跟踪。堆栈跟踪永远无法在全球处理程序(例如window.onerror())中使用,因为参数是由优化最低公分母的 DOM 定义的。一个window.onexception()处理程序(作为异常对象传递)将是一个受欢迎的补充。相反,我们被迫显式捕获所有异常。从表面上看,这似乎是一项艰巨的任务——我们不想将每段代码都包装在 try/catch 块中。然而,在 AJAX 应用程序中,所有 JavaScript 代码都在以下四种上下文中执行

* 加载脚本时的全局上下文

* 来自响应用户交互的事件处理程序

* 来自超时或间隔

* 来自处理 XMLHTTPRequest 时的回调

第一种情况我们必须推迟到window.onerror(),但由于它发生在脚本加载时,因此此类错误很难逃脱开发。对于其余情况,我们可以通过我们自己的注册函数自动将回调包装在 try/catch 块中

function mySetTimeout(callback, timeout)
        {
              var wrapper = function () {
                    try {
                          callback();
                    } catch (e) {
                          myHandleException(e);
                    }
              };  
return (setTimeout(wrapper, timeout)); }

对于事件侦听器,事情变得稍微复杂一些,因为 API 需要原始函数才能在稍后删除处理程序

function myAddEventListener(obj, event, callback, capture)
        {
              var wrapper = function (evt) {
                    try {
                          callback(evt);
                    } catch (e) {
                          myHandleException(e);
                    }
              };
              if (!obj.listeners)
                    obj.listeners = new Array();
              obj.listeners.push({
                    event: event,
                    wrapper: wrapper,
                    capture: capture,
                    callback: callback
              });
             obj.addEventListener(event, wrapper, capture);
        }

表 1 浏览器支持

浏览器 事件 消息 文件 堆栈
Firefox 3.0.5 window.onerror X X1 X1
DOM 异常 X X X
运行时异常 X X X X
用户异常 X2
IE 7.0.5730.13 window.onerror X X X
DOM 异常 X
运行时异常 X
Safari 3.2.1 window.onerror
DOM 异常 X X X
运行时异常 X X X
用户异常 X X
Chrome 1.0.154.36 window.onerror
DOM 异常 X
运行时错误 X
用户异常
Opera 9.63 window.onerror
DOM 异常 X X3
运行时异常 X X
用户异常 X3

1. Firefox 中的 DOM 错误没有显式的文件和行号,但信息包含在消息中。

2. 任意异常在 Firefox 中没有堆栈跟踪,但那些使用Error() 构造函数的异常有。

3. Opera 可以配置为为异常生成堆栈跟踪,但默认情况下未启用。

表 1 描述了从全局上下文以及在捕获不同浏览器特定类型的异常时可用的信息。该表展示了集成浏览器支持的局限性。在没有每个异常的可靠堆栈跟踪的情况下,我们被迫生成程序化的堆栈跟踪以获得更好的覆盖率。值得庆幸的是,arguments对象的语义允许我们编写一个函数来生成程序化的堆栈跟踪

function myStack()
{
      var caller, depth;
      var stack = new Array();
      for (caller = arguments.callee, depth = 0;
          caller && depth < 12;
          caller = caller.caller, depth++) {
            var args = new Array();
            for (var i = 0; i < caller.arguments.length; i++)
                  args.push(caller.arguments[i]);
            stack.push({
                caller: caller,
                args: args
            });
      }
      this.stack = stack;
}

一个完整的实现将提供一种跳过不感兴趣的帧的方法,包括本机堆栈跟踪(通过 try/catch 块),并提供一个toString()方法来转换结果。我们没有文件和行号,但我们有函数名和参数。可悲的是,JavaScript 中匿名函数的激增使得很难获得函数的规范名称。toString()方法可以为我们提供特定函数的源代码,但在打印堆栈跟踪时,我们需要一个名称。实现此目的的唯一有效方法是在沿途构建函数的易于理解的名称时搜索所有对象的全局命名空间。这看起来很昂贵,但我们只需要在发生错误时打印堆栈跟踪。大多数函数要么位于全局命名空间中,要么位于一级深度,要么位于特定对象的原型中的二级深度。要获取函数的名称,我们只需要搜索 window 对象的成员、其所有子对象以及其原型对象的所有子对象。如果我们找到匹配项,那么我们可以使用此沿袭构建名称。

借助函数名称和参数,即使在没有本机堆栈跟踪支持的浏览器上,我们也可以显示合理的堆栈跟踪副本。然而,需要注意的一个问题是,获取函数名称不适用于 Internet Explorer 7。由于原因尚不清楚,全局函数不包含在迭代 window 对象成员时。

仔细构建全局异常处理程序使我们能够处理本机浏览器异常和动态生成的异常。虽然将堆栈跟踪附加到我们的自定义异常很有用,但当在复杂的环境中处理异步闭包(尤其是异步 XMLHTTPRequest 对象)时,此机制的真正威力显而易见。在复杂的 AJAX 应用程序中,所有服务器活动都必须异步发生;否则,浏览器将在等待响应时挂起。典型的服务模型将执行以下操作

function dosomething(a, b)
        {
              service.dosomething(a + b, function (ret, err) {
                    if (err)
                          throw (err);
                    process(ret);
              });
        }

如果在process()函数中发生异常,则嵌入在服务实现中的包装器将捕获结果并将其传递给我们的异常处理程序。但堆栈跟踪将在process()处结束,而我们真正想要的是调用dosomething()时的堆栈跟踪。由于我们的堆栈跟踪是按需生成的,并且组装成本很低,因此我们可以通过在调度每个异步调用之前记录堆栈跟踪,然后将其链接到任何捕获的异常来实现此目的。我们的核心调度例程如下所示

function dispatch(func, args, callback)
        {
              var stack = new myStack();
              dodispatch(func, args, function (ret, err) {
                    try {
                          callback(ret, err);
                    } catch (e) {
                          e.linkedStack = stack;
                          myHandleException(e);
                    }
              });
        }

这允许使用相同的异常处理程序透明地处理服务器端故障。如果异步闭包生成意外异常,我们可以包含原始 XMLHTTPRequest 发出的上下文。

通过认真遵循这些设计原则,我们可以构建一个环境,通过使用户能够向开发人员提供更丰富的信息以进行进一步分析,从而显着提高我们调试问题的能力。不幸的是,需要此环境来克服当前 JavaScript 运行时环境的不足。在没有处理所有未捕获异常的单一入口点的情况下,我们被迫将所有回调包装在 try/catch 块中;并且在没有可靠的堆栈跟踪的情况下,我们被迫生成我们自己的调试基础设施。似乎很明显,一个实现这两个功能的浏览器很快将成为 AJAX 应用程序的首选开发环境。在实现这一目标之前,仔细设计 AJAX 环境仍然可以显着提高应用程序用户的可调试性和可维护性。

ERIC SCHROCK 自 2003 年以来一直是 Sun Microsystems 的工程师。在 Solaris 内核组开始工作(在那里他参与了 ZFS 等项目)之后,Schrock 在过去几年中一直在帮助开发 Sun Storage 7000 系列设备,作为公司 Fishworks 工程团队的一员。

此处包含的示例的完整源代码以及浏览器支持表的最新版本可以在 http://blogs.sun.com/eschrock/resource/ajax/index.html 找到。

acmqueue

最初发表于 Queue 第 7 卷,第 1 期
数字图书馆 中评论本文





更多相关文章

Pete Hunt, Paul O'Shannessy, Dave Smith, Terry Coatta - React:Facebook 在编写 JavaScript 中的函数式转变
用户友好的 JavaScript 前端长期以来的讽刺之一是,构建它们通常需要艰难地遍历 DOM(文档对象模型),而 DOM 几乎不以对开发人员的友好性而闻名。但是现在,由于 Facebook 决定开源其用于构建用户界面组件的 React 库,开发人员有了一种避免直接与 DOM 交互的方法。


Bruce Johnson - 在约束中狂欢
Web 向交互性发展的轨迹始于用于验证 HTML 表单的简单 JavaScript 代码片段,但最近才真正开始加速。一种新型的 Web 应用程序开始涌现,它具有基于直接操作浏览器 DOM(文档对象模型)并通过越来越多的 JavaScript 构建的越来越交互式的用户界面。Google Wave 于 2009 年 5 月在旧金山举行的 Google I/O 开发者大会上首次公开演示,它体现了这种新型 Web 应用程序的风格。


Jeff Norwalk - 案例研究:转向 AJAX
小型初创公司经常面临令人眼花缭乱的技术选择:如何交付他们的应用程序,使用哪种语言,是使用现有组件(商业或开源)还是自己开发……等等。更重要的是,围绕这些选择的决策通常需要快速做出。本案例研究真实地再现了一家年轻的初创公司在致力于在 Web 上部署产品时面临的各种挑战。与许多初创公司一样,这也是一个没有美好结局的故事。





© 保留所有权利。

© . All rights reserved.