Содержание


Создание мобильных Web-приложений с применением HTML 5

Часть 4. Использование механизма Web Workers для ускорения работы мобильных Web-приложений

Добавьте в HTML 5 многопотоковый JavaScript!

Comments

Серия контента:

Этот контент является частью # из серии # статей: Создание мобильных Web-приложений с применением HTML 5

Следите за выходом новых статей этой серии.

Этот контент является частью серии:Создание мобильных Web-приложений с применением HTML 5

Следите за выходом новых статей этой серии.

29 июня 2010 г.: добавлены ссылки на часть 5 этого цикла в разделах "Об этом цикле статей", "Заключение" и "Ресурсы".

Об этом цикле статей

HTML 5 – чрезвычайно популярная технология, и тому есть уважительная причина. Она обещает стать технологической переломной точкой в процессе переноса настольных приложений в браузер. Она перспективна для традиционных браузеров, но еще больше возможностей обещает браузерам мобильным. Более того, разработчики самых популярных мобильных браузеров уже освоили и реализовали многие важные элементы спецификации HTML 5. В этом цикле из пяти статей мы рассмотрим некоторые из этих новых технологий, которые входят в HTML 5 и могут оказать огромное влияние на разработку мобильных Web-приложений. В каждой статье мы будем разрабатывать какое-нибудь действующее мобильное Web-приложение, демонстрирующее функции HTML 5, которое можно использовать с современными мобильными Web-браузерами вроде тех, что работают на устройствах iPhone и Android.

В этой статье мы разработаем Web-приложение с использованием новейших Web-технологий. Большая часть кода – это просто HTML, JavaScript и CSS – основные технологии любого Web-разработчика. Главное, что вам понадобится – это браузеры для тестирования. Большая часть кода из этой статьи будет работать с последними версиями настольных браузеров – за некоторыми редкими исключениями. Конечно, приложение нужно будет протестировать и на мобильных браузерах, так что понадобятся последние версии SDK для iPhone и Android. В этой статье используются iPhone SDK 3.1.3 и Android SDK 2.1. В примере для этой статьи применяется также прокси-сервер для доступа из браузера к удаленным службам. Прокси-сервер – это простой сервлет Java™, но его легко заменить прокси-сервером, написанном на PHP, Ruby или другом языке. См. ссылки в разделе Ресурсы.

Многопотоковый JavaScript на мобильных устройствах

Для большинства разработчиков в многопотоковом, или параллельном программировании нет ничего нового. Оно так или иначе поддерживается в большинстве современных языков. Однако JavaScript в их число не входит. Его создатель счел это слишком сложным и ненужным для языка, предназначенного для решения простых задач в пределах Web-страницы. Но Web-страницы превратились в Web-приложения, и уровень сложности задач, решаемых с помощью JavaScript, поднялся настолько, что вывел JavaScript на один уровень с другими языками. В то же время разработчики, использующие языки, которые поддерживают параллельное программирование, часто жалуются на чрезвычайную сложность элементов параллельного программирования, таких как потоки и мьютексы. В последнее время появился ряд новых языков, таких как Scala, Clojure, и F#, которые обещают упростить распараллеливание потоков.

Спецификация Web Worker – это не только добавление параллелизма в JavaScript и Web-браузеры; речь идет о том, чтобы расширить возможности разработчиков, не вынуждая их использовать инструмент, который может вызвать проблемы. Например, разработчики настольных приложений уже много лет используют многопоточность, обеспечивая доступ к ресурсам ввода-вывода без замораживания пользовательского интерфейса в ожидании освобождения этих ресурсов. Однако если эти потоки изменяют какие-нибудь общие ресурсы (в том числе пользовательский интерфейс), приложение часто замирает или зависает. В случае Web Workers этого происходить не должно. Порожденный поток не имеет доступа к тем же ресурсам, что и основной поток пользовательского интерфейса. На самом деле, код порожденного потока даже не может оказаться в том же файле, что и код, выполняемый основным потоком пользовательского интерфейса.

В рамках конструктора необходимо создать внешний файл, как показано в листинге 1.

