使用 JavaScript 构建更高效的 Windows 应用商店应用程序:错误处理
不管您相信与否,应用程序开发人员有时会编写出不起作用的代码。 或者,代码虽然起作用,但效率低得可怕,还占用大量内存。 更糟糕的是,低效率的代码会导致用户体验较差,使用户感到十分生气,因此不得不卸载应用程序,留下差评。
下面我将探究在使用 JavaScript 构建 Windows 应用商店应用程序时可能遇到的常见性能与效率问题。 在本文中,我将讨论使用 Windows JavaScript 库 (WinJS) 进行错误处理的最佳做法。 在后续文章中,我将讨论不阻塞 UI 线程而完成工作的技巧,尤其是使用 Windows 8.1 中提供的 Web 工作线程或 WinJS 2.0 中的新 WinJS.Utilities.Scheduler API。 我还将展示 WinJS 2.0 中新的可预测对象生命周期模型,重点介绍何时以及如何处理控件。
对于每个主题领域,我重点讨论三个方面:
- 使用 JavaScript 构建的 Windows 应用商店应用程序中可能出现的错误或效率低下的情况。
- 用于查找这些错误和低效的诊断工具。
- 可以改善特定问题的 WinJS API、功能和最佳做法。
我提供了一些特意弄错的代码,不过请放心,我会指出代码中哪些应该或不应该正常工作。
我使用 Visual Studio 2013、Windows 8.1 和 WinJS 2.0 来进行这些演示。 我使用的许多诊断工具是 Visual Studio 2013 提供的。 如果您尚未下载这些工具的最新版本,可从 Windows 开发人员中心 (bit.ly/K8nkk1) 获得。 新诊断工具是通过 Visual Studio 更新发布的,因此请务必定期检查更新。
我假设您对使用 JavaScript 构建 Windows 应用商店应用程序已十分熟悉。 如果您对这个平台相对陌生,建议您从基本的“Hello World”示例开始 (bit.ly/vVbVHC),或者先从挑战性更大的 JavaScript 的 Hilo 示例开始 (bit.ly/SgI0AA)。
设置示例
首先,我在 Visual Studio 2013 中使用导航应用程序模板创建一个新项目,此模板为构建基本多页应用程序提供了一个很好的起点。 我还向解决方案根目录下的 default.html 页面添加了一个 NavBar 控件 (bit.ly/14vfvih),以取代模板提供的 AppBar 代码。 因为我需要演示多个概念、诊断工具和编程技巧,所以,我将针对每个演示向应用程序添加一个新页面。 这样,我在所有测试用例之间导航就方便得多了。
图 1 显示了 NavBar 的完整 HTML 标记。 如果您沿用该示例,请将这些代码复制并粘贴到解决方案中。
图 1 NavBar 控件
<!-- The global navigation bar for the app. -->
<div id="navBar" data-win-control="WinJS.UI.NavBar">
<div id="navContainer"
data-win-control="WinJS.UI.NavBarContainer">
<div id="homeNav"
data-win-control="WinJS.UI.NavBarCommand"
data-win-options="{
location: '/pages/home/home.html',
icon: 'home',
label: 'Home page'
}">
</div>
<div id="handlingErrors"
data-win-control="WinJS.UI.NavBarCommand"
data-win-options="{
location: '/pages/handlingErrors/handlingErrors.html',
icon: 'help',
label: 'Handling errors'
}">
</div>
<div id="chainedAsync"
data-win-control="WinJS.UI.NavBarCommand"
data-win-options="{
location: '/pages/chainedAsync/chainedAsync.html',
icon: 'link',
label: 'Chained asynchronous calls'
}">
</div>
</div>
</div>
有关构建导航栏的详细信息,请参阅 Rachel Appel 撰写的一些“新型应用程序”专栏文章,例如 msdn.microsoft.com/magazine/dn342878 的专栏文章。
您可以仅带导航栏运行此项目,只是这样的话,单击任何一个导航栏按钮都会在 navigator.js 中引发异常。 在本文的后面部分,我将讨论如何处理 navigator.js 中出现的错误。 现在请注意,应用程序总是在主页上启动,您需要右键单击应用程序才会显示导航栏。
处理错误
显然,要避免错误,最好的办法是发布不会引发错误的应用程序。 在理想情况下,每位开发人员都编写不会崩溃、不会引发异常的完美代码。 但这种理想情况是不存在的。
正如用户希望应用程序没有任何错误一样,他们同时也特别擅长于发现创造性新方法使程序中断 — 您做梦都想不到的方法。 因此,您需要在应用程序中加入可靠的错误处理。
使用 JavaScript 和 HTML 构建的 Windows 应用商店应用程序中的错误与普通网页中的错误类似。 在允许错误处理的文档对象模型 (DOM) 对象(例如,<script>、<style> 或 <img> 元素)中发生错误时,会引发该元素的 onerror 事件。 对于 JavaScript 调用堆栈中的错误,错误会沿调用链向上行进,直至被捕获(例如,在 try/catch 块中)或到达窗口对象,从而引发 window.onerror 事件。
除了由 Microsoft Web Platform 向普通网页提供的内容外,WinJS 还针对代码提供了多个错误处理机会层。 在基本级别上,任何未在 try/catch 块中捕获的错误或应用于 WinJS.Promise 对象的 onError 处理程序(例如,在对 then 或 done 方法的调用中)都会引发 WinJS.Application.onerror 事件。 稍后我会进行介绍。
实际上,除 Application.onerror 外,您还可以侦听其他级别的错误。 通过 WinJS 以及由 Visual Studio 提供的模板,您还可以处理页面控件级别以及导航级别的错误。 若在应用程序导航至和加载页面时引发了错误,则该错误会触发导航级别的错误处理,然后触发页面级别的错误处理,最后触发应用程序级别的错误处理。 您可以取消导航级别的错误,但还是会引发应用于页面错误处理程序的任何事件处理程序。
在本文中,我将介绍每个错误处理层,先从最重要的开始:Application.onerror 事件。
应用程序级别的错误处理
WinJS 提供 WinJS.Application.onerror 事件 (bit.ly/1cOctjC),这是应用程序防止错误的最基本防线。 它接收由 window.onerror 捕获的所有错误。此外,它还捕获出错的 promise 以及管理应用程序模型事件过程中发生的任何错误。 您可以向应用程序中的 window.onerror 事件应用事件处理程序,但最好还是将 Application.onerror 仅用于要监视的单个事件队列。
在 Application.onerror 处理程序捕获到错误后,您需要决定如何对其进行处理。 有几种选择:
- 对于关键的阻塞错误,通过消息对话框提醒用户。 关键错误是会影响应用程序的连续运行、可能需要用户输入才能继续的错误。
- 对于信息性错误和非阻塞错误(如无法同步或获取在线数据),可通过弹出窗口或内联消息来提醒用户。
- 对于不会影响用户体验的错误,可以静默接受错误。
- 大多数情况下,可将错误写入跟踪日志(尤其是挂接到分析引擎的跟踪日志),这样可以获取客户遥测。 有关可用的分析 SDK,请访问 services.windowsstore.com 上的 Windows 服务目录,单击左侧列表中的“Analytics”(在“By service type”下面)。
在本示例中,我都使用消息对话框。 我打开 default.js (/js/default.js),在 app.oncheckpoint 事件的处理程序下面的主匿名函数内添加图 2 中显示的代码。
图 2 添加消息对话框
app.onerror = function (err) {
var message = err.detail.errorMessage ||
(err.detail.exception && err.detail.exception.message) ||
"Indeterminate error";
if (Windows.UI.Popups.MessageDialog) {
var messageDialog =
new Windows.UI.Popups.MessageDialog(
message,
"Something bad happened ...");
messageDialog.showAsync();
return true;
}
}
在本示例中,错误事件处理程序显示一条消息,告诉用户发生了错误以及发生了什么错误。 事件处理程序返回 true 以保持该消息对话框打开,直至用户将其关闭。 (返回 true 还会通知 WWAHost.exe 进程该错误已得到处理,它可以继续运行。)
现在,我将创建一些供此代码处理的错误。 我会创建一个自定义错误,引发该错误,然后在事件处理程序中将其捕获。 对于第一个示例,我向页面文件夹中添加一个名为 handlingErrors 的新文件夹。 在该文件夹中,我通过在解决方案资源管理器中右键单击该项目并选择“添加 | 新建项”添加新页面控件。 在我向项目添加 handlingErrors 页面控件时,Visual Studio 会在 handlingErrors 文件夹 (/pages/handlingErrors) 中提供三个文件:handlingErrors.html、handlingErrors.js 和 handlingErrors.css。
打开 handlingErrors.html,在正文的 <section> 标记内添加此简单标记:
<!-- When clicked, this button raises a custom error. -->
<button id="throwError">Throw an error!</button>
接下来,我打开 handlingErrors.js,向 PageControl 对象的 ready 方法的按钮添加事件处理程序,如图 3 所示。 我已针对上下文在 handlingErrors.js 中提供了整个 PageControl 定义。
图 3 handlingErrors PageControl 的定义
// For an introduction to the Page Control template, see the following documentation:
// https://go.microsoft.com/fwlink/?LinkId=232511
(function () {
"use strict";
WinJS.UI.Pages.define("/pages/handlingErrors/handlingErrors.html", {
ready: function (element, options) {
// ERROR: This code raises a custom error.
throwError.addEventListener("click", function () {
var newError = new WinJS.ErrorFromName("Custom error",
"I'm an error!");
throw newError;
});
},
unload: function () {
// Respond to navigations away from this page.
},
updateLayout: function (element) {
// Respond to changes in layout.
}
});
})();
现在,按 F5 运行示例,导航至 handlingErrors 页面,然后单击“Throw an error!”按钮。 (如果您这样操作,可以看到一个 Visual Studio 对话框,通知您已引发错误。 单击“Continue”使示例继续运行。)随后会弹出该错误的消息对话框,如图 4 所示。
图 4 消息对话框中显示的自定义错误
自定义错误
Application.onerror 事件对于它所处理的错误有一些预期。 创建自定义错误的最佳方式是使用 WinJS.ErrorFromName 对象 (bit.ly/1gDESJC)。 所创建的对象公开一个供错误处理程序进行分析的标准接口。
若要不使用 ErrorFromName 对象来创建自己的自定义错误,您需要实现一个返回该错误消息的 toString 方法。
否则,在引发自定义错误时,Visual Studio 调试器和消息对话框都会显示“[Object 对象]”。它们分别针对该对象调用 toString 方法,但由于未在直接对象中定义这种方法,调试器或消息对话框会在整个原型继承链中寻找 toString 的定义。 在到达具有 toString 方法的 Object 基元类型时,它调用该方法(仅显示有关该对象的信息)。
页面级别的错误处理
WinJS 中的 PageControl 对象提供应用程序的另一个错误处理层。 WinJS 将在加载页面过程中发生错误时调用 IPageControlMembers.error 方法。 不过,在加载页面之后,Application.onerror 事件处理程序将选取 IPageControlMembers.error 方法错误,忽略页面错误方法。
我将向表示 handleErrors 页面的 PageControl 添加一个错误方法。 该错误方法使用 WinJS.log 写入 Visual Studio 中的 JavaScript 控制台。 首先需要启动日志记录功能,因此,在尝试使用该方法之前,我需要调用 WinJS.Utilities.startLog。 另请注意,我先检查 WinJS.log 成员是否存在,然后实际调用它。
图 5 显示了 handleErrors.js 的完整代码 (/pages/handleErrors/handleErrors.js)。
图 5 完整 handleErrors.js
(function () {
"use strict";
WinJS.UI.Pages.define("/pages/handlingErrors/handlingErrors.html", {
ready: function (element, options) {
// ERROR: This code raises a custom error.
throwError.addEventListener("click", function () {
var newError = {
message: "I'm an error!",
toString: function () {
return this.message;
}
};
throw newError;
})
},
error: function (err) {
WinJS.Utilities.startLog({ type: "pageError", tags: "Page" });
WinJS.log && WinJS.log(err.message, "Page", "pageError");
},
unload: function () {
// TODO: Respond to navigations away from this page.
},
updateLayout: function (element) {
// TODO: Respond to changes in layout.
}
});
})();
WinJS.log
图 5 中显示的对 WinJS.Utilities.startLog 的调用会启动 WinJS.log 帮助程序函数,默认情况下,此函数将输出写至 JavaScript 控制台。 虽然这样做在设计期间进行调试时会大有帮助,但是它不允许您在用户安装应用程序后捕获错误数据。
对于随时可以发布和部署的应用程序,应考虑自己创建调用分析引擎的 WinJS.log 实施方式。 这样,您可以收集有关应用程序性能的遥测数据,从而可以在应用程序的将来版本中修复无法预见的错误。 只是要确保客户知道会收集数据,并且要在应用程序的隐私声明中清楚列出分析引擎会收集哪些数据。
请注意,在以这种方式重写 WinJS.log 时,WinJS.log 函数会捕获以其他方式来到 JavaScript 控制台的所有输出,包括来自 Scheduler 的状态更新等内容。 因此,您需要将有意义的名称和类型值传递到对 WinJS.Utilities.startLog 的调用中,以便筛选出所有不需要的错误。
现在,我尝试运行示例并再次单击“Throw an error!”。 这会导致与以前完全相同的行为:Visual Studio 选取该错误,然后触发 Application.onerror 事件。 JavaScript 控制台不显示任何与错误有关的消息,因为错误是在加载页面之后引发的。 这样,错误仅由 Application.onerror 事件处理程序选取。
那么,为何要使用 PageControl 错误处理? 这是因为,它对捕获和诊断在 HTML 中通过声明方式创建的 WinJS 控件中的错误特别有帮助。 例如,我在按钮下面 handleErrors.html (/pages/handleErrors/handleErrors.html) 的 <section> 标记内添加以下 HTML 标记:
<!-- ERROR: AppBarCommands must be button elements by default
unless specified otherwise by the 'type' property. -->
<div data-win-control="WinJS.UI.AppBarCommand"></div>
现在,按 F5 运行示例并导航到 handleErrors 页面。 同样,将会显示消息对话框,直至将其关闭。 不过,在 JavaScript 控制台中会显示以下消息(您需要切换回桌面进行检查):
pageError: Page: Invalid argument: For a button, toggle, or flyout command, the element must be null or a button element
请注意,即使我处理了 PageControl(它记录错误)中的错误,也会显示应用程序级别的错误处理。 那么,我如何捕获页面上的错误而不让其在应用程序中出现?
捕获页面级别错误的最佳方式是向导航代码添加错误处理。 下面对此进行演示。
导航级别的错误处理
在上一个测试中,我运行 app.onerror 事件处理程序对页面级别错误进行处理,应用程序似乎停留在主页上。 然而,出于某种原因,出现了“Back”按钮控件。 当我单击“Back”按钮时,转到了(已禁用的)handlingErrors.html 页面。
这是因为,即使在页面已停止的情况下,导航布局应用程序项目模板中提供的 navigator.js (/js/navigator.js) 中的导航代码仍尝试导航到该页面。 并且,它还会导航回主页,将容易出错的页面添加到导航历史记录中。 这就是我在尝试导航到 handlingErrors.html 之后会在主页上看到“Back”按钮的原因。
为了消除 navigator.js 中的错误,我将 PageControlNavigator._navigating 函数替换为图 6 中的代码。 可以看到,导航函数包含对 WinJS.UI.Pages.render 的调用,此调用返回 Promise 对象。 render 方法尝试从传递给它的 URI 创建新的 PageControl,将其插入到 host 元素中。 因为得到的 PageControl 包含错误,所以返回的 Promise 出现错误。 为了捕获导航期间引发的该错误,我向由该 Promise 对象公开的 then 方法的 onError 参数添加错误处理程序。 这样可以有效捕获该错误,防止其引发 Application.onerror 事件。
图 6 navigator.js 中的 PageControlNavigator._navigating 函数
// Other PageControlNavigator code ...
// Responds to navigation by adding new pages to the DOM.
_navigating: function (args) {
var newElement = this._createPageElement();
this._element.appendChild(newElement);
this._lastNavigationPromise.cancel();
var that = this;
this._lastNavigationPromise = WinJS.Promise.as().then(function () {
return WinJS.UI.Pages.render(args.detail.location, newElement,
args.detail.state);
}).then(function parentElement(control) {
var oldElement = that.pageElement;
// Cleanup and remove previous element
if (oldElement.winControl) {
if (oldElement.winControl.unload) {
oldElement.winControl.unload();
}
oldElement.winControl.dispose();
}
oldElement.parentNode.removeChild(oldElement);
oldElement.innerText = "";
},
// Display any errors raised by a page control,
// clear the backstack, and cancel the error.
function (err) {
var messageDialog =
new Windows.UI.Popups.MessageDialog(
err.message,
"Sorry, can't navigate to that page.");
messageDialog.showAsync()
nav.history.backStack.pop();
return true;
});
args.detail.setPromise(this._lastNavigationPromise);
},
// Other PageControlNavigator code ...
WinJS 中的 Promise
其他很多地方已讨论过创建 Promise 和链接 Promise(及其最佳做法)的问题,因此,我在本文中会略过对这个问题的讨论。 如果需要了解更多信息,请参阅 Kraig Brockschmidt 的博客文章 (bit.ly/1cgMAnu) 或者他的免费电子书《Programming Windows Store Apps with HTML, CSS, and JavaScript, Second Edition》(bit.ly/1dZwW1k) 的附录 A。
请注意,修改 navigator.js 是完全正确的。 尽管它是 Visual Studio 项目模板提供的,但它是应用程序代码的一部分,可根据需要进行修改。
在 _navigating 函数中,我向最终的 promise.then 调用添加错误处理程序。 该错误处理程序显示一个消息对话框(与应用程序级别的错误处理一样),然后通过返回 true 取消该错误。 它还从导航历史记录中删除该页面。
当我再次运行该示例并导航至 handlingErrors.html 时,我看到一个消息对话框,通知我导航尝试已失败。 没有出现应用程序级别错误处理的消息对话框。
跟踪异步链中的错误
在 JavaScript 中生成应用程序时,我常常需要用一个异步任务跟踪另一个异步任务,我通过创建 Promise 链来解决这个问题。 链接的 Promise 沿各个任务连续移动,即使链中的一个 Promise 返回错误时也如此。 一种最佳做法是,总是结束含有对 done 方法的调用的 Promise 链。 done 函数引发所有在错误处理程序中为任何之前的 then 语句捕获的错误。 这意味着,我不需要为链中的每个 promise 定义 error 函数。
即便如此,在对 promise.done 的调用中捕获错误后,在很长的链中跟踪错误也可能十分困难。 链接的 promise 可能包含多个任务,任何一个任务都可能失败。 我可以在每个任务中设置断点以查看在何处发生错误,但这非常耗费时间。
此时,Visual Studio 2013 就派上用场了。 “任务”窗口(在 Visual Studio 2010 中引入)也已升级,可处理异步 JavaScript 调试。 在“任务”窗口中,您可以在应用程序代码中的任何给定位置查看所有活动任务和已完成任务。
在下一个示例中,我将向解决方案添加一个新页面,演示这个功能强大的工具。 在解决方案中,我在页面文件夹中创建一个名为 chainedAsync 的新文件夹,添加一个名为 chainAsync.html 的新页面控件(用于创建 /pages/chainedAsync/chainedAsync.html 和关联的 .js 和 .css 文件)。
在 chainedAsync.html 中,我在 <section> 标记内插入以下标记:
<!-- ERROR:Clicking this button starts a chain reaction with an error. -->
<p><button id="startChain">Start the error chain</button></p>
<p id="output"></p>
在 chainedAsync.js 中,我将图 7 中 startChain 按钮的单击事件的事件处理程序添加到页面的 ready 方法。
图 7 chainedAsync.js 中 PageControl.ready 函数的内容
startChain.addEventListener("click", function () {
goodPromise().
then(function () {
return goodPromise();
}).
then(function () {
return badPromise();
}).
then(function () {
return goodPromise();
}).
done(function () {
// This *shouldn't* get called
},
function (err) {
document.getElementById('output').innerText = err.toString();
});
});
最后,我在 chainAsync.js 中定义函数 goodPromise 和 badPromise(如图 8 所示),以便它们可在 PageControl 的方法中使用。
图 8 chainAsync.js 中 goodPromise 和 badPromise 函数的定义
function goodPromise() {
return new WinJS.Promise(function (comp, err, prog) {
try {
comp();
} catch (ex) {
err(ex)
}
});
}
// ERROR: This returns an errored-out promise.
function badPromise() {
return WinJS.Promise.wrapError("I broke my promise :(");
}
再次运行示例,导航到“Chained asynchronous”页面,然后单击“Start the error chain.”。在短时等待后,按钮下面会显示消息“I broke my promise :(。
现在,我需要跟踪错误是在何处发生的,确定修复错误的方法。 (显然,在这样精心设计的情形中,我准确知道发生错误的位置。 出于学习目的,我会忘掉 badPromise 已将该错误注入到我的链接 promise 中。)
为了确定链接 promise 在何处出现偏差,我在错误处理程序(在 startChain 按钮的单击处理程序中对 done 的调用中定义的)上放置断点,如图 9 所示。
图 9 chainedAsync.html 中的断点位置
我再次运行同一测试,当我返回到 Visual Studio 时,程序执行已在断点处停止。 接下来,打开“任务”窗口(调试 | 窗口 | 任务),查看哪些任务当前处于活动状态。 结果如图 10 所示。
图 10 Visual Studio 2013 中显示错误的“任务”窗口
开始时,此窗口没有突出显示任何引起错误的内容。 窗口中列了五个任务,都标记为活动状态。 不过,更仔细地查看时,我看到这些活动任务之一是将 promise 错误排队的 Scheduler — 这看起来很有希望(请原谅我的俏皮话)。
(如果您想了解 Scheduler,建议您阅读本系列的下一篇文章,我将在其中讨论 WinJS 中的新 Scheduler API。)
将鼠标悬停在“任务”窗口中的这一行(图 10 中的 ID 120)上时,我得到了该任务的调用堆栈的目标视图。 我看到几个错误处理程序,badPromise 在该调用堆栈的开始位置附近。 双击这一行时,Visual Studio 直接转到 badPromise 中引发了错误的代码行。 在实际情况下,此时会诊断为何 badPromise 引发了错误。
WinJS 在应用程序中可靠的 try-catch-finally 块的上面、下面以及更远的位置提供了多层错误处理。 一个性能良好的应用程序应使用适当程度的错误处理来提供平稳的用户体验。 在本文中,我演示了如何一个应用程序中包含应用程序级别、页面级别和导航级别的错误处理。 我还演示了如何使用 Visual Studio 2013 中的某些新工具来跟踪链接 promise 中的错误。
在本系列的下一篇文章中,我将探讨一些可以改进 Windows 应用商店应用程序性能的方法。 我还将探讨 Web 工作线程、WinJS 2.0 中的新 Scheduler API 以及 WinJS 2.0 中的新 Dispose 模式。
Eric Schmidt 是 Microsoft 的 Windows 开发人员内容团队中的内容开发人员,负责撰写有关 Windows JavaScript 库 (WinJS) 的文章。他以前在 Microsoft Office 部门工作,为 Office 平台的应用程序编写代码示例。工作之余,他与家人在一起,弹奏弦贝司,制作 HTML5 视频游戏,并撰写有关塑料积木玩具的博客文章 (historybricks.com)。
衷心感谢以下 Microsoft 技术专家对本文的审阅:Kraig Brockschmidt 和 Josh Williams
Kraig Brockschmidt 是 Windows Ecosystem 团队的高级程序经理,直接与开发人员社区和核心合作伙伴在构建 Windows 应用商店应用程序领域进行协作。 他是《Programming Windows Store Apps in HTML, CSS, and JavaScript》(现在为第二版)一书的作者,在 http://www.kraigbrockschmidt.com/blog 上分享其他见解。
Josh Williams 是 Windows 开发人员体验团队的软件开发工程师主管。 他和他的团队构建了 Windows JavaScript 库 (WinJS)。