Опубликован: 02.04.2013 | Уровень: для всех | Доступ: свободно
Лекция 4:

Анатомия приложения и навигация по страницам

Последовательные асинхронные операции: объединение отложенных результатов в цепочку

В предыдущем примере кода вы могли заметить, как мы передаем информацию об исключениях, когда мы не получаем ожидаемый результат в любой из асинхронных операций. Кроме того, у нас есть только один обработчик ошибок в конце конструкции, у нас имеется эта странная конструкция, возвращающая результат (promise-объект, отложенный результат) от каждой последующей асинхронной операции вместо того, чтобы обрабатывать promise-объекты то тут, то там.

Хотя, на первый взгляд это может выглядеть странным, подобный подход, на самом деле, является наиболее распространённым шаблоном для работы с последовательными асинхронными операциями, так как он работает лучше, чем более очевидный подход с использованием вложенных конструкций. Вложенность подразумевает вызов следующего асинхронного API внутри обработчика завершения предыдущего, каждый из них завершается оператором done. Вот как код из предыдущего примера может быть переписан с использованием такого подхода (посторонний код удалён для упрощения примера):

captureUI.captureFileAsync(Windows.Media.Capture.CameraCaptureUIMode.photo)
.done(function (capturedFileTemp) {
//...
local.createFolderAsync("HereMyAm", ...)
.done(function (myFolder) {
//...
capturedFile.copyAsync(myFolder, newName)
.done(function (newFile) {
})
})
});

Единственное преимущество такого подхода заключается в том, что каждый обработчик завершения будет иметь доступ ко всем переменным, объявленным ранее. А вот недостатков у него довольно много. С одной стороны, здесь, между асинхронными вызовами, обычно достаточно много промежуточного кода, что делает структуру выглядящей неопрятно. Важнее то, что обработка ошибок значительно усложняется. Когда promise-вызовы вложены друг в друга, обработку ошибок нужно проводить на каждом из уровней. Если ошибка возникнет на одном из внутренних уровней, обработчик на внешнем уровне не сможет её обработать. Таким образом, каждый promise-вызов нуждается в собственном обработчике ошибок, что превращает базовую структуру подобного кода в нечто, напоминающее спагетти:

captureUI.captureFileAsync(Windows.Media.Capture.CameraCaptureUIMode.photo)
.done(function (capturedFileTemp) {
//...
local.createFolderAsync("HereMyAm", ...)
.done(function (myFolder) {
//...
capturedFile.copyAsync(myFolder, newName)
.done(function (newFile) {
},
function (error) {
})
},
function (error) {
});
},
function (error) {
});

Не знаю, как вы, а я теряюсь во всех этих } и ) (хотя очень старался вспомнить занятия по LISP в колледже), здесь непросто увидеть, какая функция обработки ошибок применяется к конкретному асинхронному вызову. Объединение promise-вызовов в цепочку решает все эти проблемы, в обмен на небольшую плату в виде необходимости объявления нескольких дополнительных переменных за пределами цепочки. При объединение в цепочку, вы выполняете команду return для следующего promise-объекта в каждом из обработчиков завершения, вместо того, чтобы дополнять его вызовом done. Это позволяет вам выравнивать все асинхронные вызовы лишь однажды, и даёт эффект распространения ошибок по цепочке. Когда в promise-вызове произошла ошибка, вы видите, что то, что возвращается, является promise-объектом, и если вы вызовете метод then (но не done - смотрите следующий раздел), снова будет возвращён другой promise-объект, содержащий ошибку. В результате, любые ошибки быстро доходят по цепочке до первого доступного обработчика ошибок, что позволяет вам иметь лишь один обработчик ошибок в самом конце:

captureUI.captureFileAsync(Windows.Media.Capture.CameraCaptureUIMode.photo)
.then(function (capturedFileTemp) {
//...
return local.createFolderAsync("HereMyAm", ...);
})
.then(function (myFolder) {
//...
return capturedFile.copyAsync(myFolder, newName);
})
.done(function (newFile) {
},
function (error) {
})

Для моих глаз (и моего ума) эта структура кода кажется более чистой - и такой код легче отлаживать и поддерживать. Если хотите, вы даже можете завершить цепочку вызовом done(null, errorHandler), заменив предыдущий done на then:

captureUI.captureFileAsync(Windows.Media.Capture.CameraCaptureUIMode.photo)
//...	
.then(function (newFile) {	
})	
.done(null, function (error) {	
})	
})	

И, наконец, немного об отладке promise-вызовов, объединенных в цепочку (или вложенных, если уж на то пошло). Каждый шаг включает асинхронную операцию, поэтому вы не можете просто применить пошаговое исполнение, как это делается с синхронным кодом (в противном случае, вы окажетесь глубоко в WinJS). Вместо этого, установите точку останова на первой строке внутри каждого обработчика завершения и на первой строке функции обработки ошибок, которая находится в конце. Когда каждая из точек останова сработает, вы можете пошагово исполнить обработчик завершения. Когда вы дойдёте до следующего асинхронного вызова, нажмите на кнопку Далее (Continue) в Visual Studio, в итоге сможет выполниться асинхронная операция, после которой сработает точка останова в следующем обработчике завершения (или точка останова в обработчике ошибок).