В этом процессе используются три вещи:

  1. JavaScript Web-страницы (будем называть его сценарием страницы), который исполняется в основном потоке.
  2. Объект Worker, то есть объект JavaScript, который используется для выполнения функций Web Worker.
  3. Сценарий, который будет выполняться вновь порожденным потоком. Будем называть его сценарием Worker.

Итак, рассмотрим сценарий страницы, представленный в листинге 1.

Листинг 1. Использование Web Worker в сценарии страницы
var worker = new Worker("worker.js");
worker.onmessage = function(message){
    // выполнить что-то
};
worker.postMessage(someDataToDoStuffWith);

В нем видны три основных шага по использованию Web Workers. Сначала создается объект Worker, и этому объекту передается URL сценария, который будет выполняться в новом потоке. Весь код, который будет выполнять Worker, должен содержаться в сценарий Worker, URL которого передан конструктору Worker. URL этого сценария Worker ограничен все той же исходной политикой браузера – он должен исходить из того же домена, откуда загружен сценарий страницы, создавший Web Worker.

Затем нужно указать функцию обработчика обратного вызова с использованием функции onmessage. Это функция обратного вызова активизируется после выполнения сценария Worker. message – это данные, возвращаемые сценарием Worker, и с этими данными можно делать все, что угодно. Функция обратного вызова выполняется в главном потоке, поэтому он должен иметь доступ к DOM. Сценарий Worker работает в другом потоке и не имеет доступа к DOM, так что данные из сценария Worker нужно возвращать в основной поток, где можно безопасно изменять DOM для редактирования пользовательского интерфейса приложения. Это ключевая особенность архитектуры Web Workers, основанной на принципе "невмешательства".

Последняя строка листинга 1 иллюстрирует, как инициируется Worker – путем вызова функции postMessage. Здесь в Worker передается сообщение (опять же, это просто данные). Конечно, postMessage – это асинхронная функция; вы вызываете ее, и она сразу же откликается.

Теперь, рассмотрим сценарий Worker. Код листинга 2 – это содержание файла worker.js из листинга 1.

Листинг 2. Сценарий Worker
importScripts("utils.js");
var workerState = {};
onmessage = function(message){
     workerState = message.data;
      // сделать что-то с сообщением
    postMessage({responseXml: this.responseText});
}

Как видите, сценарий Worker имеет собственную функцию onmessage. Она активизируется при вызове postMessage из основного потока. Данные, переданные из сценария страницы, направляются в функцию postMessage объекта message. Для обращения к данным извлекаем свойство data объекта message. После завершения обработки данных в сценарии Worker вызываем функцию postMessage для передачи данных обратно в основной поток. Основной поток может получить эти данные и через свойство data принимаемого им сообщения.

Пока мы рассмотрели простую, но мощную семантику Web Workers. Теперь вы увидите, как применить ее для ускорения работы мобильных Web-приложений. Но перед этим необходимо поговорить о поддержке устройств. Ведь это мобильные Web-приложения, и при их разработке важно преодолеть различия между браузерами.

Поддержка устройств

Начиная с Android 2.0, браузер Android полностью поддерживает спецификацию Web Worker HTML 5. На момент написания этой статьи большинство новых Android-устройств, включая чрезвычайно популярный Motorola Droid, оснащено версией Android 2.1. Кроме того, эта функция полностью поддерживаются в браузере Mozilla Fennec, устанавливаемым на устройства Nokia с операционной системой Maemo и на устройства Windows Mobile. Заметным исключением является iPhone. iPhone OS версий 3.1.3 и 3.2 (версии ОС, которые работают на IPad) пока не поддерживают Web Workers. Однако эта функция уже поддерживается в Safari, и ее появление на браузере Mobile Safari, с которой работает iPhone, – просто вопрос времени. Учитывая доминирование iPhone (особенно в США), лучше не полагаться на присутствие Web Workers и использовать их только для улучшения работы мобильных Web-приложений, когда это присутствие обнаружено. Итак, посмотрим, как можно использовать Web Workers для ускорения работы мобильного Web-приложения.

