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

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

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

Майкл Галпин, инженер по программному обеспечению, Vitria Technology

Майкл Галпин (Michael Galpin) имеет учёную степень по математике в Калифорнийском Технологическом институте. Он является Java-разработчиком с конца 90-х гг. и работает инженером по программному обеспечению в Vitria Technology, в Саннивейл, Калифорния.



29.06.2010 (Впервые опубликовано 13.02.2012)

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#, которые обещают упростить распараллеливание потоков.

Часто используемые сокращения

  • Ajax: Asynchronous JavaScript + XML
  • API: Application Programming Interface – интерфейс программирования приложений
  • CSS: Cascading stylesheet – каскадная таблица стилей
  • DOM: Document Object Model – объектная модель документов
  • HTML: Hypertext Markup Language – язык гипертекстовой разметки
  • REST: Representational State Transfer – передача репрезентативного состояния
  • SDK: Software Developer Kit – пакет ПО разработчика
  • UI: User Interface – пользовательский интерфейс
  • URL: Uniform Resource Locator – унифицированный указатель ресурсов
  • W3C: World Wide Web Consortium
  • XML: Extensible Markup Language – расширяемый язык разметки гипертекста

Спецификация 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

Этот пользовательский интерфейс отображается при вызове функции 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-приложений, которым нужно загружать данные по потенциально медленной сети. Добавьте к этому стратегии кэширования, и ваши пользователи будут поражены проворностью вашего приложения.


Загрузка

ОписаниеИмяРазмер
Исходный код для статьиWorkers.zip8 КБ

Ресурсы

Научиться

Получить продукты и технологии

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


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