Обработка ошибок внутри promise-вызовов: then против done

Хотя обрабатывать ошибки в конце цепочки promise-вызовов - это обычная практика, как показано в коде выше, вы можете использовать обработчик ошибок в любом месте цепочки - и then, и done принимают одинаковые аргументы. Если на данном уровне возникает исключение, оно будет обработано ближайшим внутренним обработчиком ошибок.

Это приводит нас к различию между then и done. Во-первых, then возвращает еще один promise-объект, тем самым позволяя объединять команды в цепочки, в то время как done возвращает undefined, поэтому он должен быть в конце цепочки. Во-вторых, если исключение возникает внутри асинхронной операции с методом then и на данном уровне нет обработчика ошибок, информация об ошибке сохраняется в promise-объекте, который возвращает then. В противоположность этому, если done видит исключение и не имеет обработчика ошибок, он направляет исключение в цикл событий (event loop) приложения. Оно обходит любые локальные (синхронные) блоки try/catch, хотя вы можете перехватить их с помощью обработчиков WinJS.Application.onerror и window.onerror. (Последний получит ошибку, если первый её не обработал). Если вы этого не сделаете, приложение будет остановлено и информация об ошибке будет отправлена в Магазин Windows и появится на информационной панели. По этой причине мы рекомендуем, чтобы вы реализовывали обработчик WinJS.Application.onerror.

На практике, это означает, что если вы завершите цепочку promise-вызовов then, а не done, все исключения в цепочке будут потеряны и вы никогда не узнаете о проблеме. Это может привести приложение к неопределенному состоянию и привести к большим проблемам позднее. В итоге, если только вы не собираетесь передавать последний promise-объект в цепочке в другой участок кода, в котором будет вызвана команда done (вы можете так поступить, если пишете библиотеку, которая возвращает promise-объект), всегда используйте done в конце цепочки, даже для единичной асинхронной операции3Множество примеров в Windows SDK используют then вместо done, особенно для одиночных асинхронных операций. Это так, потому что done до определенного момента просто не существовал и эти примеры не всегда обновлены до текущего состояния дел..

С promise-объектами вы можете делать и многое другое, кстати, вроде комбинирования их, отмены и так далее. Мы вернемся к этому в конце данной лекции.

Тестовый вывод, отчёты об ошибках и средство просмотра событий