Повышение производительности с помощью Workers

Поддержка Web Worker на браузерах смартфонов достаточно хороша и постоянно улучшается. Таким образом, встает вопрос: в каких случаях следует использовать Workers в мобильных Web-приложениях? Ответ прост: всякий раз, когда нужно сделать что-то, что занимает много времени. Есть примеры использования Workers для выполнения интенсивных математических вычислений, таких как расчет числа "пи" с точностью до десятитысячных. Маловероятно, что вам когда-нибудь понадобится выполнять такие вычисления в Web-приложении, тем более мобильном. Однако извлечение данных из удаленных ресурсов применяется довольно часто, и для этой статьи выбран пример, решающий именно такую задачу.

В этом примере мы получим список Daily Deals (ежедневные предложения) с аукциона eBay. Список содержит краткую информацию о каждом предложении. Более подробную информацию можно получить с помощью API eBay Shopping. Воспользуемся Web Workers для извлечения этой дополнительной информации, пока пользователь просматривает список предложений, выбирая наиболее интересные. Чтобы получить доступ ко всем этим данным eBay из Web-приложения, нужно воспользоваться все той же исходной политикой браузера с помощью общего прокси-сервера. Для данного прокси-сервера использовался простой сервлет Java. Он входит в состав кода для этой статьи, и здесь мы его не рассматриваем. Давайте лучше сосредоточимся на коде, который работает с Web Workers. В листинге 3 показана базовая HTML-страница приложения.

Листинг 3. Код HTML приложения для работы с аукционом
<!DOCTYPE HTML>
<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
    <meta name = "viewport" content = "width = device-width">
    <title>Worker Deals</title>
    <script type="text/javascript" src="common.js"></script>
  </head>
  <body onload="loadDeals()">
    <h1>Deals</h1>
    <ol id="deals">
    </ol>
    <h2>More Deals</h2>
    <ul id="moreDeals">
    </ul>
  </body>
</html>

Как видно, это очень простой код HTML, просто оболочка. Мы извлекаем данные и создаем пользовательский интерфейс с помощью JavaScript. Это оптимальный дизайн мобильного Web-приложения, так как он позволяет помещать весь код и статическую разметку в кэш на устройстве, и пользователь ожидает только данные с сервера. Заметьте, что в листинге 3, как только загружено тело, мы вызываем функцию loadDeals, которая загружает исходные данные для приложения, как показано в листинге 4.

Листинг 4. Функция loadDeals
var deals = [];
var sections = [];
var dealDetails = {};
var dealsUrl = "http://deals.ebay.com/feeds/xml";
function loadDeals(){
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function(){
        if (this.readyState == 4 && this.status == 200){
               var i = 0;
               var j = 0;
               var dealsXml = this.responseXML.firstChild;
               var childNode = {};
               for (i=0; i< dealsXml.childNodes.length;i++){
                   childNode = dealsXml.childNodes.item(i);
                   switch(childNode.localName){
                   case 'Item': 
                       deals.push(parseDeal(childNode));
                       break;
                   case "MoreDeals":
                       for (j=0;j<childNode.childNodes.length;j++){
                           var sectionXml= childNode.childNodes.item(j);
                           if (sectionXml && sectionXml.hasChildNodes()){
                               sections.push(parseSection(sectionXml));
                           }
                       }
                       break;    
                   default:
                       break;
                   }
               }
               deals.forEach(function(deal){
                   var entry = createDealUi(deal);
                   $("deals").appendChild(entry);
               });
               loadDetails(deals);
               sections.forEach(function(section){
                   var ui = createSectionUi(section);
                   $("moreDeals").appendChild(ui);
                   loadDetails(section.deals);
               });
        }
    };
    xhr.open("GET", "proxy?url=" + escape(dealsUrl));
    xhr.send(null);
}

В листинге 4 показана функция loadDeals, а также глобальные переменные, используемые в приложении. Мы используем массив предложений и массив разделов. Это дополнительные группы взаимосвязанных предложений (например, предложения стоимостью до $10). Есть также отображение dealDetails, ключами которого будут идентификаторы Item (мы будем получать их из данных по предложениям), а значениями – более подробная информация, полученная от API eBay Shopping.

Первое, что необходимо сделать, это Ajax-вызов, обращенный к прокси-серверу, который, в свою очередь, вызывает REST-API eBay Daily Deals. Результатом становится список предложений в виде XML-документа. Документ анализируется в функции onreadystatechange объекта XMLHttpRequest, который использовался для Ajax-вызова. Две другие функции, parseDeal и parseSection, используются для разложения XML-узлов на простые в применении объекты JavaScript. Эти функции присутствуют в загрузке примера кода (см. раздел Загрузки), но так как они выполняют лишь рутинные операции анализа XML, мы их здесь не рассматриваем. Наконец, после разбора XML можно использовать еще две функции для модификации DOM, createDealUi и createSectionUi. Когда все готово, интерфейс выглядит как показано на рисунке 1.

Рисунок 1. Интерфейс пользователя Mobile Deals
Снимок экрана пользовательского интерфейса  мобильного предложения с примерами предложений, включающий кнопку Показать детали для каждого предложения
Снимок экрана пользовательского интерфейса мобильного предложения с примерами предложений, включающий кнопку Показать детали для каждого предложения

Возвращаясь к листингу 4, обратите внимание, что после первоначальной загрузки предложений мы вызываем функцию loadDetails для каждого из разделов с предложениями. Эта функция загружает дополнительную информацию по каждому предложению с помощью API eBay Shopping – но только если браузер поддерживает Web Workers. В листинге 5 показана функция loadDetails.

Листинг 5. Предварительная выборка деталей предложения
function loadDetails(items){
    if (!!window.Worker){
        items.forEach(function(item){
            var xmlStr = null;
            if (window.localStorage){
                xmlStr = localStorage.getItem(item.itemId);
            }
            if (xmlStr){
                var itemDetails = parseFromXml(xmlStr);
                dealDetails[itemDetails.id] = itemDetails;
            } else {
                var worker = new Worker("details.js");
                worker.onmessage = function(message){
                    var responseXmlStr =message.data.responseXml;
                    var itemDetails=parseFromXml(responseXmlStr);
                    if (window.localStorage){
                        localStorage.setItem(
                                        itemDetails.id, responseXmlStr);
                    }
                    dealDetails[itemDetails.id] = itemDetails;
                };
                    worker.postMessage(item.itemId);
            }
        });
    }
}

В loadDetails мы сначала проверяем функцию Worker в глобальном масштабе (объект window.) Если ее там нет, просто ничего не делаем. Если она есть, то в первую очередь проверяем localStorage в коде XML деталей данного предложения. Это стратегия локального кэширования, общепринятая для мобильных Web-приложений, которая подробно описана во второй части настоящего цикла статей (см. ссылку в разделе Ресурсы).

Если XML найден локально, он анализируется функцией parseFromXml, и детали добавляются в объект dealDetails. Если он не найден, то создаем Web Worker и передаем ему ID элемента предложения с помощью postMessage. Как только Worker получает данные и возвращает их в основной поток, мы анализируем XML, добавляем результат в dealDetails и сохраняем XML в localStorage. В листинге 6 показан сценарий Worker, details.js.

Листинг 6. Сценарий извлечения деталей предложения с помощью Worker
importScripts("common.js");
onmessage = function(message){
    var itemId = message.data;
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function(){
        if (this.readyState == 4 && this.status == 200){
            postMessage({responseXml: this.responseText});
        }
    };
    var urlStr = generateUrl(itemId);
    xhr.open("GET", "proxy?url=" + escape(urlStr));
    xhr.send(null);
}

Сценарий Worker довольно прост. Мы используем Ajax для вызова прокси, который в свою очередь вызывает API eBay Shopping. Получив XML от прокси-сервера, мы возвращаем его в основной поток с использованием литерала объекта JavaScript. Отметим, что, несмотря на возможность использования XMLHttpRequest из Worker, все возвращается в его свойство responseText, а не в свойство responseXml. Это потому, что в сферу действия сценария Worker не входит анализатор JavaScript DOM. Отметим, что функция generateUrl поступает из файла common.js (см. листинг 7). Common.js импортируется с помощью функции importScripts.