Когда идёт разговор об исключениях и обработке ошибок, разработчиков обычно огорчает то, что команды window.prompt и window.alert не доступны приложениям для Магазина Windows в качестве средств быстрой отладки. К счастью, у вас есть два других хороших инструмента для этих целей. Первый - это Windows.UI.Popups.MessageDialog, который обычно используется для того, чтобы выводить предупреждения пользователям. Второй - это console.log, как показано ранее, отправляющий текст в панель вывода Visual Studio. Эти сообщения, кроме того, можно записать в качестве лог-файла событий Windows, как мы скоро увидим4Для тех читателей, которые серьезно относятся к регистрации событий в лог-файлах, после игр с бензопилами, посмотрите функции startLog (http://msdn.microsoft.com/en-us/library/windows/apps/hh701617.aspx), stopLog (http://msdn.microsoft.com/en-us/library/windows/apps/hh701626.aspx) и formatLog (http://msdn.microsoft.com/en-us/library/windows/apps/hh701587.aspx) из пространства имен WinJS.Utilities (http://msdn.microsoft.com/en-us/library/windows/apps/br229783.aspx), которые обеспечивают дополнительную функциональность на базе console.log. Думаю, вы сможете узнать об этом из документации, однако, считаю важным довести информацию о них до вашего сведения..

Другая функция DOM API, которой вам может захотеться воспользоваться - это window.close. Вы можете пользоваться ей во время разработки, но в опубликованном приложении Windows воспринимает её как аварийное завершение программы и генерирует в ответ отчёт об ошибке. Этот отчёт появится в информационной панели Магазина Windows для вашего приложения, с сообщением о том, что вам не следует пользоваться данной функцией! В конце концов, приложения для Магазина Windows не должны предоставлять собственных средств для закрытия приложения, как описано в разделе 3.6. сертификационных требований.

Однако, возможна ситуация, когда опубликованное приложение должно завершить работу в ответ на неисправимое происшествие. Хотя вы можете использовать для этого команду window.close, лучше воспользоваться командой MSApp.terminateApp, так как она позволяет вам, кроме того, включить информацию о произошедшей ошибке. Эти подробности будут показаны в информационной панели Магазина Windows, упрощая диагностику проблемы.

В дополнение к информационной панели Магазина Windows, вам следует ознакомиться со средством просмотра событий Windows (Windows Event Viewer)5Если вы не можете найти Просмотр событий, нажмите клавишу Windows для перехода на Начальный экран и воспользуйтесь чудо-кнопкой Параметры. Выберите раздел Плитки и активируйте параметр Показать средства администрирования. После этого вы увидите плитку для запуска Просмотра событий на Начальном экране.. Это место, где могут быть записаны отчёты об ошибках, консольные журналы и сведения о необработанных исключениях (которые завершают приложения без предупреждения).

Для того, чтобы активировать эту возможность, для начала вам нужно перейти к разделу Журналы приложений и служб, развернуть ветку Microsoft > Windows > AppHost, щёлкнуть левой кнопкой мыши по элементу Администратор (Admin) для того, чтобы выделить его (это важно), после чего щёлкнуть по элементу Администратор правой кнопкой мыши и выбрать Вид > Отобразить аналитический и отладочный журналы (View > Show Analytic and Debug logs) для включения вывода полной информации, как показано на Рис. 3.4. Это включит отслеживание ошибок и исключений. Затем щёлкните правой кнопкой пункт AppTracing (так же в разделе AppHost) и выберите пункт Включить журнал (Enable Log). Это позволит отслеживать ваши вызовы console.log, так же, как и другую диагностическую информацию, поступающую от хост-процесса приложения.

События хост-процесса приложения, такие, как необработанные исключения и ошибки загрузки, можно найти в средстве просмотра событий

увеличить изображение
Рис. 3.4. События хост-процесса приложения, такие, как необработанные исключения и ошибки загрузки, можно найти в средстве просмотра событий

Мы уже говорили о диалоговом окне Исключения в Visual Studio в "Быстрый старт" . Вернитесь к Рис. 2.16. Для каждого типа JavaScript-исключений это диалоговое окно предоставляет два флага, названные Вызванное (Thrown) и Не обработанное пользовательским кодом (User-unhandled). Установка флага Вызванное приведет к отображению диалогового окна в отладчике (Рис. 3.5), когда будет вызвано исключение, независимо от того, было ли оно обработано и до того, как сработают любые из ваших обработчиков событий. Если у вас есть обработчики событий, вы можете без проблем нажать на кнопку Продолжить (Continue) в диалоговом окне и ошибка будет передана вашим обработчикам ошибок. (В противном случае работа приложения завершится). Если вы, вместо этого нажмёте на кнопку Остановить отладку (Break), вы можете увидеть подробности об исключении в окне Локальные (Locals), как показано на Рис. 3.6.

Диалоговое окно исключения в Visual Studio. Как показывает диалоговое окно, можно безопасно нажать на кнопку Продолжить (Continue), если в вашем приложении есть обработчик ошибок; в противном случае работа приложения завершится. Обратите внимание на флаг в этом окне, который позволяет быстро включить опцию Вызванное (Thrown) для данного типа исключений в диалоговом окне Исключения

Рис. 3.5. Диалоговое окно исключения в Visual Studio. Как показывает диалоговое окно, можно безопасно нажать на кнопку Продолжить (Continue), если в вашем приложении есть обработчик ошибок; в противном случае работа приложения завершится. Обратите внимание на флаг в этом окне, который позволяет быстро включить опцию Вызванное (Thrown) для данного типа исключений в диалоговом окне Исключения
Информация в окне Visual Studio Локальные (Locals), когда вы останавливаете работу приложения при исключении

увеличить изображение
Рис. 3.6. Информация в окне Visual Studio Локальные (Locals), когда вы останавливаете работу приложения при исключении

Опция Не обработанное пользовательским кодом (User-unhandled) (включенная для всех типов исключений по умолчанию) отобразит похожее диалоговое окно всякий раз, когда исключение попадёт в цикл событий, показывая то, что оно не было обработано в функции обработки ошибок приложения (в "пользовательском" коде - с точки зрения системы).

Обычно вы включаете параметр Вызванное только для тех исключений, которые вы собираетесь обрабатывать. Включение их всех может привести к сложностям при работе с приложением. Вы можете сделать это в качестве эксперимента и затем оставить данный параметр установленным лишь для тех исключений, которые вы ожидаете перехватить. Оставьте параметр Не обработанное пользовательским кодом для всех остальных исключений. На самом деле, если у вас нет особых причин не делать этого, убедитесь в том, что параметр Не обработанное пользовательским кодом включен у группы Ошибки времени выполнения JavaScript (JavaScript Runtime Exceptions), так как эта установка включает в себя все исключения, даже не перечисленные. При таком подходе вы можете перехватить (и исправить) любое исключение, которое может внезапно завершить работу приложения, а это кое-что из того, с чем вашим пользователям никогда не следует сталкиваться.

Юрий Макушин
Юрий Макушин
Россия, Москва, РЭА им. Плеханова, 2004