Листинг 7. Сценарий, импортированный Worker
function generateUrl(itemId){
    var appId = "YOUR APP ID GOES HERE";
    return "http://open.api.ebay.com/shopping?callname=GetSingleItem&"+
        "responseencoding=XML&appid=" + appId + "&siteid=0&version=665"
            +"&ItemID=" + itemId;
}

Теперь, когда мы увидели, как заполнить детали предложения (для браузеров, которые поддерживают Web Workers), вернемся к рисунку 1, чтобы посмотреть, как это применяется в приложении. Обратите внимание, что каждое предложение снабжено кнопкой Show Details (Показать детали). При ее нажатии интерфейс пользователя изменяется, как показано на рисунке 2.

Рисунок 2. Отображение подробной информации
Снимок экрана деталей предложения с описанием, фотографиями и ценой двух пакетов Golla
Снимок экрана деталей предложения с описанием, фотографиями и ценой двух пакетов Golla

Этот пользовательский интерфейс отображается при вызове функции showDetails. Эта функция показана в листинге 8.

Листинг 8. Функция ShowDetails
function showDetails(id){
    var el = $(id);
    if (el.style.display == "block"){
        el.style.display = "none";
    } else {
        el.style.display = "block";
        if (!el.innerHTML){
            var details = dealDetails[id];
            if (details){
                var ui = createDetailUi(details);
                el.appendChild(ui);
            } else {
                var itemId = id;
                var xhr = new XMLHttpRequest();
                xhr.onreadystatechange = function(){
                    if (this.readyState == 4 && 
                                      this.status == 200){
                        var itemDetails = 
                                        parseFromXml(this.responseText);
                        if (window.localStorage){
                            localStorage.setItem(
                                              itemDetails.id, 
                                              this.responseText);
                        }
                        dealDetails[id] = itemDetails;
                        var ui = createDetailUi(itemDetails);
                        el.appendChild(ui);
                    }
                };
                var urlStr = generateUrl(id);
                xhr.open("GET", "proxy?url=" + escape(urlStr));
                xhr.send(null);                        
            }
        }
    }
}

Мы получили идентификатор предложения, которое будет показано, и переключатель, указывающий, показывать его или нет. При первом вызове функция проверяет, хранятся ли уже детали в отображении dealDetails. Если браузер поддерживает Web Workers, то эти данные уже присутствуют, и пользовательский интерфейс для них создан и добавлен в DOM. Если детали еще не загружены, или если браузер не поддерживает Workers, мы делаем Ajax-вызов, чтобы загрузить эти данные. Так приложение может работать одинаково хорошо независимо от наличия поддержки Workers. Если Workers поддерживается, то данные уже загружены и пользовательский интерфейс отреагирует мгновенно. Если нет, интерфейс все равно загрузится, но на это потребуется несколько секунд.

Заключение

Для Web-разработчиков Web Workers звучит как новая экзотическая технология. На самом же деле, как видно из этой статьи, она имеет вполне практическое применение. Она особенно актуальна для мобильных Web-приложений. Workers можно использовать для предварительной выборки данных или других предварительных операций, чтобы обеспечить гораздо более отзывчивый пользовательский интерфейс. Это может быть особенно важно для мобильных Web-приложений, которым нужно загружать данные по потенциально медленной сети. Добавьте к этому стратегии кэширования, и ваши пользователи будут поражены проворностью вашего приложения.


Ресурсы для скачивания


Похожие темы


Комментарии

Войдите или зарегистрируйтесь для того чтобы оставлять комментарии или подписаться на них.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=XML, Open source
ArticleID=793261
ArticleTitle=Создание мобильных Web-приложений с применением HTML 5: Часть 4. Использование механизма Web Workers для ускорения работы мобильных Web-приложений
publish-date=06292010