22. Прикладные интерфейсы HTML5


    22.1. Геопозиционирование
    22.2. Управление историей посещений
    22.3. Взаимодействие документов с разным происхождением
    22.4. Фоновые потоки выполнения
         22.4.1. Объект Worker
         22.4.2. Область видимости фонового потока
         22.4.3. Примеры использования фоновых потоков
    22.5. Типизированные массивы и буферы
    22.6. Двоичные объекты
         22.6.1. Файлы как двоичные объекты
         22.6.2. Загрузка двоичных объектов
         22.6.3. Конструирование двоичных объектов
         22.6.4. URL-адреса, ссылающиеся на двоичные объекты
         22.6.5. Чтение двоичных объектов
    22.7. Прикладной интерфейс к файловой системе
    22.8. Базы данных на стороне клиента
    22.9. Веб-сокеты

Под термином HTML5 обычно подразумевается последняя версия спецификации языка разметки HTML, но этот термин также используется для обозначения целого комплекса веб-технологий, которые разрабатываются и определяются как часть языка разметки HTML или сопутствующие ему. Официально этот комплекс технологий называется «Open Web Platform». Однако на практике чаще используется сокращенное название «HTML5», и в данной главе мы будем использовать его именно в этом смысле. В других Главах этой книги уже описывались некоторые новые прикладные интерфейсы HTML5:

Особенности, описываемые в этой главе, либо не укладываются естественным образом ни в одну из тем, обсуждавшихся в предыдущих главах, либо пока не являются достаточно стабильными и зрелыми, чтобы обсуждать их в основных Главах этой книги. Некоторые из прикладных интерфейсов выглядят достаточно стабильными, чтобы их можно было описать в справочном разделе, тогда как другие все еще продолжают изменяться и потому не были включены в четвертую часть книги. На тот момент, когда книга была отправлена в печать, все примеры в этой главе, кроме одного (пример 22.9), работали, по крайней мере, в одном из браузеров. Поскольку спецификации, описываемые здесь, все еще продолжают дорабатываться, некоторые из этих примеров могут перестать работать, когда вы будете читать эту главу.


содержание 22.1. Геопозиционирование содержание

Прикладной интерфейс объекта Geolocation (http://www.w3.org/TR/geolocation-API/) позволяет программам на языке JavaScript запрашивать у браузера географическое местонахождение пользователя. Такие приложения могут отображать карты, маршруты и другую информацию, связанную с текущим местонахождением пользователя. При этом, конечно, возникает важная проблема соблюдения тайны частной информации, поэтому браузеры, поддерживающие прикладной интерфейс Geolocation, всегда запрашивают у пользователя подтверждение, прежде чем передать JavaScript-программе информацию о физическом местонахождении пользователя.

Браузеры с поддержкой интерфейса Geolocation определяют свойство navigator.geolocation. Это свойство ссылается на объект с тремя методами:

navigator.geolocation.getCurrentPosition()
Запрашивает текущее географическое местонахождение пользователя.
navigator.geolocation.watchPosition()
Не только запрашивает текущее местонахождение, но и продолжает следить за координатами, вызывая указанную функцию обратного вызова при изменении местонахождения пользователя.
navigator.geolocation.clearWatch()
Останавливает слежение за местонахождением пользователя. В аргументе этому методу следует передавать число, возвращаемое соответствующим вызовом метода watсhPosition().

В устройствах, включающих аппаратную поддержку GPS, имеется возможность определять местонахождение с высокой степенью точности с помощью устройства GPS. Однако чаще информация о местонахождении поступает из Всемирной паутины. Если браузер отправит IP-адрес специализированной веб-службе, она в большинстве случаев сможет определить (на основе информации о поставщиках услуг Интернета), в каком городе находится пользователь (и рекламодатели часто пользуются этой возможностью, реализуя определение местонахождения на стороне сервера). Браузер часто в состоянии получить еще более точную информацию о местонахождении, запросив у операционной системы список ближайших беспроводных сетей и силы их сигналов. Затем эта информация отправляется веб-службе, которая позволяет вычислить местонахождение с большой точностью (обычно с точностью до микрорайона в городе).

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

navigator.geolocation.getCurrentPosition(function(pos) {
 var latitude = pos.coords.latitude;
 var longitude = pos.coords.longitude;
 alert ("Ваши координаты: " + latitude + "', " + longitude);
});

В дополнение к широте и долготе в ответ на каждый успешный запрос возвращается также значение (в метрах), указывающее точность определения местонахождения. Пример 22.1 демонстрирует получение информации о местонахождении: он вызывает метод getCurrentPosition(), чтобы определить текущее местонахождение, и использует полученную информацию для отображения карты (полученной от службы Google Maps) текущего местонахождения в масштабе, примерно соответствующем точности определения местонахождения.

Пример 22.1 (js). Использование информации о местонахождении для отображения карты

// Возвращает вновь созданный элемент <img>, настроенный (в случае успешного
// определения местонахождения) на отображение карты для текущего
// местонахождения. Обратите внимание, что вызывающая программа сама должна
// вставить возвращаемый элемент в документ, чтобы отобразить его. Возбуждает
// исключение, если возможность определения местонахождения не поддерживается
// браузером. 
function getmap() { 
 // Проверить поддержку объекта geolocation 
 if (!navigator.geolocation) throw "Определение местонахождения\
 не поддерживается"; 
 
 // Создать новый элемент <img>, отправить запрос определения местонахождения,
 // чтобы в img отобразить карту местонахождения и вернуть изображение.
 var image = document.createElement("img"); 
 navigator.geolocation.getCurrentPosition(setMapURL); 
 return image; 
 
 // Эта функция будет вызвана после того, как вызывающая программа получит
 // объект изображения, в случае успешного выполнения запроса определения
 // местонахождения.
 function setMapURL(pos) { 
  // Получить информацию о местонахождении из объекта аргумента
  var latitude = pos.coords.latitude; // Градусы к северу от экватора 
  var longitude = pos.coords.longitude; // Градусы к востоку от Гринвича 
  var accuracy = pos.coords.accuracy; // Метры 
 
  // Сконструировать URL для получения статического изображения карты
  // от службы Google Map для этого местонахождения 
  var url = "http:// maps.google.com/maps/api/staticmap" + 
   "?center=" + latitude + "," + longitude + 
   "&size=640x640&sensor=true"; 
   
  // Установить масштаб карты, используя грубое приближение 
  var zoomlevel=20;  // Для начала установить самый крупный масштаб 
  if (accuracy > 80) // Уменьшить масштаб для низкой точности 
   zoomlevel -= Math.round(Math.log(accuracy/50)/Math.LN2); 
  url += "&zoom=" + zoomlevel; // Добавить масштаб в URL 
 
  // Отобразить карту в объекте изображения. Спасибо, Google! 
  image.src = url; 
 } 
} 

Эти дополнительные возможности демонстрируются в примере 22.2.

Пример 22.2 (js). Демонстрация всех возможностей определения местонахождения

// Асинхронно определяет местонахождение и отображает его в указанном элементе,   
function whereami(elt) {
 // Этот объект передается методу getCurrentPosition() в 3 аргументе 
 var options = {
  // Чтобы получить координаты с высокой точностью (например, с устройства GPS), 
  // присвойте этому свойству значение true. Отметьте, однако, что это может 
  // увеличить расход энергии в аккумуляторах. 
  enableHighAccuracy: false, // Приблизительно: по умолчанию 

  // Определите свое значение, если допустимо брать координаты из кэша. 
  // По умолчанию имеет значение 0, что обеспечивает получение самой 
  // свежей информации. 
  maximumAge: 300000, // Пригодна информация, полученная в течение 
                      // последних 5 минут 

  // Предельное время ожидания выполнения запроса. 
  // По умолчанию имеет значение Infinity, что соответствует бесконечному 
  // времени ожидания выполнения запроса вызовом метода getCurrentPosition() 
  timeout: 15000     // Ждать не более 15 секунд 
 };

 if (navigator.geolocation) // Запросить координаты, если возможно 
  navigator.geolocation.getCurrentPosition(success, error, options); 
 else 
  elt.innerHTMl = "Geolocation not supported in this browser";

 // Эта функция будет вызвана в случае неудачного выполнения запроса 
 function error(e) {
  // Объект ошибки содержит числовой код и текстовое сообщение. Коды: 
  // 1: пользователь не дал разрешения на определение местонахождения 
  // 2: браузер не смог определить местонахождение 
  // 3: истекло предельное время ожидания 
  elt.innerHTML = "Ошибка определения местонахождения " + 
				e.code + ": " + e.message;
 }

 // Эта функция будет вызвана в случае успешного выполнения запроса 
 function success(pos) {
  // Эти поля возвращаются всегда. Обратите внимание, что поле timestamp 
  // принадлежит внешнему объекту pos, а не вложенному coords, 
  var msg = "At " +
   new Date(pos.timestamp).toLocaleString() + " вы находились в " + 
   pos.coords.accuracy + " метрах от точки " +
   pos.coords.latitude + " северной широты " + 
   pos.coords.longitude + " восточной долготы.";

  // Если устройство возвращает высоту над уровнем моря, 
  // добавить эту информацию, 
  if (pos.coords.altitude) {
   msg += " Вы находитесь на высоте " + pos.coords.altitude + " ± " +
    pos.coords.altitudeAccuracy + "метров над уровнем моря.";
  }
  
  // Если устройство возвращает направление и скорость движения, 
  // добавить и эту информацию, 
  if (pos.coords.speed) {
   msg += " Вы перемещаетесь со скоростью " + 
    pos.coords.speed + "м/сек в направлении " +
    pos.coords.heading + ".";
  }

  elt.innerHTML = msg; // Отобразить информацию о местонахождении 
 }
}


содержание 22.2. Управление историей посещений содержание

Веб-Браузеры запоминают, какие документы загружались в окно, и предоставляют кнопки Back и Forward, позволяющие перемещаться между этими документами. Эта модель хранения истории посещений в браузерах появилась еще в те дни, когда документы были статическими и все вычисления выполнялись на стороне сервера. В настоящее время веб-приложения часто загружают содержимое динамически и отображают новые состояния приложения без полной перезагрузки документа. Такие приложения должны предусматривать собственные механизмы управления историей посещений, если необходимо дать пользователю возможность использовать кнопки Back и Forward для перехода из одного состояния приложения в другое интуитивно понятным способом. Спецификация HTML5 определяет два механизма управления историей посещений.

Простейший способ работы с историей посещений связан с использованием свойства location.hash и события «hashchange». На момент написания этих строк данный способ был такэке наиболее широко реализованным: его поддержка в браузерах появилась еще до того, как он был стандартизован спецификацией HTML5. В большинстве браузеров (кроме старых версий IE) изменение свойства location.hash приводит к изменению URL, отображаемого в строке ввода адреса, и добавлению записи в историю посещений. Свойство hash определяет идентификатор фрагмента в URL и традиционно использовалось для перемещения к разделу документа с указанным идентификатором. Но свойство location.hash не обязательно должно определять идентификатор элемента: в него можно записать произвольную строку. Если состояние приложения можно представить в виде строки, эту строку можно будет использовать как идентификатор фрагмента. Предусмотрев изменение значения свойства location.hash, вы даете пользователю возможность использовать кнопки Back и Forward для перемещения между состояниями приложения. Чтобы такие перемещения были возможны, приложение должно иметь способ определять момент изменения состояния, прочитать строку, хранящуюся в виде идентификатора фрагмента, и обновить себя в соответствии с требуемым состоянием. Согласно спецификации HTML5, при изменении идентификатора фрагмента браузер должен возбуждать событие «hashchange» в объекте Window. В браузерах, поддерживающих событие «hashchange», можно присвоить свойству window.onhashchange функцию обработчика, которая будет вызываться при каждом изменении идентификатора фрагмента документа, вызванном перемещением по истории посещений. При вызове эта функция-обработчик должна проанализировать значение location.hash и отобразить содержимое страницы, соответствующее выбранному состоянию.

Спецификация HTML5 также определяет другой, более сложный и более надежный способ управления историей посещений, основанный на использовании метода history.pushState() и события «popstate». При переходе в новое состояние веб-приложение может вызвать метод history.pushState(), чтобы добавить это состояние в историю посещений. В первом аргументе методу передается объект, содержащий всю информацию, необходимую для восстановления текущего состояния приложения. Для этой цели подойдет любой объект, который можно преобразовать в строку вызовом метода JSON.stringifyO, а также некоторые встроенные типы, такие как Date и RegExp (смотрите врезку ниже). Во втором аргументе передается необязательное заглавие (простая текстовая строка), которую браузер сможет использовать для идентификации сохраненного состояния в истории посещений (например, в меню кнопки Back). В третьем необязательном аргументе передается строка URL, которая будет отображаться как адрес текущего состояния. Относительные URL-адреса интерпретируются относительно текущего адреса документа и нередко определяют лишь часть URL, соответствующую идентификатору фрагмента, такую как #state. Связывание различных состояний приложения с собственными URL-адресами дает пользователю возможность делать закладки на внутренние состояния приложения, и если в строке URL будет указан достаточное количество информации, приложение сможет восстановить это состояние при запуске с помощью закладки.

Структурированные копии

Как отмечалось выше, метод pushState() принимает объект с информацией о состоянии и создает его частную копию. Это полная, глубокая копия объекта: при ее создании рекурсивно копируется содержимое всех вложенных объектов и массивов. В стандарте HTML5 такие копии называются структурированными копиями. Процедура создания структурированной копии напоминает передачу объекта функции JSON.stringifу() и обработку результата функцией JSON. parse() (раздел 6.9). Но в формат JSON можно преобразовать только простые значения JavaScript, а также объекты и массивы. Стандарт HTML5 требует, чтобы алгоритм создания структурированных копий поддерживал возможность создания копий объектов Date и RegExp, ImageData (полученных из элементов <canvas> - раздел 21.4.14) и FileList, File и Blob (описывается в разделе 22.6). Функции JavaScript и объекты ошибок явно исключены из списка объектов, поддерживаемых алгоритмом создания структурированных копий, также как и большинство объектов среды выполнения, таких как окна, документы, элемент и т. д. Вряд ли вам понадобится сохранять файлы или изображения как часть состояния приложения, однако структурированные копии также используются некоторыми другими стандартами, связанными со стандартом HTML5, и мы будем встречать их на протяжении всей этой главы.

В дополнение к методу pushState() объект History определяет метод replaceState(), который принимает те же аргументы, но не просто добавляет новую запись в историю посещений, а замещает текущую запись.

Когда пользователь перемещается по истории посещений с помощью кнопок Back и Forward, браузер возбуждает событие «popstate» в объекте Window. Объект, связанный с этим событием, имеет свойство с именем state, содержащее копию (еще одну структурированную копию) объекта с информацией о состоянии, переданного методу pushState().

В примере 22.3 демонстрируется простое веб-приложение - игра «Угадай число», изображенная на рис. 22.1, - в которой используются описанные приемы сохранения истории посещений, определяемые стандартом HTML5, с целью дать пользователю возможность «вернуться назад», чтобы пересмотреть или повторить попытку. Когда эта книга готовилась к печати, в браузере Firefox 4 было внесено два изменения в прикладной интерфейс объекта History, которые могут быть заимствованы и другими браузерами. Во-первых, в Firefox 4 информация о текущем состоянии теперь доступна через свойство state самого объекта History, а это означает, что вновь загружаемые страницы не должны ждать события «popstate». Во-вторых, Firefox 4 более не возбуждает событие «popstate» для вновь загруженных страниц, для которых отсутствует сохраненное состояние. Это второе изменение означает, например, что пример, приведенный ниже, будет работать неправильно в Firefox 4.

Рис. 22.1. Игра «Угадай число»



Пример 22.3. Управление историей посещений с помощью pushState()

<!DOCTYPE html>
<html><head><title>I′m thinking of a number...</title>
<script>
window.onload = newgame;        // Начать новую игру при загрузке 
window.onpopstate = popState;   // Обработчик событий истории посещений 
var state, ui;                  // Глобальные переменные, инициализируемые 
                                // в функции newgame() 

function newgame(playagain) {  // Начинает новую игру "Угадай число" 
  // Настроить объект для хранения необходимых элементов документа 
  ui = {
    heading: null, // Заголовок в начале документа. 
    prompt: null, // Текст предложения ввести число. 
    input: null,  // Поле, куда пользователь вводит-числа. 
    low: null,   // Три ячейки таблицы для визуального представления 
    mid: null,   // ...диапазона, в котором находится загаданное число. 
    high: null
  };
  // Отыскать каждый из этих .элементов по их атрибутам id 
  for(var id in ui) ui[id] = document.getElementById(id);

  // Определить обработчик событий для поля ввода 
  ui.input.onchange = handleGuess;

  // Выбрать случайное число и инициализировать состояние игры 
  state = {
    n: Math.floor(99 * Math.random()) + 1, // Целое число: 0 < n < 100 
    low: 0,      // Нижняя граница, где находится угадываемое число 
    high: 100,     // Верхняя граница, где находится угадываемое число 
    guessnum: 0,    // Количество выполненных попыток угадать число 
    guess: undefined  // Последнее число, указанное пользователем 
  };
  
  // Изменить содержимое документа, чтобы отобразить начальное состояние 
  display(state); 

  // Эта функция вызывается как обработчик события onload, а также 
  // как обработчик щелчка на кнопке Play Again (Играть еще раз), которая 
  // появляется в конце игры. Во втором случае аргумент playagain будет иметь 
  // значение true, и если это так, мы сохраняем новое состояние игры. 
  // Но если функция была вызвана в ответ на событие "load", сохранять 
  // состояние не требуется, потому что событие "load" может возникнуть 
  // также при переходе назад по истории посещений из какого-то другого 
  // документа в существующее состояние игры. Если бы мы сохраняли начальное 
  // состояние, в этом случае мы могли бы затереть имеющееся в истории 
  // актуальное       состояние игры. В браузерах, поддерживающих 
  // метод pushState(), за событием "load" всегда следует событие "popstate". 
  // Поэтому, вместо того чтобы сохранять состояние здесь, мы ждем событие 
  // "popstate". Если вместе с ним будет получен объект состояния, мы просто 
  // используем его. Иначе, если событие "popstate" содержит в поле state 
  // значение null, мы знаем, что была начата новая игра, и поэтому используем 
  // replaceState для сохранения нового состояния игры, 
  if (playagain === true) save(state);
}

// Сохраняет состояние игры с помощью метода pushState(), если поддерживается 
function save(state) { 
  if (!history.pushState) return; // Вернуться, если pushStateO не определен 

  // С каждым состоянием мы связываем определенную строку URL-адреса. 
  // Этот адрес отображает число попыток, но не содержит информации о состоянии 
  // игры, поэтому его нельзя использовать для создания закладок. 
  // Мы не можем поместить информацию о состоянии в URL-адрес, 
  // потому что при этом пришлось бы указать в нем угадываемое число, 
  var url = "#guess" + state.guessnum;
  // Сохранить объект с информацией о состоянии и строку URL 
  history.pushState(state, // Сохраняемый объект с информацией о состоянии 
           "",   // Заглавие: текущие браузеры игнорируют его 
           url);  // URL состояния: бесполезен для закладок 
}

// Обработчик события onpopstate, восстанавливающий состояние приложения, 
function popState(event) {
  if (event.state) { // Если имеется объект состояния, восстановить его 
    // Обратите внимание, что event.state является полной копией 
    // сохраненного объекта состояния, поэтому мы можем изменять его, 
    // не опасаясь изменить сохраненное значение, 
    state = event.state;  // Восстановить состояние 
    display(state);     // Отобразить восстановленное состояние 
  }
  else {
    // Когда страница загружается впервые, событие "popstate" поступает 
    // без объекта состояния. Заменить значение null действительным 
    // состоянием: смотрите комментарий в функции newgame(). 
    // Нет необходимости вызывать displayO здесь, 
    history.replaceState(state, "", "#guess" + state.guessnum);
  }
};

// Этот обработчик событий вызывается всякий раз, 
// когда пользователь вводит число. 
// Он обновляет состояние игры, сохраняет и отображает его. 
function handleGuess() {
  // Извлечь число из поля ввода 
  var g = parseInt(this.value);
  // Если это число и оно попадает в требуемый диапазон 
  if ((g > state.low) && (g < state.high)) { 
    // Обновить объект состояния для этой попытки 
    if (g < state.n) state.low = g;     
    else if (g > state.n) state.high = g;
    state.guess = g;
    state.guessnum++;
    // Сохранить новое состояние в истории посещений 
    save(state);
    // Изменить документ в ответ на попытку пользователя 
    display(state);
  }
  else { // Ошибочная попытка: не сохранять новое состояние 
    alert("Please enter a number greater than " + state.low +
       " and less than " + state.high);
  }
}

// Изменяет документ, отображая текущее состояние игры, 
function display(state) { 
  // Отобразить заголовок документа 
  ui.heading.innerHTML = document.title =
    "I′m thinking of a number between " +
    state.low + " and " + state.high + ".";

  // Отобразить диапазон чисел с помощью таблицы 
  ui.low.style.width = state.low + "%";
  ui.mid.style.width = (state.high-state.low) + "%";
  ui.high.style.width = (100-state.high) + "%";

  // Сделать поле ввода видимым, очистить его и установить фокус ввода 
  ui.input.style.visibility = "visible"; 
  ui.input.value = "";
  ui.input.focus();

  // Вывести приглашение к вводу, опираясь на последнюю попытку 
  if (state.guess === undefined)
    ui.prompt.innerHTML = "Type your guess and hit Enter: ";
  else if (state.guess < state.n)
    ui.prompt.innerHTML = state.guess + " is too low. Guess again: ";
  else if (state.guess > state.n)
    ui.prompt.innerHTML = state.guess + " is too high. Guess again: ";
  else {
    // Если число угадано, скрыть поле ввода и отобразить кнопку 
    // Play Again (Играть еще раз). 
    ui.input.style.visibility = "hidden"; // Попыток больше не будет 
    ui.heading.innerHTML = document.title = state.guess + " is correct! ";
    ui.prompt.innerHTML =
      "You Win! <button onclick=′newgame(true)′>Play Again</button>";
  }
}
</script>
<style>/* CSS-стили, чтобы придать игре привлекательный внешний вид */ 
#prompt { font-size: 16pt; } 
table { width: 90%; margin:10px; margin-left:5%; } 
#low, #high { background-color: lightgray; height: 1em; } 
#mid { background-color: green; } 
</style> 
</head> 
<body> 
 
<h1 id="heading">I'm thinking of a number... 
 
<table>
<tr><td id="low"></td><td id="mid"></td><td id="high"></td></tr>
</table> 
 
<label id="prompt"></label><input id="input" type="text"> 
</body></html> 

содержание 22.3. Взаимодействие документов с разным происхождением содержание

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

Однако иногда сценарий имеет возможность сослаться на другой объект Window, но поскольку содержимое этого окна имеет иное происхождение, веб-браузер (следующий требованиям политики общего происхождения) не позволит сценарию просматривать содержимое документа в этом другом окне. Браузер не позволит сценарию читать значения свойств или вызывать методы другого окна. Единственный метод окна, который может вызвать сценарий из документа с другим происхождением, называется postMessage(), и этот метод обеспечивает возможность ограниченных взаимодействий - в форме асинхронных сообщений - между сценариями из документов с разным происхождением. Этот вид взаимодействий определяется стандартом HTML5 и реализован во всех текущих браузерах (включая IE8 и более поздние версии). Этот прием известен как «обмен сообщениями между документами с разным происхождением», но так как прикладной интерфейс поддержки определен в объекте Window, а не в объекте документа, возможно, лучше было бы назвать этот прием «обменом сообщениями между окнами». Метод postMessage() принимает два обязательных аргумента. Первый- передаваемое сообщение. Согласно спецификации HTML5, это может быть любое простое значение или объект, который можно скопировать (смотрите выше врезку «Структурированные копии»), но некоторые текущие браузеры (включая Firefox 4 beta) принимают только строки, поэтому, если в сообщении потребуется передать объект или массив, его необходимо будет предварительно сериализовать с помощью функции JSON.stringifу() (раздел 6.9).

Во втором аргументе должна передаваться строка, определяющая ожидаемое происхождение окна, принимающего сообщение. Эта строка должна включать протокол, имя хоста и (необязательно) номер порта (можно передать полный URL-адрес, но все, кроме протокола, хоста и порта будет игнорироваться). Это предусмотрено из соображений безопасности: злонамеренный программный код или обычные пользователи могут открывать новые окна с документами, взаимодействие с которыми вы не предполагали, поэтому postMessage() не будет доставлять ваши сообщения, если окно будет содержать документ с происхождением, отличным от указанного. Если передаваемое сообщение не содержит конфиденциальной информации и вы готовы передать ее сценарию из документа с любым происхождением, то во втором аргументе можно передать строку "*", которая будет играть роль шаблонного символа. Если необходимо указать, что документ должен иметь то же происхождение, что и документ в текущем окне, можно передать строку "/". Если происхождение документа совпадет с указанным в аргументе, вызов метода postMessage() приведет к возбуждению события «message» в целевом объекте Window. Сценарий в этом окне может определить обработчик для обработки событий «message», которому будет передан объект события со следующими свойствами:

data
Копия сообщения, переданного методу postMessage() в первом аргументе.
source
Объект Window, из которого было отправлено сообщение.
origin
Строка, определяющая происхождение (в виде URL) документа, отправившего сообщение.
Большинство обработчиков onmessage() должно сначала проверить свойство origin своего аргумента и должно игнорировать сообщения из неподдерживаемых доменов.

Обмен сообщениями с документами из сторонних доменов посредством метода postMessage() и обработки события «message» может пригодиться, например, когда к веб-странице нужно подключить модуль, или «гаджет», с другого сайта. Если модуль прост и автономен, его можно просто загрузить в элемент <iframe>. Однако представьте, что это более сложный модуль, который определяет свой прикладной интерфейс, и ваша веб-страница должна управлять этим модулем или как-то иначе взаимодействовать с ним. Если модуль определен в виде элемента <script>, он сможет предложить полноценный прикладной интерфейс на языке JavaScript, но с другой стороны включение его в страницу даст ему полный контроль над страницей и ее содержимым. Такой подход широко используется в современной Всемирной паутине (особенно в веб-рекламе), но в действительности это не самое лучшее решение, даже если вы доверяете другому сайту. Альтернативой является обмен сообщениями между документами с разным происхождением: автор модуля может упаковать его в HTML-файл, который принимает события «message» и передает их для обработки соответствующим функциям на языке JavaScript. В этом случае веб-страница, подключившая модуль, сможет взаимодействовать с ним, отправляя сообщения с помощью метода postMessage(). Этот прием демонстрируется в примерах 22.4 и 22.5. В примере 22.4 приводится реализация простого модуля, подключаемого с помощью элемента <iframe>, который выполняет поиск на сайте Twitter и отображает сообщения, соответствующие указанной фразе. Чтобы выполнить поиск с помощью этого модуля, подключившая его страница должна просто отправить ему сообщение с требуемой фразой.

Пример 22.4. Модуль поиска на сайте Twitter с помощью метода postMessage()

<!DOCTYPE html>
<!--
Это модуль поиска на сайте Twitter. Модуль можно подключить к любой странице внутри 
элемента <iframe> и запросить его выполнить поиск, отправив ему строку запроса 
 с помощью метода postMessage(). Поскольку модуль подключается в элементе <iframe>, 
а не <script>, он не сможет получить доступ к содержимому вмещающего документа. 
-->
<html>
<head>
<style>body { font: 9pt sans-serif; }</style>
<!-- Подключить библиотеку jQuery ради ее утилиты jQuery.getJSONO --> 
<script src="http://code. jquery.com/jquery-1.4.4.min. js"></script>
<script>
// Можно было бы просто использовать свойство window.onmessage, 
// но некоторые старые браузеры (такие как Firefox 3) не поддерживают его, 
// поэтому обработчик устанавливается таким способом. 
if (window.addEventListener)
 window.addEventListener("message", handleMessage, false);
else 
 window.attachEvent("onmessage", handleMessage); // Для IE8 
 
function handleMessage(e) {
 // Нас не волнует происхождение документа, отправившего сообщение: 
 // мы готовы выполнить поиск на сайте Twitter для любой страницы. 
 // Однако сообщение должно поступить от окна, вмещающего этот модуль, 
 if (e.source !== window.parent) return;
 
 var searchterm = e.data; // Это фраза, которую требуется отыскать 
 
 // С помощью утилит поддержки Ajax из библиотеки jQuery и прикладного 
 // интерфейса Twitter отыскать сообщения, соответствующие фразе. 
 jQuery.getJSON("http://search, twitter, com/search. json?callback=?"', 
     { q: searchterm },
     function(data) { // Вызывается с результатами запроса 
      var tweets = data.results;
      // Создать HTML-документ для отображения результатов 
      var escaped = searchterm.replace("<", "<");
      var html = "<h2>" + escaped + "</h2>";
      if (tweets.length == 0) {
       html += "No tweets found";
      }
      else {
       html += "<dl>"; //<dl> list of results 
       for(var i = 0; i < tweets.length; i++) {
        var tweet = tweets[i];
        var text = tweet.text;
        var from = tweet.from_user;
        var tweeturl = "http://twitter.eom/#!/"' + 
         from + "/status/" + tweet.id_str;
        html += "<dt><a target=′_blank′ href=′" +
         tweeturl + "′>" + tweet.from_user +
         "</a></dt><dd>" + tweet.text + "</dd>";
       }
       html += "</dl>";
      }
      // Вставить документ в <iframe> 
      document.body.innerHTML = html;
     });
}
 
$(function() {
 // Сообщить вмещающему документу, что модуль готов к поиску. Вмещающий документ 
 // не может отправлять модулю сообщения до получения этого сообщения, потому что 
 // модуль еще не готов принимать сообщения. Вмещающий документ может просто 
 // дождаться события onload, чтобы определить момент, кода будут загружены 
 // все фреймы. Но мы предусматриваем отправку этого сообщения, чтобы известить 
 // вмещающий документ, что можно начать выполнять поиск еще до получения 
 // события onload. Модулю неизвестно происхождение вмещающего документа, 
 // поэтому мы используем *, чтобы браузер доставил сообщение любому документу, 
 window.parent.postMessage("Twitter Search v0.1", "*");
});
</script>
</head>
<body>
</body>
</html>

В примере 22.5 представлен простой сценарий на языке JavaScript, который может быть вставлен в любую веб-страницу, где требуется использовать модуль поиска по сайту Twitter. Он вставляет модуль в документ и затем устанавливает обработчики событий во все ссылки в документе, чтобы при наведении указателя мыши на ссылку производился вызов метода postMessage() модуля, заставляющий его выполнить поиск по строке URL ссылки. Это позволит узнать, что говорят люди о веб-сайте перед тем, как перейти к нему.

Пример 22.5 (js). Использование модуля поиска по сайту Twitter с помощью метода postMessage()

// Этот сценарий JS вставляет Twitter Search Gadget в документ и добавляет 
// обработчик событий ко всем ссылкам в документе, чтобы при наведении 
// указателя мыши на них с помощью модуля поиска отыскать упоминания
//  URL-адресов в ссылках. Это позволяет узнать, что говорят
// другие о сайтах, прежде чем щелкнуть на ссылке.
window.addEventListener("load", function() {       // He работает в IE < 9 
    var origin = "http://davidflanagan.com";       // Происхождение модуля
    var gadget = "/demos/TwitterSearch.html";      // Путь к модулю
    var iframe = document.createElement("iframe"); // Создать iframe 
    iframe.src = origin + gadget;                  // Установить его URL
    iframe.width = "250";                          // Ширина 250 пикселов
    iframe.height = "100%";                        // Во всю высоту документа
    iframe.style.cssFloat = "right";               // Разместить справа

    // Вставить фрейм в начало документа
    document.body.insertBefore(iframe, document.body.firstChild);
    
    // Отыскать все ссылки и связать их с модулем
    var links = document.getElementsByTagName("a");
    for(var i = 0; i < links.length; i++) {
        // addEventListener не работает в версии 1Е8 и ниже
        links[i].addEventListener("mouseover", function() {
            // Отправить url как искомую фразу и доставлять его, только если
            // фрейм все еще отображает документ с сайта davidflanagan.com 
            iframe.contentWindow.postMessage(this.href, origin);
        }, false);
    }
}, false);

содержание 22.4. Фоновые потоки выполнения содержание

Одна из основных особенностей клиентских сценариев на языке JavaScript заключается в том, что они выполняются в единственном потоке выполнения: браузер, к примеру, никогда не будет выполнять два обработчика событий одновременно, и таймер никогда не сработает, пока выполняется обработчик события. Параллельные обновления переменных приложения или документа просто невозможны, и программистам, разрабатывающим клиентские сценарии, не требуется задумываться или хотя бы понимать особенности параллельного программирования. Как следствие, функции в клиентских сценариях на языке JavaScript должны выполняться очень быстро, иначе они остановят работу цикла событий и веб-браузер перестанет откликаться на действия пользователя. Именно по этой причине прикладной интерфейс поддержки Ajax всегда действует асинхронно, и по этой же причине в клиентском JavaScript отсутствуют синхронные версии функций load() или require() для загрузки библиотек на языке JavaScript. с основным потоком выполнения только посредством передачи асинхронных сообщений. Это означает, что параллельные изменения дерева DOM по-прежнему невозможны, но это также означает, что теперь имеется возможность использовать синхронные прикладные интерфейсы и писать функции, выполняющиеся длительное время, которые не будут останавливать работу цикла событий и подвешивать браузер. Создание нового фонового потока выполнения не является такой тяжеловесной операцией, как открытие нового окна браузера, но также не является легковесной операцией, вследствие чего нет смысла создавать новые фоновые потоки для выполнения тривиальных операций. В сложных веб-приложениях может оказаться полезным применение даже нескольких десятков фоновых потоков, но весьма маловероятно, что приложения с сотнями и тысячами таких потоков смогут иметь практическую ценность.

Спецификация «Web Workers»¹ со всеми мерами предосторожности ослабляет ограничение на единственный поток выполнения в клиентском JavaScript. «Фоновые потоки», определяемые спецификацией, фактически являются параллельными потоками выполнения. Однако эти потоки выполняются в изолированной среде, не имеют доступа к объектам Window и Document и могут взаимодействовать с основным потоком выполнения только посредством передачи асинхронных сообщений. Это означает, что параллельные изменения дерева DOM по-прежнему невозможны, но это также означает, что теперь имеется возможность использовать синхронные прикладные интерфейсы и писать функции, выполняющиеся длительное время, которые не будут останавливать работу цикла событий и подвешивать броузер. Создание нового фонового потока выполнения не является такой тяжеловесной операцией, как открытие нового окна броузера, но также не является легковесной операцией, вследствие чего нет смысла создавать новые фоновые потоки для выполнения тривиальных операций. В сложных веб-приложениях может оказаться полезным применение даже нескольких десятков фоновых потоков, но весьма маловероятно, что приложения с сотнями и тысячами таких потоков смогут иметь практическую ценность.

¹ Спецификация «Web Workers» изначально была частью спецификации HTML5, но затем была выделена в отдельный, независимый, хотя и тесно связанный со стандартом, документ. На момент написания этих строк проект спецификации был доступен по адресам http://dev.w3.org/html5/workers/ и http://whatwg.org/ww.

Как и любой прикладной интерфейс поддержки многопоточных приложений, спецификация «Web Workers» определяет две различные части. Первая - объект Worker, который представляет фоновый поток выполнения в программе, создавшей его. Вторая-объект WorkerGlobalScope, глобальный объект нового фонового потока выполнения, который представляет фоновый поток выполнения внутри него самого. Оба объекта описываются в следующих подразделах. За ними следует раздел с примерами.

содержание 22.4.1. Объект Worker

Чтобы создать новый фоновый поток, достаточно просто вызвать конструктор Worker(), передав ему URL-адрес, определяющий программный код на языке JavaScript, который должен выполняться в фоновом потоке:

var loader = new Worker("utils/loader. js");

Если указать относительный URL-адрес, он будет интерпретироваться относительно URL-адреса документа, содержащего сценарий, который вызвал конструктор Worker(). Если указать абсолютный URL-адрес, он должен иметь то же происхождение (протокол, имя хоста и порт), что и вмещающий документ. После создания объекта Worker ему можно отправлять данные с помощью его метода postMessage(). Значение, переданное методу postMessage(), будет скопировано (смотрите выше врезку «Структурированные копии»), и полученная копия будет передана фоновому потоку вместе с событием «message»:

loader.postMessage("file.txt");

Обратите внимание, что, в отличие от метода postMessage() объекта Window, метод postMessage() объекта Worker не имеет аргумента, в котором передавалась бы строка, описывающая происхождение (раздел 22.3). Кроме того, метод postMessage() объекта Worker корректно копирует сообщение во всех текущих браузерах, в отличие от Window.postMessage(), который в некоторых основных браузерах способен принимать только строковые сообщения.

Принимать сообщения от фонового потока можно с помощью обработчика события «message» объекта Worker:

worker.onmessage = function(e) {
var message = e.data; // Извлечь сообщение console.log("'Содержимое: " + message);
// Выполнить некоторые действия }

Если фоновый поток возбудит исключение и не обработает его, это исключение продолжит распространение в виде события, которое также можно перехватить:

worker.onerror = function(e) {
// Вывести текст ошибки, включая имя файла фонового потока и номер строки
console, log ("Ошибка в " + е. filename + ":"' + e.lineno + ": " + е. message); }

Подобно всем объектам, в которых могут возбуждаться события, объект Worker определяет стандартные методы addEventListener() и removeEventListener(), которые можно использовать вместо свойств onmessage и onerror, если необходимо установить несколько обработчиков событий.

Объект Worker имеет еще один метод, terminate(), который останавливает выполнение фонового потока.


содержание 22.4.2. Область видимости фонового потока

При создании нового фонового потока с помощью конструктора Worker() вы задаете URL-адрес файла с программным кодом на языке JavaScript. Этот программный код выполняется в новой, нетронутой среде выполнения JavaScript, полностью изолированной от сценария, запустившего фоновый поток. Глобальным объектом этой изолированной среды выполнения является объект WorkerGlobalScope. Объект WorkerGlobalScope - это чуть больше, чем просто глобальный объект JavaScript, но меньше, чем полноценный клиентский объект Window. Объект WorkerGlobalScope имеет метод postMessage() и свойство обработчика события onmessage, подобные своим аналогам в объекте Worker, но действующие в обратном направлении. Вызов метода postMessage() внутри фонового потока сгенерирует событие «message» за его пределами, а сообщения, отправляемые извне, будут превращаться в события и передаваться обработчику onmessage. Обратите внимание: благодаря тому, что объект WorkerGlobalScope является глобальным объектом для фонового потока, метод postMessage() и свойство onmessage воспринимаются программным кодом, выполняющимся в фоновом потоке, как глобальная функция и глобальная переменная.

Функция close() позволяет фоновому потоку завершить свою работу, и по своему действию она напоминает метод terminate() объекта Worker. Отметьте, однако, что в объекте Worker нет метода, который позволил бы определить, прекратил ли работу фоновый поток, как и нет свойства обработчика события onclose. Если попытаться с помощью метода postMessage() передать сообщение фоновому потоку, который прекратил работу, это сообщение будет просто проигнорировано без возбуждения исключения. В общем случае, если фоновый поток может завершить работу самостоятельно вызовом метода close(), неплохо было бы предусмотреть отправку сообщения, извещающего о прекращении работы.

Наибольший интерес представляет глобальная функция importScripts(), определяемая объектом WorkerGlobalScope: фоновые потоки выполнения могут использовать ее для загрузки любых необходимых им библиотек. Например:

// Перед началом работы загрузить необходимые классы и утилиты
importScriptsC'collections/Set.js", "collections/Map.js", "utils/base64.js");

Функция importScripts() принимает один или более аргументов с URL-адресами, каждый из которых должен ссылаться на файл с программным кодом на языке JavaScript. Относительные URL-адреса интерпретируются относительно URL-адреса, переданного конструктору Worker(). Она загружает и выполняет указанные файлы один за другим в том порядке, в каком они были указаны. Если при загрузке или при выполнении сценария возникнет какая-либо ошибка, ни один из последующих сценариев не будет загружаться или выполняться. Сценарий, загруженный функцией importScripts(), сам может вызвать функцию importScripts(), чтобы загрузить необходимые ему файлы. Отметьте, однако, что функция importScripts() не запоминает, какие сценарии были загружены, и не предусматривает защиту от циклических ссылок.

Функция importScripts() выполняется синхронно: она не вернет управление, пока не будут загружены и выполнены все сценарии. Сценарии, указанные в вызове функции importScripts(), можно использовать сразу, как только она вернет управление: нет никакой необходимости определять функцию обратного вызова или обработчик события. После того как вы свыклись с асинхронной природой клиентского JavaScript, такой возврат к простой, синхронной модели может показаться странным. Но в этом и заключается достоинство потоков выполнения: в фоновом потоке можно использовать блокирующие функции, не блокируя цикл событий в основном потоке выполнения и не блокируя вычисления, выполняемые параллельно в других фоновых потоках.

Модель выполнения фонового потока

Фоновые потоки выполняют свой программный код (и все импортированные сценарии) синхронно, от начала до конца, и затем переходят в асинхронную фазу выполнения, когда они откликаются на события и таймеры. Если фоновый поток регистрирует обработчик события onmessage, он никогда не завершит работу, пока есть вероятность поступления событий «message». Но если фоновый поток не принимает сообщения, он будет работать, пока не будут выполнены все задания (такие как загрузка или таймеры) и не будут вызваны все функции, связанные с этими заданиями. После вызова всех зарегистрированных функций обратного вызова в фоновом потоке нет никакой возможности начать выполнять новые задания, поэтому он может смело завершить свою работу. Представьте себе фоновый поток без обработчика событий onmessage, который загружает файл с помощью объекта XMLHttpRequest. Если обработчик onload запустит загрузку другого файла или зарегистрирует обработчик таймера вызовом функции setTimeout(), поток выполнения получит новое задание и продолжит работу. Иначе он завершится.

Поскольку для фоновых потоков выполнения WorkerGlobalScope является глобальным объектом, он обладает всеми свойствами базового глобального объекта JavaScript, такими как объект JS0N, функция isNaN() и конструктор Date(). (Полный список можно найти в справочной статье Global, в третьей части книги). Однако, кроме того, объект WorkerGlobalScope имеет следующие свойства клиентского объекта Window:

Дополнительные особенности объекта Worker

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

Роль «сокета» для разделяемого потока выполнения играет объект MessagePort. Он определяет прикладной интерфейс обмена сообщениями, подобный аналогичному интерфейсу в выделенных потоках выполнения, а также используемому для обмена сообщениями между документами с разным происхождением. Объект MessagePort имеет метод postMessage() и атрибут onmessage обработчика событий. Спецификация HTML5 предусматривает возможность создания связанных пар объектов MessagePort с помощью конструктора MessageChannel(). Вы можете передавать объекты MessagePort (в специальном аргументе метода postMessage()) другим окнам или другим фоновым потокам выполнения и использовать их как выделенные каналы связи. Объекты MessagePort и MessageChannel являются дополнительным прикладным интерфейсом, который поддерживается лишь немногими браузерами и здесь не рассматривается.

Наконец, объект WorkerGlobalScope включает конструкторы объектов, важных для клиентских сценариев. В их числе конструктор XMLHttpRequest(), позволяющий фоновым потокам выполнять HTTP-запросы (Глава 18), и конструктор Worker() дающий возможность фоновым потокам создавать свои фоновые потоки. (Однако на момент написания этих строк конструктор Worker() был недоступен фоновым потокам в браузерах Chrome и Safari.)

Некоторые прикладные интерфейсы HTML5, описываемые далее в этой главе, определяют особенности, доступные как через обычный объект Window, так и через объект WorkerGlobalScope. Часто асинхронному прикладному интерфейсу объекта Window соответствует его синхронная версия в объекте WorkerGlobalScope. Эти прикладные интерфейсы «с поддержкой фоновых потоков выполнения» мы рассмотрим далее в этой главе.


содержание 22.4.3. Примеры использования фоновых потоков

Завершают этот раздел два примера использования фоновых потоков выполнения. Первый демонстрирует, как реализовать выполнение длительных вычислений в фоновом потоке, чтобы они не влияли на отзывчивость пользовательского интерфейса, обслуживаемого основным потоком выполнения. Второй демонстрирует, как можно использовать фоновые потоки для работы с простейшими синхронными прикладными интерфейсами.

В примере 22.6 определяется функция smear(), которая принимает элемент <img> в виде аргумента. Она применяет эффект размытия для «смазывания» изображения вправо. Для реализации этого эффекта используется описанный в главе 21 прием копирования изображения в неотображаемый элемент <canvas> и последующего извлечения пикселей изображения с помощью объекта ImageData. Элементы <img> и <canvas> нельзя передать фоновому потоку выполнения с помощью метода postMessage(), но можно передать объект ImageData (подробности выше во врезке «Структурированные копии»). Пример 22.6 создает объект Worker и вызывает его метод postMessage(), чтобы передать ему изображение. Когда фоновый поток отправит обработанные пикселы изображения обратно, программный код скопирует их снова в элемент <canvas>, извлекая их как ресурс с URL-адресом вида data:// и устанавливая этот URL-адрес в качестве значения свойства src оригинального элемента <img>.

Пример 22.6 (js). Создание фонового потока выполнения для обработки изображения

// Асинхронная замена изображения его смазанной версией. 
// Используется так: <img src="testimage.jpg" onclick="smear(this)”/> 
function smear(img) {
  // Создать неотображаемый элемент <canvas> того же размера, что и изображение
  var canvas = document.createElement("canvas");
  canvas.width = img.width; 
  canvas.height = img.height;

  // Скопировать изображение в холст и извлечь его пикселы
  var context = canvas.getContext("2d"); 
  context.drawImage(img, 0, 0);     
  var pixels = context.getImageData(0,0,img.width,img.height)

  // Отправить пикселы фоновому потоку выполнения
  var worker = new Worker("SmearWorker.js");   // Создать фоновый поток
  worker.postMessage(pixels);           // Скопировать и отдать пикселы 

  // Зарегистрировать обработчик для получения ответа от фонового потока
  worker.onmessage = function(e) {
    var smeared_pixels = e.data;        // Пикселы, полученные от потока
    context.putImageData(smeared_pixels, 0, 0); // Скопировать в холст
    img.src = canvas.toDataURL();        // А затем в изображение
    worker.terminate();             // Остановить поток
    canvas.width = canvas.height = 0;      // Освободить память
  }
}

В примере 22.7 приводится программный код реализации фонового потока, используемого в примере 22.6. Основу этого примера составляет функция обработки изображения: модифицированная версия примера 21.10. Обратите внимание, что этот пример настраивает свою инфраструктуру обмена сообщениями единственной строчкой программного кода: обработчик события onmessage просто накладывает эффект смазывания на изображение и сразу же отправляет его обратно.

Пример 22.7 (js). Обработка изображения в фоновом потоке выполнения

// Получает объект ImageData от основного потока выполнения, обрабатывает его
 и отправляет обратно 
onmessage = function(e) { postMessage(smear(e.data)); }

// Смазывает пикселы в ImageData вправо, воспроизводя эффект быстрого движения.
// При обработке больших изображений этой функции приходится выполнять огромный
// объем вычислений, что может вызвать эффект подвисания пользовательского
// интерфейса, если использовать ее в основном потоке выполнения. 
function smear(pixels) {
  var data = pixels.data, width = pixels.width, height = pixels.height;
  var n = 10, m = n-1; // Чем больше n, тем сильнее эффект смазывания
  for(var row = 0; row < height; row++) {      // Для каждой строки 
    var i = row*width*4 + 4;            // Индекс 2-го пиксела
    for(var col = 1; col < width; col++, i += 4) { // Для каждого столбца 
      data[i] =  (data[i] + data[i-4]*m)/n;   // Красная составляющая 
      data[i+1] = (data[i+1] + data[i-3]*m)/n;  // Зеленая 
      data[i+2] = (data[i+2] + data[i-2]*m)/n;  // Синяя 
      data[i+3] = (data[i+3] + data[i-1]*m)/n;  // Альфа-составляющая 
    }
  }
  return pixels;
}

Обратите внимание, что программный код в примере 22.7 может обрабатывать любое количество изображений, которые будут отправлены ему. Однако для простоты пример 22.6 создает новый объект Worker для обработки каждого изображения. Чтобы не плодить фоновые потоки, которые ничего не делают в ожидании новых сообщений, по завершении обработки изображения работа фонового потока завершается вызовом метода terminate().


Следующий пример демонстрирует, как с помощью фоновых потоков выполнения можно писать синхронный программный код и безопасно использовать его в клиентских сценариях на языке JavaScript. В разделе 18.1.2.1 было показано, как с помощью объекта XMLHttpRequest выполнять синхронные HTTP-запросы, и говорилось, что такой способ его использования в основном потоке выполнения является не лучшим решением. Однако в фоновом потоке вполне оправданно выполнять синхронные запросы, и в примере 22.8 демонстрируется реализация фонового потока выполнения, которая выполняет именно такие запросы. Его обработчик события onmessage принимает массив URL-адресов, использует синхронный прикладной интерфейс объекта XMLHttpRequest для извлечения их содержимого и затем посылает полученное текстовое содержимое в виде массива строк обратно основному потоку выполнения. Или, если какой-либо HTTP-запрос потерпит неудачу, возбуждает исключение, которое распространится до обработчика onerror объекта Worker.

Пример 22.8 (js). Выполнение синхронных HTTP-запросов в фоновом потоке

// Этот файл будет загружен вызовом конструктора Worker(), поэтому он будет 
// выполнятьсяв независимом потоке выполнения и может безопасно использовать 
//  синхронный прикладной интерфейс объекта XMLHttpRequest. 
// В качестве сообщения фоновому потоку должен
// передаваться массив URL-адресов. Поток синхронно извлечет содержимое
// из указанных адресов и вернет его в виде массива строк.
onmessage = function(e) {
    var urls = e.data;   //
    var contents = [];   // Выходные данные: содержимое указанных URL-адресов

    for(var i = 0; i < urls.length; i++) { 
        var url = urls[i];                 // Для каждого URL-адреса
        var xhr = new XMLHttpRequest();    // Создать HTTP-запрос
        xhr.open("GET", url, false);       // false обеспечит синхронное выполн.
        xhr.send();                        // Блокируется до выполнения запроса
        if (xhr.status !== 200)            // Возбудить исключение при неудаче
            throw Error(xhr.status + " " + xhr.statusText + ": " + url);
        contents.push(xhr.responseText);   // Иначе сохранить содержимое 
    }

    // Отослать массив содержимого URL-адресов обратно основному потоку
    postMessage(contents);
}

Отладка фоновых потоков выполнения

Одним из прикладных интерфейсов, недоступных в объекте WorkerGlobalScope (по крайней мере, на момент написания этих строк), является прикладной интерфейс доступа к консоли и одна из самых ценных его функций - console.log(). Фоновые потоки не могут выводить текст в консоль и вообще не могут взаимодействовать с документом, поэтому их отладка может оказаться весьма трудным делом. Если фоновый поток возбудит исключение, основной поток получит событие «error» в объекте Worker. Но чаще бывает необходимо иметь в фоновом потоке хоть какой-нибудь способ выводить отладочные сообщения, которые будут видимы в веб-консоли браузера. Один из самых простых способов добиться этого - изменить протокол передачи сообщений, используемый для взаимодействия с фоновым потоком, чтобы он мог посылать отладочные сообщения. Так, в примере 22.6 можно было бы вставить следующий программный код в начало обработчика событий

onmessage:
if (typeof e.data === "string") {
console.log("Worker: *' + e.data);
return;
}

Благодаря этому дополнительному программному коду фоновый поток получает возможность отображать отладочные сообщения, просто передавая строки методу postMessage().


<

содержание 22.5. Типизированные массивы и буферы содержание

Как говорилось в главе 7, массивы в языке JavaScript являются многоцелевыми объектами с числовыми именами свойств и специальным свойством length. Элементами массива могут быть любые JavaScript-значения. Массивы могут увеличиваться и уменьшаться в размерах и быть разреженными. Реализации JavaScript выполняют множество оптимизаций, благодаря которым типичные операции с массивами в языке JavaScript выполняются очень быстро.

Существует восемь разновидностей типизированных массивов, каждый со своим типом элементов. Создавать их можно с помощью следующих конструкторов:

Конструктор Числовой тип
Int8Array()байты со знаком
Uint8Array() байты без знака
Int16Array() 16-битные короткие целые со знаком
Uint16Array()16-битные короткие целые без знака
Int32Array()32-битные целые со знаком
Uint32Array()32-битные целые без знака
Float32Array()32-битные вещественные значения
Float64Array()64-битные вещественные значения

Типизированные массивы, элемент <canvas> и базовый JavaScript

Типизированные массивы являются важной частью прикладного интерфейса создания трехмерной графики WebGL в элементе <canvas>, и в браузерах они реализованы как часть прикладного интерфейса WebGL. WebGL не рассматривается в этой книге, но типизированные массивы весьма полезны сами по себе, и поэтому обсуждаются здесь. В главе 21 говорилось, что прикладной интерфейс объекта Canvas определяет метод getlmageData(), возвращающий объект ImageData. Свойство data объекта ImageData является массивом байтов. В спецификации HTML он называется CanvasPixelArray, но, по сути, это то же самое, что описываемый здесь Uint8Array, за исключением способа обработки значений, выходящих за диапазон 0-255. Имейте в виду, что эти типы не являются частью базового языка. Будущие версии языка JavaScript, возможно, будут включать поддержку типизированных массивов, подобных этим, но на момент написания этих строк еще было неясно, примет ли язык прикладной интерфейс, описываемый здесь, или будет создан новый прикладной интерфейс.

При создании типизированного массива конструктору передается его размер. Вместо размера можно передать массив или типизированный массив для инициализации элементов нового массива. После создания типизированного массива его элементы можно читать или изменять с помощью обычной формы записи с квадратными скобками, как и при работе с любым другим объектом, подобным массиву:

var bytes = new Uint8Array(1024); // Один килобайт байтов
for(var i = 0; i < bytes.length; i++) // Для каждого элемента массива
  bytes[i] = i & 0xFF; // Записать 8 младших бит индекса
var copy = new Uint8Array(bytes); // Создать копию массива
var ints = new Int32Array([0,1,2,3]); // Типизированный массив с 4 целыми

Современные реализации JavaScript оптимизируют операции с массивами и делают их очень эффективными. Но типизированные массивы могут быть еще эффективнее как по времени выполнения операций с ними, так и по использованию памяти. Следующая функция вычисляет наибольшее простое число, меньшее указанного значения. Она использует алгоритм «Решето Эратосфена», основанный на использовании большого массива, в котором запоминается, какие числа простые, а какие составные. Так как каждый элемент массива хранит всего один бит информации, объект Int8Array может оказаться эффективнее в использовании, чем обычный массив JavaScript:

// Возвращает наибольшее целое простое число меньше n.
// Использует алгоритм "Решето Эратосфена"
function sieve(n) {
width="50%">  var a = new Int8Array(n+1);// в a[x] записывается 1, 
                                         // если x - составное число
    var max = Math.floor(Math.sqrt(n));  // Множитель не может быть выше
	                                 // этого значения
    var p = 2;                           // 2 - первое простое число
    while(p <= max) {                    // Для простых чисел меньше max
        for(var i = 2*p; i <= n; i += p) // Пометить числа, кратные p, 
            a[i] = 1;                    // как составные 
        while(a[++p]) /* пустое тело */; // Следующий непомеченный индекс - 
    }                                    // простое число 

    while(a[n]) n--;     // Цикл в обр. напр., чтобы отыскать последнее простое
    return n;            // И вернуть его 
} 

Функция sieve() будет работать, если вызов конструктора Int8Array() заменить вызовом традиционного конструктора Аггау(), но выполняться она будет в два- три раза дольше и будет расходовать гораздо больше памяти при больших значениях параметра n. Типизированные массивы могут также пригодиться при обработке графических изображений и для математических вычислений:

var matrix = new Float64Array(9); // Матрица 3x3 
var 3dPoint = new Int16Array(3);  // Точка в 3-мерном пространстве
var rgba = new Uint8Array(4);     // 4-байтовое значение RGBA пиксела
var sudoku = new Uint8Array(81);  // Доска 9x9 для игры в судоку

Форма записи с квадратными скобками, используемая в языке JavaScript, позволяет читать и записывать значения отдельных элементов типизированного массива. Но типизированные массивы определяют дополнительные методы для записи и чтения целого фрагмента массива. Метод set() копирует элементы обычного или типизированного массива в типизированный массив:

var bytes = new Uint8Array(1024)         // Буфер размером 1Кбайт
var pattern = new Uint8Array([0,1,2,3]); // Массив из 4 байтов
bytes.set(pattern);                      // Скопировать их в начало
                                         // другого массива байтов
bytes.set(pattern, 4);                   // Скопировать их в другое место массива
bytes.set([0,1,2,3], 8);                 // Просто скопировать значения
                                         // из обычного массива

Типизированные массивы имеют также метод subarray(), возвращающий фрагмент массива, относительно которого он был вызван:

var ints = new Int16Array([0,1,2,3,4,5,6,7,8,9]);       // 10 коротких целых
var last3 = ints.subaarray(ints.length-3, ints.length); // Последние З из них
last3[0]                                  // => 7: то же самое, что и ints[7]

Обратите внимание, что метод subarray() не создает копии данных. Он просто возвращает новое представление тех же самых значений:

ints[9] = -1;  // Изменить значение в оригинальном массиве и... 
last3[2]      // => -1: изменения коснулись фрагмента массива 

Тот факт, что метод subarray() возвращает новое представление существующего массива, раскрывает важную особенность типизированных массивов: все они являются представлениями участка памяти, который называется ArrayBuffer. Каждый типизированный массив имеет три свойства, связывающие его с лежащим в его основе буфером:

last3.buffer == ints.buffer // => true: оба - представления одного буфера 
last3.byteOffset  // => 14: это представление начинается с 14-го байта в буфере 
last3.byteLength  // => 6: размер представления 6 байт (3 16-битных целых) 

Сам объект ArrayBuffer имеет только одно свойство, возвращающее его длину:

last3.byteLength        // => 6: размер представления 6 байт
last3.buffer.byteLength // => 20: но буфер имеет размер 20 байт

ArrayBuffers - это всего лишь последовательность байтов. К этим байтам можно обращаться с помощью типизированных массивов, но сам объект ArrayBuffer не является типизированным массивом. Однако будьте внимательны: объект ArrayBuffer можно индексировать числами, как любой другой объект JavaScript, но это не обеспечивает доступ к байтам в буфере:

var bytes = new Uint8Array(8); // Разместить 8 байтов 
bytes[0] = 1;                  // Записать в первый байт значение 1 
bytes.buffer[0]                // => undefined: буфер не имеет индекса 0
bytes.buffer[1] = 255;         // Попробовать некорректно записать значение
                               // в байт буфера 
bytes.buffer[1]                // => 255: это обычное JavaScript-свойство
bytes[1]                       // => 0: строка выше не изменила байт 

Имеется возможность создавать объекты ArrayBuffer непосредственно, вызовом конструктора ArrayBuffer(), и на основе имеющегося объекта ArrayBuffer можно создать любое число представлений типизированного массива:

var buf = new ArrayBuffer(1024*1024);       // Один Мбайт 
var asbytes = new Uint8Array(buf);          // Представление в виде байтов 
var asints = new Int32Array(buf);           // В виде 32-битных целых со знаком 
var lastK = new Uint8Array(buf,1023*1024);  // Последний Кбайт в виде байтов 
var ints2 = new Int32Array(buf, 1024, 256); // 2-й Кбайт в виде 256 целых чисел

Типизированные массивы позволяют представлять одну и ту же последовательность байтов в виде целых чисел размером 8, 16, 32 или 64 бита. Это поднимает проблему «порядка следования байтов», т. е. порядка, в каком следуют байты при объединении в более длинные слова. Для эффективности типизированные массивы используют порядок следования байтов, определяемый аппаратным обеспечением. В системах с обратным порядком следования байтов байты числа располагаются в буфере ArrayBuffer в порядке от младшего к старшему. На платформах с прямым порядком следования байтов байты располагаются в порядке от старшего к младшему. Определить порядок следования байтов на текущей платформе, где выполняется сценарий, можно следующим образом:

// Если целое число 0x00000001 располагается в памяти в виде
// последовательности байтов 01 00 00 00, следовательно, сценарий выполняется
// на платформе с обратным порядком следования байтов. На платформе с прямым
// порядком следования байтов мы получим байты 00 00 00 01.
var little_endian = new Int8Array(new Int32Array([1]).buffer)[0] === 1;

В настоящее время наибольшее распространение получили процессорные архитектуры с обратным порядком следования байтов. Однако многие сетевые протоколы и некоторые двоичные форматы файлов требуют, чтобы байты следовали в прямом порядке. В разделе 22.6 вы узнаете, как использовать объекты ArrayBuffer для хранения байтов, прочитанных из файлов или полученных из сети. В подобных ситуациях нельзя просто полагаться на то, что порядок следования байтов, поддерживаемый аппаратной частью, совпадает с порядком следования байтов в данных. Вообще, при работе с внешними данными, для представления данных в виде массива отдельных байтов можно использовать Int8Array и Uint8Array, но нельзя использовать другие виды типизированных массивов для представления данных в виде массивов многобайтовых слов. Вместо этого можно использовать класс DataView, который определяет методы чтения и записи значений из буфера ArrayBuffer, использующие явно указанный порядок следования байтов:

var data;    // Предположим, что данные в ArrayBuffer получены из сети
var view = DataView(data);    // Создать представление буфера 
var int = view.getInt32(0);   // 32-битное* целое со знаком с прямым порядком
                              // следования байтов, начиная с 0-го байта 
int = view.getInt32(4,false); // Следующее 32-битное целое, также с прямым 
                              // порядком следования байтов 
int = view.getInt32(8,true)   // Следующие 4 байта как целое со знаком 
                              // и с обратным порядком следования байтов 
view.setInt32(8,int,false);   // Записать его обратно, в формате с прямым 
                              // порядком следования байтов 

Класс DataView определяет восемь методов get для каждого из восьми видов типизированных массивов. Они имеют такие имена, как getlnt16(), getUint32() и getFloat64(). В первом аргументе они принимают смещение значения в байтах в буфере ArrayBuffer. Все эти методы чтения, кроме getlnt8() и getUint8(), принимают логическое значение во втором необязательном аргументе. Если второй аргумент отсутствует или имеет значение false, используется прямой порядок следования байтов. Если второй аргумент имеет значение true, используется обратный порядок следования байтов.

Класс DataView определяет восемь соответствующих методов set, которые записывают значения в буфер ArrayBuffer. В первом аргументе этим методам передается смещение начала значения. Во втором аргументе - записываемое значение. Все методы, кроме setlnt8() и setUint8(), принимают необязательный третий аргумент. Если аргумент отсутствует или имеет значение false, значение записывается в формате с прямым порядком следования байтов, когда первым следует старший байт. Если аргумент имеет значение true, значение записывается в формате с обратным порядком следования байтов, когда первым записывается младший байт.


содержание 22.6. Двоичные объекты содержание

Двоичный объект (Blob) - это нетипизированная ссылка, или дескриптор, блока данных. Название «Blob» пришло из мира баз данных SQL, где оно расшифровывается как «Binary Large Object» (большой двоичный объект). В языке JavaScript двоичные объекты часто представляют двоичные данные, и они могут иметь большой размер, но это совсем необязательно: двоичный объект Blob может также представлять содержимое небольшого текстового файла. Двоичные объекты непрозрачны, т. е. являются своего рода черными ящиками: все, что можно с ними сделать, - это определить их размер в байтах, узнать MIME-тип и разбить на более мелкие двоичные объекты:

var blob = ... // Как получить двоичный объект, будет показано ниже
blob.size // Размер двоичного объекта в байтах
blob.type // MIME-тип двоичного объекта или "", если неизвестен
var subblob = blob.slice(0,1024, "text/plain"); // Первый килобайт - как текст
var last = blob.slice(blob.size-1024, 1024); // Последний килобайт -
// как нетипизированные данные

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

Сами по себе двоичные объекты не представляют особого интереса, но они служат важным механизмом обмена данными для некоторых прикладных интерфейсов в языке JavaScript, которые работают с двоичными данными. На рис. 22.2 показано, как можно обмениваться двоичными объектами во Всемирной паутине, читать и сохранять их в локальной файловой системе, в локальных базах данных и даже обмениваться ими с другими окнами и фоновыми потоками выполнения. Он также показывает, как можно получить содержимое двоичного объекта в виде текста, типизированного массива или URL-адреса.

Рис. 22.2. Двоичные объекты и прикладные интерфейсы, использующие их

В следующих подразделах демонстрируются простые способы получения и использования двоичных объектов. Более сложные приемы, связанные с использованием локальной файловой системы и базами данных на стороне клиента, будут описаны далее в отдельных разделах.


содержание 22.6.1. Файлы как двоичные объекты

Элемент <input type="file"> изначально предназначался для обеспечения возможности выгрузки файлов в HTML-формах. Производители браузеров всегда с особым тщанием подходили к реализации этого элемента, чтобы позволить ему выгружать только те файлы, которые были явно выбраны пользователем. Сценарии не смогут присвоить имя файла свойству value этого элемента, поэтому они лишены возможности выгружать произвольные файлы, находящиеся на компьютере пользователя. Недавно производители браузеров расширили возможности этого элемента с целью обеспечить доступ к выбранным пользователем файлам на стороне клиента. Обратите внимание, что возможность читать содержимое выбранных пользователем файлов клиентскими сценариями не более и не менее опасна, чем выгружать эти файлы на сервер.

В браузерах, поддерживающих доступ к локальным файлам, свойство files элемента <input type="file"> будет ссылаться на объект FileList. Это объект, подобный массиву, элементами которого являются объекты File, соответствующие файлам, выбранным пользователем. Объект File - это двоичный объект Blob, который имеет дополнительные свойства name и lastModifiedDate:

<script>
// Выводит информацию о выбранных файлах 
function fileinfo(files) {
    for(var i = 0; i < files.length; i++) { // files - подобный массиву объект
        var f = files[i];
        console.log(f.name, // Только имя: без пути к файлу 
            f.size, f.type, // размер и тип - свойства Blob 
            f.lastModifiedDate); // еще одно свойство объекта File 
        }
    }
</script>
<!-- Разрешить выбор нескольких файлов изображений и передать их fileinfo()-->
<input type="file" accept="image/*" multiple onchange="fileinfo(this.files)"/>

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

В дополнение к файлам, выбранным с помощью элемента <input>, пользователь может также дать сценарию доступ к локальным файлам, буксируя их мышью и сбрасывая в окно браузера. Когда приложение получает событие «drop», свойство dataTransfer.files объекта события будет содержать ссылку на объект FileList, связанный с этой операцией буксировки, если в ней участвовали файлы. Прикладной интерфейс буксировки объектов мышью рассматривался в разделе 17.7, а подобное использование файлов демонстрируется в примере 22.10.

содержание 22.6.2. Загрузка двоичных объектов

Глава 18 охватывает тему выполнения HTTP-запросов с помощью объекта XMLHttpRequest и также описывает некоторые новые возможности, определяемые проектом спецификации «XMLHttpRequest Level 2» (XHR2). На момент написания этих строк спецификация XHR2 определяла способ загрузки содержимого URL-адреса в виде двоичного объекта, но реализации браузеров пока не поддерживали его. Поскольку программный код не может быть протестирован, этот раздел является лишь схематическим описанием прикладного интерфейса, предусматриваемого спецификацией XHR2 для работы с двоичными объектами. Пример 22.9 демонстрирует простой способ загрузки двоичного объекта из Веб. Сравните его с примером 18.2, который загружает содержимое URL-адреса как простой текст.

Пример 22.9 (js). Загрузка двоичного объекта с помощью объекта XMLHttpRequest

// Запрашивает методом GET содержимое URL в виде двоичного объекта и передает
// его указанной функции обратного вызова. Этот программный код не тестировался:
// на тот момент, когда он был написан, браузеры еще не поддерживали этот
// прикладной интерфейс, 
function getBlob(url, callback) {
    var xhr = new XMLHttpRequest();  // Создать новый объект XHR 
    xhr.open("GET", url);            // Указать URL-адрес 
    xhr.responseType = "blob"        // Желательно получить двоичный объект 
    xhr.onload = function() {        // onload проще, чем onreadystatechange 
        callback(xhr.response);      // Передать ответ функции обратного вызова
    }                                // Отметьте: .response, а не .responseText
    xhr.send(null);                  // Послать запрос 
}
Если загружаемый двоичный объект слишком велик и вам хотелось бы начать его обработку уже в процессе загрузки, можно задействовать обработчик события onprogressB комплексе с приемами чтения двоичных объектов, которые демонстрируются в разделе 22.6.5.

содержание 22.6.3. Конструирование двоичных объектов

Двоичные объекты часто представляют фрагменты данных из внешних ресурсов, таких как локальные файлы, URL-адреса или базы данных. Но иногда веб-приложению требуется создать собственный двоичный объект, чтобы выгрузить его на веб-сервер, сохранить в файле или в базе данных, или передать его фоновому потоку выполнения. Создать объект Blob из имеющихся данных можно с помощью объекта BlobBuilder:

// Создать новый объект BlobBuilder
var bb = new BlobBuilder();
// Добавить в двоичный объект строку и отметить ее конец символом NUL
bb.append("This blob contains this text and 10 big-endian 32-bit signed ints.");

bb.append("\0"); // Добавить символ NUL, чтобы отметить конец строки
// Сохранить некоторые данные в объекте ArrayBuffer
var ab = new ArrayBuffer(4*10);
var dv = new DataView(ab);
for(var i = 0; i < 10; i++) dv.setInt32(i*4,i);
// Добавить ArrayBuffer в двоичный объект
bb.append(ab);
// Теперь извлечь полученный двоичный объект, указав искусственный МIМЕ-тип
var blob = bb.getBlob("x-optional/mime-type-here");

В начале этого раздела мы узнали, что двоичные объекты имеют метод slice(), который разбивает их на фрагменты. Точно так же имеется возможность объединять двоичные объекты, передавая их методу append() объекта BlobBuilder

содержание 22.6.4. URL-адреса, ссылающиеся на двоичные объекты

В предыдущих разделах было показано, как можно получить или создать двоичный объект. Теперь мы поговорим о том, что можно делать с полученными или созданными двоичными объектами. Самое простое, что можно сделать с двоичным объектом, - это создать URL-адрес, ссылающийся на него. Такой URL-адрес можно использовать везде, где используются обычные URL-адреса: в элементах DOM, в таблицах стилей и даже при работе с объектом XMLHttpRequest. Создаются URL-адреса, ссылающиеся на двоичные объекты, с помощью функции createObjectURL(). На момент написания этих строк проект спецификации и Firefox 4 помещали эту функцию в глобальный объект URL, а браузер Chrome и библиотека Webkit добавляли свой префикс к имени этого объекта, называя его webkitURL. Ранние версии спецификации (и ранние реализации браузеров) помещали эту функцию непосредственно в объект Window. Чтобы получить возможность переносимым образом создавать URL-адреса, ссылающиеся на двоичные объекты, можно определить вспомогательную функцию, как показано ниже:

var getBlobURL = (window.URL && URL.createObjectURL.bind(URL)) ||
  (window.webkitURL && webkitURL.createObjectURL.bind(webkitURL)) ||
  window.createObjectURL;

Фоновые потоки выполнения также могут использовать этот прикладной интерфейс и обращаться к тем же функциям, в том же объекте URL (или webkitURL).

Если передать двоичный объект функции createObjectURL(), она вернет URL-адрес (в виде обычной строки). Этот адрес начинается с названия схемы blob://, за которой следует короткая строка, ссылающаяся на двоичный объект с некоторым уникальным идентификатором. Обратите внимание, что эти URL-адреса совершенно не похожи на URL-адреса data://, которые представляют свое собственное содержимое. URL-адрес, ссылающийся на двоичный объект, - это просто ссылка на объект Blob, хранящийся в памяти браузера или на диске. URL-адреса вида blob:// также отличаются от URL-адресов file://, которые ссылаются непосредственно на файл в локальной файловой системе, давая возможность увидеть путь к файлу, позволяя просматривать содержимое каталогов и тем самым затрагивая проблемы безопасности.

Пример 22.10 демонстрирует два важных приема. Во-первых, он реализует «площадку для сброса», которая обрабатывает события механизма буксировки мышью (drag-and-drop), имеющие отношение к файлам. Во-вторых, когда пользователь сбросит один или более файлов на эту «площадку», с помощью функции createObjectURL() для каждого из файлов будет создан URL-адрес и элемент <img>, чтобы отобразить миниатюры изображений, на которые ссылаются эти URL-адреса.


Пример 22.10. Отображение файлов изображений с использованием URL-адресов двоичных объектов

<!DOCTYPE html>
<html><head>
<script>
// На момент написания этих строк создатели Firefox и Webkit еще не пришли
// к соглашению об именовании функции createObjectURL()
var getBlobURL = (window.URL && URL.createObjectURL.bind(URL)) ||
  (window.webkitURL && webkitURL.createObjectURL.bind(webkitURL)) ||
  window.createObjectURL;
var revokeBlobURL = (window.URL && URL.revokeObjectURL.bind(URL)) ||
  (window.webkitURL && webkitURL.revokeObjectURL.bind(webkitURL)) ||
  window.revokeObjectURL;

// После загрузки документа добавить обработчики событий к элементу droptarget,
// чтобы он мог обрабатывать сбрасываемые файлы
window.onload = function() {
  // Отыскать элемент, к которому следует добавить обработчики событий,
  var droptarget = document.getElementById("droptarget");
  // Выделяет элемент droptarget изменением рамки, когда пользователь
  // буксирует файлы над ним. 
  droptarget.ondragenter = function(e) {
    // Игнорировать, если буксируется что-то иное, не являющееся файлом.
    // Когда будет реализована поддержка атрибута dropzone, определяемого
    // спецификацией HTML5, это позволит упростить обработчик,
    var types = e.dataTransfer.types;
    if (!types ||
      (types.contains && types.contains("Files")) ||
      (types.indexOf && types.indexOf("Files") != -1)) {
      droptarget.classList.add("active"); // Выделить элемент droptarget
      return false;                       // Нас интересует
    }                                     // буксируемый объект
  };
  // Снимает выделение площадки сброса, как только пользователь
  droptarget.ondragleave = function() { // отбуксирует файл за ее пределы
    droptarget.classList.remove("active");
  };

  // Этот обработчик просто сообщает броузеру продолжать посылать события
  droptarget.ondragover = function(e) { return false; };

  // Когда пользователь сбросит файлы, необходимо получить их URL-адреса
  // и отобразить миниатюры.
  droptarget.ondrop = function(e) {
    var files = e.dataTransfer.files;             // Сброшенные файлы 
    for(var i = 0; i < files.length; i++) {       // Обойти все файлы в цикле 
      var type = files[i].type;
      if (type.substring(0,6) !== "image/")       // Пропустить не являющиеся 
        continue;                                 // изображениями 
      var img = document.createElement("img"); ); // Создать элемент <img>

      img.src = getBlobURL(files[i]);             // URL blob:// в <img> 
      img.onload = function() {                   // После загрузки изобр. 
        this.width = 100;                         // установить его размеры 
        document.body.appendChild(this);          // и вставить в документ. 
        revokeBlobURL(this.src);                  // Предотвратить утечки памяти!
      }
    }

    droptarget.classList.remove("active");    // Снять выделение 
    return false;                             // Событие сброса обработано
  }
};
</script>
<style> /* Простые стили для оформления площадки сброса */
#droptarget { border: solid black 2px; width: 200px; height: 200px; }
#droptarget.active { border: solid red 4px; }
</style>
</head>
<body> <!-- Изначально в документе имеется только площадка сброса -->
<div id="droptarget">C6pocьте сюда файлы изображений</div>
</body>
</html>

URL-адреса двоичных объектов имеют то же происхождение (раздел 13.6.2), что и сценарии, создавшие их. Это делает их более универсальными по сравнению с URL-адресами file://, которые имеют иное происхождение, из-за чего последние сложнее использовать в веб-приложениях. URL-адреса двоичных объектов считаются допустимыми только в документах с общим происхождением. Если, например, с помощью метода postMessage() передать URL-адрес двоичного объекта в окно с документом, имеющим другое происхождение, для этого окна URL-адрес будет бессмысленным.

URL-адреса двоичных объектов не являются постоянными. Такой URL-адрес перестанет быть действительным, как только пользователь закроет документ или выйдет из документа, в котором был создан этот URL-адрес. Нельзя, например, сохранить URL-адрес двоичного объекта в локальном хранилище и затем повторно использовать его, когда пользователь начнет новый сеанс работы с веб-приложением. Имеется также возможность вручную «прекращать» действие URL-адреса двоичного объекта вызовом метода URLrevokeObjectURL() (или webkitURLrevokeObjectURL()), и, как вы могли заметить, пример 22.10 использует эту возможность. Это связано с проблемой управления памятью. После того как миниатюра будет отображена, двоичный объект становится ненужным и его следует сделать доступным для утилизации сборщиком мусора. Но если веб-браузер будет хранить ссылку на созданный двоичный объект в виде URL-адреса двоичного объекта, он не сможет утилизировать его, даже если он не будет больше использоваться в приложении. Интерпретатор JavaScript не может следить за использованием строк, и если URL-адрес по-прежнему остается допустимым, он вправе предположить, что этот адрес все еще используется. Это означает, что интерпретатор не сможет утилизировать двоичный объект, пока не будет прекращено действие URL-адреса. Пример 22.10 работает с локальными файлами, не требующими утилизации, но представьте, какие серьезные утечки памяти могут быть при работе с двоичными объектами, создаваемыми в памяти методом BlobBuilder или загружаемыми с помощью объекта XMLHttpRequest и сохраняемыми во временных файлах.

URL-схема blob:// явно проектировалась как упрощенный вариант схемы http://, и при обращении по URL-адресу blob:// браузеры должны действовать как своеобразные HTTP-серверы. При запросе недействительного URL-адреса двоичного объекта браузер должен послать в ответ код состояния 404 «Not Found». При запросе URL-адреса двоичного объекта с другим происхождением браузер должен вернуть код состояния 403 «Not Allowed». URL-адреса двоичных объектов могут использоваться только в запросах GET, и в случае успешного выполнения запроса браузер должен отправить код состояния 200 «ОК» и заголовок Content-Type со значением свойства type двоичного объекта Blob. Поскольку URL-адреса двоичных объектов действуют как упрощенные URL-адреса http://, их содержимое можно «загружать» с помощью объекта XMLHttpRequest. (Однако, как будет показано в следующем разделе, содержимое двоичного объекта можно прочитать более непосредственным способом - с помощью объекта FileReader.)


содержание 22.6.5. Чтение двоичных объектов

До сих пор двоичные объекты были для нас непрозрачными фрагментами данных, которые позволяют обращаться к их содержимому только косвенным способом, посредством URL-адресов двоичных объектов. Объект FileReader дает возможность символы или байты, хранящиеся в двоичном объекте, и его можно рассматривать как противоположность объекту BlobBuilder. (Для него больше подошло бы имя BlobReader, поскольку он может работать с любыми двоичными объектами, а не только с файлами.) Так как двоичные объекты могут быть очень большими и храниться в файловой системе, прикладной интерфейс чтения их содержимого действует асинхронно, во многом подобно тому, как действует объект XMLHttpRequest. Фоновым потокам доступна также синхронная версия прикладного интерфейса в виде объекта FileReaderSync, хотя они могут использовать и асинхронную версию. Чтобы воспользоваться объектом FileReader, сначала необходимо создать его экземпляр с помощью конструктора FileReader(). Затем определить обработчики событий. Обычно в приложениях определяются обработчики событий «load» и «error» и иногда - обработчик событий «progress». Сделать это можно посредством свойств onload, onerror и onprogress или с помощью стандартного метода addEventListener(). Кроме того, объекты FileReader генерируют события «loadstart», «loadend» и «abort», которые соответствуют одноименным событиям в объекте XMLHttpRequest (раздел 18.1.4).

После создания объекта FileReader и регистрации необходимых обработчиков событий можно передать двоичный объект, содержимое которого требуется прочитать, одному из четырех методов: readAsText(), readAsArrayBuffer(), readAsDataURL() и readAsBinaryString(). (Разумеется, можно сначала вызвать один из этих методов и лишь потом зарегистрировать обработчики событий - благодаря однопоточной природе JavaScript, о которой рассказывалось в разделе 22.4, обработчики событий не могут быть вызваны, пока ваша функция не вернет управление и браузер не сможет продолжить цикл обработки событий.) Первые два метода являются наиболее важными, и только они будут описаны здесь. Каждый из этих методов чтения принимает двоичный объект Blob в первом аргументе. Метод readAsText() принимает необязательный второй аргумент, определяющий имя кодировки текста. Если кодировка не указана, метод автоматически будет обрабатывать текст в кодировках ASCII и UTF-8 (а также текст в кодировке UTF-16 с маркером порядка следования байтов (byte-order mark, BOM)).

По мере чтения содержимого указанного двоичного объекта объект FileReader будет обновлять значение своего свойства readyState. Первоначально это свойство получает значение 0, показывающее, что еще ничего не было прочитано. Когда становятся доступны какие-нибудь данные, оно принимает значение 1 и по окончании чтения - значение 2. Свойство result хранит частично или полностью прочитанные данные в виде строки или объекта ArrayBuffer. Веб-приложения обычно не опрашивают свойства readyState и result и вместо этого используют обработчик события onprogress или onload.

Пример 22.11 демонстрирует, как использовать метод readAsText() для чтения локальных текстовых файлов, выбранных пользователем.

Пример 22.11. Чтение текстовых файлов с помощью объекта FileReader

<script>
// Читает указанный текстовый файл и отображает его в элементе <рге> ниже 
function readfile(f) {
    var reader = new FileReader();  // Создать объект FileReader 
    reader.readAsText(f);           // Прочитать файл 
    reader.onload = function() {    // Определить обработчик события 
        var text = reader.result;   // Это содержимое файла 
        var out = document.getElementById("output");    // Найти элемент output 
        out.innerHTML = "";                             // Очистить его 
        out.appendChild(document.createTextNode(text)); // Вывести содержимое 
    }// файла 
    reader.onerror = function(e) {  // Если что-то пошло не так 
        console.log("Error", e);    // Вывести сообщение об ошибке 
    };
}
</script>
Select the file to display:// Выберите файл для отображения: 
<input type="file" onchange="readfile(this.files[0])"></input>
<pre id="output"></pre>

Метод readAsArrayBuffer() похож на метод readAsText(), за исключением того, что требует приложить чуть больше усилий при работе с результатом, возвращая объект ArrayBuffer вместо строки. Пример 22.12 демонстрирует использование метода readAsArrayBuffer() для чтения первых четырех байтов из файла в виде целого числа с прямым порядком следования байтов.

Пример 22.12. Чтение первых четырех байтов из файла

<script>
// Исследует первые 4 байта в указанном двоичном объекте. Если это "сигнатура", 
// определяющая тип файла, асинхронно устанавливает свойство двоичного объекта, 
function typefile(file) {
    var slice = file.slice(0,4);       // Читать только первые 4 байта 
    var reader = new FileReader();     // Создать асинхронный FileReader 
    reader.readAsArrayBuffer(slice);   // Прочитать фрагмент файла 
    reader.onload = function(e) {
        var buffer = reader.result;           // Результат - ArrayBuffer 
        var view = new DataView(buffer);      // Получить доступ к результату 
        var magic = view.getUint32(0, false); // 4 байта, прямой порядок 
        switch(magic) {                       // Определить по ним тип файла 
        case 0x89504E47: file.verified_type = "image/png"; break;
        case 0x47494638: file.verified_type = "image/gif"; break;
        case 0x25504446: file.verified_type = "application/pdf"; break;
        case 0x504b0304: file.verified_type = "application/zip"; break;
        }
        console.log(file.name, file.verified_type);
    };
}
</script>
<input type="file" onchange="typefile(this.files[0])"></input>

содержание 22.7. Прикладной интерфейс к файловой системе содержание

В разделе 22.6.5 вы познакомились с классом FileReader, использовавшимся для чтения содержимого файлов, выбираемых пользователем, или любых двоичных объектов. Типы File и Blob определяются проектом спецификации, известной как «File API». Проект другой спецификации, еще более новой, чем «File API», дает веб-приложениям управляемый доступ к частной файловой системе, где они могут писать в файлы, читать файлы, создавать каталоги, читать содержимое каталогов и т. д. На момент написания этих строк данный прикладной интерфейс к файловой системе был реализован только в браузере Google Chrome. Это мощная и важная форма локального хранилища, поэтому она будет описана здесь, несмотря на то что ее прикладной интерфейс еще менее стабилен, чем другие прикладные интерфейсы, описываемые в этой главе. Данный раздел охватывает лишь основные задачи, выполняемые с файловой системой, и не демонстрирует всех возможностей прикладного интерфейса. Так как обсуждаемый здесь прикладной интерфейс является новым и нестабильным, он не описывается в справочном разделе этой книги. Работа с файлами в локальной файловой системе является многоэтапным процессом. Прежде всего, необходимо получить объект, представляющий саму файловую систему. Сделать это можно с помощью синхронного прикладного интерфейса в фоновом потоке или асинхронного - в основном потоке выполнения:

// Метод синхронного получения файловой системы. Принимает параметры,
// определяющие срок существования файловой системы и ее размер.
// Возвращает объект файловой системы или возбуждает исключение,
var fs = requestFileSystemSync(PERSISTENT, 1024*1024);
// Асинхронная версия принимает функции обратного вызова для обработки
// успешного или неудачного создания файловой системы
requestFileSystem(TEMPORARY, // срок существования
    50*1024*1024, // размер: 50 Мбайт
    function(fs) { // будет вызвана с объектом файловой системы
        // Здесь используется объект fs
    }
    function(e) { // будет вызвана с объектом ошибки
        console.log(e); // Или как-то иначе обработать ошибку
    });

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

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

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

¹ На момент написания этих строк браузер Chrome не запрашивал разрешение у пользователя, но требовал, чтобы он был запущен с ключом командной строки — unlimited- quota-for-files

Объект файловой системы, полученный одним из методов, представленных выше, имеет свойство root, ссылающееся на корневой каталог файловой системы. Это объект DirectoryEntry, и он может иметь вложенные каталоги, представленные собственными объектами DirectoryEntry. Каждый каталог в файловой системе может содержать файлы, представленные объектами FileEntry. Объект DirectoryEntry определяет методы для получения объектов DirectoryEntry и FileEntry по пути в файловой системе (они могут создавать новые каталоги и файлы, если указанное имя не существует). Объект DirectoryEntry также определяет фабричный метод createReader(), возвращающий объект DirectoryReader, который позволяет получить список содержимого каталога.

Класс FileEntry определяет метод для получения объекта File (двоичный объект Blob), представляющий содержимое файла. Получив этот объект, его содержимое можно прочитать с помощью объекта FileReader (как показано в разделе 22.6.5). Объект FileEntry определяет еще один метод, возвращающий объект FileWriter, который можно использовать для записи в файл.

Операции чтения и записи с использованием этого прикладного интерфейса представляют собой многоэтапный процесс. Прежде всего, необходимо получить объект файловой системы. Затем, используя свойство root этого объекта, необходимо отыскать (и при необходимости создать) объект FileEntry, представляющий требуемый файл. Затем с помощью объекта FileEntry нужно получить объект File или ileWriter для выполнения операции чтения или записи. Этот процесс становится особенно сложным при использовании асинхронного прикладного интерфейса:

// Читает текстовый файл "hello.txt" и выводит его содержимое. При использовании
// асинхронного прикладного интерфейса глубина вложенности функций достигает
// четырех уровней. Этот пример не включает определения функций обработки ошибок.
requestFileSystem(PERSISTENT, 10*1024*1024, function(fs) { // Получить объект ФС
    fs.root.getFile("hello.txt", {}, function(entry) { // Получить FileEntry
        entry.file(function(file) { // Получить File
            var reader = new FileReader();
            reader.readAsText(file);
            reader.onload = function() { // Получить содержимое файла
                console.log(reader.result);
            };
        });
    });
});

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

Пример 22.13. Использование асинхронного прикладного интерфейса доступа к файловой системе

/*
 * Следующие функции были протестированы в Google Chrome 10.0 dev. 
 * Вам может потребоваться запустить Chrome со следующими ключами:
 * --unlimited-quota-for-files : разрешает доступ к файловой системе
 * --allow-file-access-from-files : разрешает тестировать из URL file:// 
 */
// Многие асинхронные функции, используемые здесь, принимают 
// необязательные функции обратного вызова для обработки ошибок. 
// Следующая функция просто выводит сообщение об ошибке. 
function logerr(e) { console.log(e); }

// requestFileSystem() возвращает локальную файловую систему, доступную 
// только приложениям с указанным происхождением. Приложение может читать и
// писать файлы в ней, но не может получить доступ к остальной файловой системе.
var filesystem; // Предполагается, что эта переменная будет инициализирована 
                // перед вызовом функции, объявленной ниже. 
requestFileSystem(PERSISTENT,  // Или TEMPORARY для кэширования файлов 
         10*1024*1024,         // Требуется 10 Мбайт 
         function(fs) {        // После выполнения вызвать эту функцию 
           filesystem = fs;    // Просто сохранить ссылку на файловую систему 
         },                    // в глобальной переменной. 
         logerr);              // Вызвать эту функцию в случае ошибки 

// Читает содержимое указанного файла как текст 
//и передает его функции обратного вызова, 
function readTextFile(path, callback) {
  // Вызвать getFile(), чтобы получить объект FileEntry для файла 
  // с указанным именем 
  filesystem.root.getFile(path, {}, function(entry) {
    // При вызове этой функции передается объект FileEntry, 
    // соответствующий файлу. 
    // Теперь следует вызвать метод FileEntry.file(), чтобы получить объект File
    entry.file(function(file) {      // Вызвать с объектом File 
      var reader = new FileReader(); // Создать объект FileReader 
      reader.readAsText(file);       // И прочитать файл 
      reader.onload = function() {   // В случае успешного чтения 
        callback(reader.result);     // Передать данные функции callback 
      } 
      reader.onerror = logerr;      // Сообщить об ошибке в readAsText() 
    }, logerr);                     // Сообщить об ошибке в file() 
  }, 
  logerr);                          // Сообщить об ошибке в getFile() 
}

// Добавляет указанное содержимое в конец файла с указанным именем, 
// создает новый файл, если файл с указанным именем не существует. 
// Вызывает callback по завершении операции, 
function appendToFile(path, contents, callback) {
   filesystem.root - это корневой каталог, 
  filesystem.root.getFile( // Получить объект FileEntry 
    path,                  // Имя и путь к требуемому файлу 
    {create:true},         // Создать, если не существует 
    function(entry) {      // Вызвать эту функцию, когда файл будет найден
      entry.createWriter(  // Создать для файла объект FileWriter 
        function(writer) { // Вызвать эту функцию после создания 
          // По умолчанию запись производится в начало файла. 
          // Нам же требуется выполнить запись в конец файла 
          writer.seek(writer.length); // Переместиться в конец файла 
          
          // Преобразовать содержимое файла в объект Blob. Аргумент contents
          // может быть строкой, объектом Blob или объектом ArrayBuffer. 
          var bb = new BlobBuilder()
          bb.append(contents);
          var blob = bb.getBlob();
          
          // Записать двоичный объект в файл 
          writer.write(blob);
          writer.onerror = logerr;  // Сообщить об ошибке в write() 
          if (callback)             // Если указана функция callback 
            writer.onwrite = callback; // вызвать в случае успеха 
        },
        logerr);   // Сообщить об ошибке в createWriter() 
    },
    logerr);       // Сообщить об ошибке в getFile() 

// Удаляет файл с указанным именем, вызывает callback по завершении операции, 
function deleteFile(name, callback) {
  filesystem.root.getFile(name, {},    // Получить FileEntry по имени файла 
              function(entry) {        // Передать FileEntry сюда 
                entry.remove(callback, // Удалить файл 
                       logerr);        // Или сообщить 
              },                       // об ошибке в remove() 
              logerr);                 // Сообщить об ошибке в getFile() 
}

// Создает новый каталог с указанным именем 
function makeDirectory(name, callback) {
  filesystem.root.getDirectory(name, // Имя создаваемого каталога 
                 {       // Параметры 
                   create: true,     // Создать, если не сущ. 
                   exclusive:true    // Ошибка, если существует 
                 },
                 callback,           // Вызвать по завершении 
                 logerr);            // Выводить любые ошибки 
}

// Читает содержимое указанного каталога и передает его 
// в виде массива строк указанной функции callback 
function listFiles(path, callback) {
  // Если каталог не указан, получить содержимое корневого каталога. 
  // Иначе отыскать каталог с указанным именем и вернуть список 
  // с его содержимым (или сообщить об ошибке поиска). 
  if (!path) getFiles(filesystem.root);
  else filesystem.root.getDirectory(path, {}, getFiles, logerr);

  function getFiles(dir) {            // Эта функция используется выше 
    var reader = dir.createReader();  // Объект DirectoryReader 
    var list = [];                    // Для сохранения имен файлов 
    reader.readEntries(handleEntries, // Передать функции ниже 
              logerr);                // или сообщить об ошибке. 
    // Чтение каталогов может превратиться в многоэтапный процесс. 
    // Необходимо сохранять результаты вызовов readEntries(), пока не будет 
    // получен пустой массив. На этом операция будет закончена, 
    // и полный список можно будет передать функции callback. 
    function handleEntries(entries) {
      if (entries.length == 0) callback(list);     // Операция закончена 
      else {
        // Иначе добавить эти записи в общий список и запросить 
        // очередную порцию. Объект, подобный массиву, содержит 
        // объекты FileEntry, и нам следует получить имя для каждого. 
        for(var i = 0; i < entries.length; i++) {
          var name = entries[i].name;              // Получить имя записи 
          if (entries[i].isDirectory) name += "/"; // Пометить каталоги 
          list.push(name);                         // Добавить в список 
        }
        // Получить следующую порцию записей 
        reader.readEntries(handleEntries, logerr);
      }
    }
  }
}

Работать с файлами и с файловой системой намного проще в фоновых потоках выполнения, где допускается выполнять блокирующие вызовы и можно использовать синхронный прикладной интерфейс. Пример 22.14 определяет те же функции для работы с файловой системой, что и пример 22.13, но использует синхронный прикладной интерфейс, и потому получился намного короче.

Пример 22.14. Синхронный прикладной интерфейс для работы с файловой системой

// Утилиты для работы с файловой системой, использующие синхронный прикладной 
// интерфейс, предназначенный для фоновых потоков выполнения 
var filesystem = requestFileSystemSync(PERSISTENT, 10*1024*1024);

function readTextFile(name) {
    // Получить объект File из объекта FileEntry из корневого DirectoryEntry 
    var file = filesystem.root.getFile(name).file();
    // Использовать для чтения синхронную версию FileReader 
    return new FileReaderSync().readAsText(file);
}

function appendToFile(name, contents) {
    // Получить FileWriter из FileEntry из корневого DirectoryEntry 
    var writer = filesystem.root.getFile(name, {create:true}).createWriter();
    writer.seek(writer.length);  // Начать запись с конца файла 
    var bb = new BlobBuilder()   // Собрать содержимое в виде объекта Blob 
    bb.append(contents);
    writer.write(bb.getBlob());   // Записать двоичный объект в файл 
}

function deleteFile(name) {
    filesystem.root.getFile(name).remove();
}

function makeDirectory(name) {
    filesystem.root.getDirectory(name, { create: true, exclusive:true });
}

function listFiles(path) {
    var dir = filesystem.root;
    if (path) dir = dir.getDirectory(path);
    
    var lister = dir.createReader();
    var list = [];
    do {
        var entries = lister.readEntries();
        for(var i = 0; i < entries.length; i++) {
            var name = entries[i].name;
            if (entries[i].isDirectory) name += "/";
            list.push(name);
        }
    } while(entries.length > 0);
    return list;
}

// Разрешить основному потоку выполнения использовать эти утилиты,
// посылая сообщения 
onmessage = function(e) {
     //Сообщение должно быть объектом: 
     { function: "appendToFile", args: ["test", "testing, testing"]} 
     // Вызвать указанную функцию с указанными аргументами 
     // и послать сообщение обратно 
     post the message back 
    var f = self[e.data.function];
    var result = f.apply(null, e.data.args);
    postMessage(result);
};

содержание 22.8. Базы данных на стороне клиента содержание

Архитектура веб-приложений традиционно была основана на поддержке HTML, CSS и JavaScript на стороне клиента и базы данных на стороне сервера. Поэтому одним из самых удивительных прикладных интерфейсов, определяемых спецификацией HTML5, является поддержка баз данных на стороне клиента. Это не прикладные интерфейсы доступа к базам данных на стороне сервера, а действительно интерфейсы доступа к базам данных, хранящимся на компьютере клиента и доступным непосредственно из программного кода на языке JavaScript, выполняемого браузером.

Прикладной интерфейс веб-хранилища, определяемый спецификацией «Web Storage» и описанный в разделе 20.1 , можно расценивать как простейшую разновидность базы данных, хранящую пары ключ/значение. Но помимо него имеются еще два прикладных интерфейса доступа к клиентским базам данных, которые являются «настоящими» базами данных. Один из них известен как «Web SQL Database» - простая реляционная база данных, поддерживающая простейшие SQL-запросы. Этот прикладной интерфейс реализован в браузерах Chrome, Safari и Opera. Он не реализован в Firefox и IE и, скорее всего, никогда не будет реализован в них. Работа над официальной спецификацией этого прикладного интерфейса была остановлена, и поддержка полноценной базы данных SQL, вероятно, никогда не приобретет статус официального стандарта или неофициальной, но широко поддерживаемой особенности веб-платформы.

В настоящее время все усилия по стандартизации сконцентрированы на другом прикладном интерфейсе к базам данных, известном как IndexedDB. Пока слишком рано описывать детали этого прикладного интерфейса (его описание отсутствует в четвертой части книги), но Firefox 4 и Chrome 11 включают его реализацию, и в этом разделе будет представлен действующий пример, демонстрирующий некоторые из наиболее важных особенностей прикладного интерфейса IndexedDB.

IndexedDB - это объектная, а не реляционная база данных, и она намного проще, чем базы данных, поддерживающие язык запросов SQL. Однако она гораздо мощнее, эффективнее и надежнее, чем веб-хранилище пар ключ/значение, доступное посредством прикладного интерфейса Web Storage. Как и в случае прикладных интерфейсов к веб-хранилищам и файловой системе, доступность базы данных IndexedDB определяется происхождением создавшего ее документа: две веб-страницы с общим происхождением могут обращаться к данным друг друга, но веб-страницы с разным происхождением - нет.

Для каждого происхождения может быть создано произвольное число баз данных IndexedDB. Каждая база данных имеет имя, которое должно быть уникальным для данного происхождения. С точки зрения прикладного интерфейса IndexedDB база данных является простой коллекцией именованных хранилищ объектов. Как следует из этого названия, хранилище объектов хранит объекты (или любые другие значения, которые можно копировать, - смотрите выше врезку «Структурированные копии»). Каждый объект должен иметь ключ, под которым он сохраняется и извлекается из хранилища. Ключи должны быть уникальными - два объекта в одном хранилище не могут иметь одинаковые ключи, - и они должны иметь естественный порядок следования, чтобы их можно было сортировать. Примерами допустимых ключей являются строки, числа и объекты Date. База данных IndexedDB может автоматически генерировать уникальные ключи для каждого объекта, добавляемого в базу данных. Однако часто объекты, сохраняемые в хранилище объектов, уже будут иметь свойство, пригодное для использования в качестве ключа. В этом случае при создании хранилища объектов достаточно просто определить «путь к ключу», определяющий это свойство. Концептуально, путь к ключу - это значение, сообщающее базе данных, как извлечь ключ из объекта.

Помимо возможности извлекать объекты из хранилища по значению первичного ключа существует также возможность выполнить поиск по значениям других свойств объекта. Чтобы обеспечить эту возможность, в хранилище объектов можно определить любое количество индексов. (Способность индексировать объекты подчеркивается самим названием «IndexedDB».) Каждый индекс определяет вторичный ключ хранимых объектов. Эти индексы в целом могут быть неуникальными, и одному и тому же ключу может соответствовать множество объектов. Поэтому в операциях обращения к хранилищу объектов с использованием индекса обычно используется курсор, определяющий прикладной интерфейс для извлечения объектов из потока результатов по одному. Курсоры могут также использоваться для обращения к хранилищу объектов с использованием диапазона ключей (или индексов), и прикладной интерфейс IndexedDB включает объект, используемый для описания диапазонов (с верхней и/или с нижней границей, включающих или не включающих границы) ключей.

IndexedDB гарантирует атомарность операций: операции чтения и записи в базу данных объединяются в транзакции, благодаря чему либо они все будут успешно выполнены, либо ни одна из них не будет выполнена, и база данных никогда не останется в неопределенном, частично измененном состоянии. Транзакции в IndexedDB реализованы намного проще, чем во многих других прикладных интерфейсах к базам данных, и мы еще вернемся к ним ниже.

Концепция прикладного интерфейса IndexedDB чрезвычайно проста. Чтобы прочитать или изменить данные, сначала необходимо открыть требуемую базу данных (указав ее имя). Затем создать объект транзакции и с помощью этого объекта отыскать требуемое хранилище объектов в базе данных, также по имени. Наконец, отыскать объект вызовом метода get() хранилища объектов или сохранить новый объект вызовом метода put(). (Или вызвать метод add(), если необходимо избежать затирания существующих объектов.) Если потребуется отыскать объекты по диапазону ключей, нужно создать объект IDBRange и передать его методу openCursor() хранилища объектов. Или, если потребуется выполнить запрос по вторичному ключу, отыскать именованный индекс в хранилище объектов и затем вызвать метод get() или openCursor() объекта-индекса.

Однако эта концептуальная простота осложняется тем фактом, что прикладной интерфейс должен быть асинхронным, чтобы веб-приложения могли пользоваться им, не блокируя основной поток выполнения браузера, управляющий пользовательским интерфейсом. (Спецификация IndexedDB определяет синхронную версию прикладного интерфейса для использования в фоновых потоках выполнения, но на момент написания этих строк ни один браузер еще не реализовал эту версию, поэтому она не рассматривается здесь.) Создание транзакции, а также поиск хранилища объектов и индексов являются простыми синхронными операциями. Но открытие базы данных, обновление хранилища объектов с помощью метода put() и получение хранилища или индекса с помощью метода get() или openCursor() являются асинхронными операциями. Все эти асинхронные методы немедленно возвращают объект запроса. В случае успешного или неудачного выполнения запроса браузер генерирует событие «success» или «error» в объекте запроса, которые можно обработать, определив обработчики событий с помощью свойств onsuccess и onerror. В обработчике onsuccess результат операции доступен в виде свойства result объекта запроса.


Одно из удобств этого асинхронного прикладного интерфейса заключается в простоте управления транзакциями. При типичном использовании прикладного интерфейса IndexedDB сначала открывается база данных. Это асинхронная операция, поэтому по ее выполнении вызывается обработчик onsuccess. В этом обработчике создается объект транзакции, и затем этот объект используется для поиска хранилища или хранилищ объектов, которые предполагается использовать. После этого производится серия вызовов методов get() и put() хранилищ объектов. Они также действуют асинхронно, поэтому непосредственно при их вызове ничего не происходит, но запросы, сгенерированные этими методами get() и put(), автоматически будут связаны с объектом транзакции. При необходимости можно отменить все операции в транзакции, ожидающие выполнения, и откатить любые уже выполненные операции вызовом метода abort() объекта транзакции. Во многих других прикладных интерфейсах к базам данных объект транзакции обычно имеет метод commit(), подтверждающий транзакцию. Однако в IndexedDB транзакция подтверждается после выхода из обработчика onsuccess, создавшего транзакцию, когда браузер вернется в цикл обработки событий, и после выполнения всех операций, запрошенных в транзакции (без запуска новых операций в их функциях обратного вызова). Такая схема, на первый взгляд, кажется слишком сложной, но в практическом применении она очень проста. При использовании прикладного интерфейса IndexedDB программист вынужден создавать объекты транзакций, чтобы получить доступ к хранилищам объектов, но в обычных ситуациях ему даже не приходится задумываться о транзакциях.

Наконец, существует один особый вид транзакций, обеспечивающий возможность работы очень важной части прикладного интерфейса IndexedDB. Создать новую базу данных с использованием интерфейса IndexedDB API очень просто: достаточно выбрать имя и запросить открытие этой базы данных. Но новая база данных создается абсолютно пустой, и она совершенно бесполезна, пока в нее не будет добавлено одно или более хранилищ объектов (и, возможно, нескольких индексов). Создавать хранилища объектов и индексы можно только внутри обработчика события onsuccess объекта запроса, возвращаемого методом setVersion() объекта базы данных. Метод setVersion() позволяет указать номер версии базы данных - в обычной ситуации номер версии должен изменяться при каждом изменении структуры базы данных. Однако более важно, что метод setVersion() неявно запускает специальную транзакцию, позволяющую вызвать метод createObjectStore() объекта базы данных и метод createlndex() хранилища объектов.

Теперь, получив представление о прикладном интерфейсе IndexedDB, вы сможете самостоятельно разобраться в примере 22.15. Этот пример использует IndexedDB для создания базы данных, отображающей почтовые индексы США в названия городов, и выполнения запросов к ней. Он демонстрирует многие, хотя и не все, основные особенности IndexedDB. На момент написания этих строк пример действовал в Firefox 4 и Chrome 11, но из-за того, что спецификация все еще продолжала меняться и реализации находились на предварительной стадии разработки, велика вероятность, что он не будет работать именно так, как описывается здесь, когда вы будете читать эти строки. Однако общая структура примера должна сохранить свою полезность для вас. Пример 22.15 получился достаточно длинным, но в нем имеется большое количество комментариев, которые облегчат его изучение.


Пример 22.15. База данных IndexedDB с почтовыми индексами США

<!DOCTYPE html>
<html>
<head>
<title>Zipcode Database</title>
<script>
// Реализации IndexedDB все еще используют префиксы в именах
var indexedDB = window.indexedDB ||  // Использовать стандартный API БД
  window.mozIndexedDB ||             // Или раннюю версию в Firefox
  window.webkitIndexedDB;            // Или раннюю версию в Chrome
// В Firefox не используются префиксы для следующих двух объектов:
var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction;
var IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange;

// Эта функция будет использоваться для вывода сообщений об ошибках
function logerr(e) {
  console.log("IndexedDB error" + e.code + ": " + e.message);a
}

// Эта функция асинхронно получает объект базы данных (при необходимости
// создает и инициализирует базу данных) и передает его функции f().
function withDB(f) {
  var request = indexedDB.open("zipcodes"); // Открыть базу данных zipcode 
  request.onerror = logerr;                 // Выводить сообщения об ошибках
  request.onsuccess = function() {    // Или вызвать эту функцию по завершении
    var db = request.result;          // Результатом запроса является база данных

    // Базу данных можно открыть всегда, даже если она не существует.
    // Мы проверяем версию, чтобы узнать, была ли БД создана и инициализирована.
    // Если нет - это необходимо сделать. Но если БД уже настроена,
    // остается просто передать ее функции f().
    if (db.version === "1") f(db);  // Если БД существует, передать ее f()
    else initdb(db,f);              // Иначе сначала инициализировать ее
  }
}

// Принимает почтовый индекс, отыскивает город, которому он принадлежит,
// и асинхронно передает название города указанной функции,
function lookupCity(zip, callback) {
  withDB(function(db) {
    // Создать объект транзакции для этого запроса
    var transaction = db.transaction(["zipcodes"], // Требуемое хранилище 
               IDBTransaction.READ_ONLY, // He обновлять 
                     0);                 // Время ожидания не ограничено

    // Получить хранилище объектов из транзакции
    var objects = transaction.objectStore("zipcodes");

    // Запросить объект, соответствующий указанному индексу.
    // Строки выше выполнялись синхронно, но эта выполняется асинхронно
    var request = objects.get(zip);
    request.onerror = logerr;         // Выводить сообщения об ошибках
    request.onsuccess = function() {  // Передать результаты этой функции
      // Искомый объект сейчас в свойстве request.result
      var object = request.result;
      if (object) // Если соотв. найдено, передать город и штат функции 
        callback(object.city + ", " + object.state);
      else        // Иначе сообщить о неудаче
        callback("Unknown zip code");
    }
  });
}

// По указанному названию города отыскивает все почтовые индексы для всех
// городов (в любом штате) с этим названием (с учетом регистра символов).
// Асинхронно передает результаты по одному указанной функции
function lookupZipcodes(city, callback) {
  withDB(function(db) {
    // Как и выше, создаем транзакцию и получаем хранилище объектов
    var transaction = db.transaction(["zipcodes"],
                     IDBTransaction.READ_ONLY, 0);
    var store = transaction.objectStore("zipcodes");
    // На этот раз нам требуется получить индекс по названиям городов
    var index = store.index("cities");
    
    // Этот запрос может вернуть несколько результатов, поэтому, чтобы
    // получить их все, следует использовать объект курсора. Чтобы создать
    // курсор, нужно создать объект диапазона, представляющий диапазон ключей
    var range = new IDBKeyRange.only(city); // Диапазон с одним ключом 

    // Все, что выше, выполняется синхронно.
    // Теперь нужно запросить курсор, который возвращается асинхронно,
    var request = index.openCursor(range);  // Запросить курсор 
    request.onerror = logerr;               // Сообщать об ошибках
    request.onsuccess = function() {        // Передать курсор этой функции
      // Этот обработчик событий будет вызван несколько раз, по одному
      // для каждой записи, соответствующей запросу, и еще один раз
      // с пустым курсором, указывающим на окончание результатов,
      var cursor = request.result // Курсор в свойстве request.result
      if (!cursor) return;        // Нет курсора = нет результатов
      var object = cursor.value   // Получить совпавшую запись
      callback(object);           // Передать ее указанной функции
      cursor.continue();          // Запросить следующую запись
    };
  });
}

// Эта функция используется обработчиком onchange в документе ниже.
// Она выполняет запрос к БД и отображает результаты
function displayCity(zip) {
  lookupCity(zip,
	  function(s) { document.getElementById(′city′).value = s; }
	);
}

// Это другая функция, используемая обработчиком onchange в документе ниже.
// Она выполняет запрос к БД и отображает результаты
function displayZipcodes(city) {
  var output = document.getElementById("zipcodes");
  output.innerHTML = "Найденные индексы:";
  lookupZipcodes(city, function(o) {
    var div = document.createElement("div");
    var text = o.zipcode + ": " + o.city + ", " + o.state;
    div.appendChild(document.createTextNode(text));
    output.appendChild(div);
  });
}

// Настраивает структуру базы данных и заполняет ее данными,
// затем передает базу данных функции f(). Эта функция
// вызывается функцией withDB(), если база данных еще не была инициализирована.
// Это самая хитрая  часть программы, поэтому мы оставили ее напоследок.
function initdb(db, f) {
  // Загрузка информации о почтовых индексах и сохранение ее в базе данных
  // может потребовать некоторого дополнительного времени при первом запуске
  // этого приложения. Поэтому необходимо вывести сообщение, извещающее 		
  // о выполнении операции. 
  var statusline = document.createElement("div");
  statusline.style.cssText =
    "position:fixed; left:0px; top:0px; width:100%;" +
    "color:white; background-color: black; font: bold 18pt sans-serif;" +
    "padding: 10px; ";
  document.body.appendChild(statusline);
  function status(msg) { statusline.innerHTML = msg.toString(); };
  
  status("Инициализация базы данных почтовых индексов");
  
  // Единственное место, где можно определить или изменить структуру
  // базы данных IndexedDB - обработчик onsucess запроса setVersion.
  var request = db.setVersion("1"); // Попробовать изменить версию БД
  request.onerror = status;         // Вывести сообщение в случае ошибки
  request.onsuccess = function() {  // Иначе вызвать эту функцию
    // База данных почтовых индексов включает единственное хранилище.
    // Оно хранит объекты следующего вида:
    // zipcode: "02134", Отправьте на телепередачу Zoom!¹ :-)
    // city: "Allston", 
    // state: "MA", 
    // latitude: "42.355147", 
    // longitude: "-71.13164" 
    // }
    //
    // Свойство "zipcode" используется в качестве ключа базы данных
    // Кроме того, создается индекс по названию города
    
    // Создать хранилище объектов, указав имя хранилища и объект с параметрами,
    // включающий "путь к ключу", определяющий имя свойства-ключа для этого
    // хранилища. (Если опустить путь к ключу, IndexedDB определит свой
    // собственный уникальный целочисленный ключ.)
    var store = db.createObjectStore("zipcodes", // имя хранилища
                     { keyPath: "zipcode" });
    
    // Создать в хранилище объектов индекс по названию города.
    // Строка пути к ключу передается этому методу непосредственно,
    // как обязательный аргумент, а не как свойство объекта с параметрами.
    store.createIndex("cities", "city");
    
    // Теперь необходимо загрузить информацию о почтовых индексах, преобразовать
    // ее в объекты и сохранить эти объекты в созданном выше хранилище.
    //
    // Файл с исходными данными содержит строки следующего вида:
    // 
    // 02130,Jamaica Plain,MA.42.309998,-71.11171 
    // 02131,Roslindale,MA,42.284678,-71.13052 
    // 02132,West Roxbury,MA,42.279432,-71.1598 
    // 02133,Boston,MA,42.338947,-70.919635 
    // 02134,Allston,MA, 42.355147,-71.13164 
    //
    // Как ни странно, но почтовая служба США не обеспечивает свободный доступ
    // к этой информации, поэтому мы будет использовать устаревшие данные
    // переписи. Для загрузки данных используется объект XMLHttpRequest.
    // Но для обработки данных по мере поступления будут использованы
    // новые события onload и onprogress, определяемые спецификацией XHR2
    var xhr = new XMLHttpRequest();  // Объект XHR для загрузки данных
    xhr.open("GET", "zipcodes.csv"); // HTTP-запрос типа GET для этого URL
    xhr.send();                      // Запустить немедленно
    xhr.onerror = status;            // Отображать сообщения об ошибках
    var lastChar = 0, numlines = 0;  // Уже обработанный объем
    
    // Обрабатывает файл базы данных блоками, по мере загрузки
    xhr.onprogress = xhr.onload = function(e) { // Сразу два обработчика!
      // Обработать блок между lastChar и последним принятым символом
      // перевода строки. (Нам требуется отыскать последний символ
      // перевода строки, чтобы не обработать неполную запись)
      var lastNewline = xhr.responseText.lastIndexOf("\n");
      if (lastNewline > lastChar) {
        var chunk = xhr.responseText.substring(lastChar, lastNewline)
        lastChar = lastNewline + 1;  // Откуда начинать в следующий раз
        
        // Разбить новый фрагмент на строки
        var lines = chunk.split("\n");
        numlines += lines.length;
        
        // Чтобы вставить информацию о почтовом индексе в базу данных, необходимо
        // получить объект транзакции. Все операции добавления объектов
        // в базу данных, выполняемые с использованием этого объекта,
        // будут автоматически подтверждаться после выхода из этой функции,
        // когда браузер вернется в цикл обработки событий.
        // Чтобы создать объект транзакции, следует определить,
        // какие хранилища объектов будут использоваться (у нас имеется всего
        // одно хранилище). Кроме того, требуется сообщить, что будет
        // выполняться не только чтение, но и запись в базу данных:
        var transaction = db.transaction(["zipcodes"], // хранилища 
                         IDBTransaction.READ_WRITE);
        
        // Получить ссылку на хранилище из объекта транзакции
        var store = transaction.objectStore("zipcodes");
        
        // Теперь обойти в цикле строки в файле с почтовыми индексами,
        // создать на их основе объекты и добавить их в хранилище.
        for(var i = 0; i < lines.length; i++) {
          var fields = lines[i].split(","); // Значения через запятую 
          var record = {                    // Сохраняемый объект
            zipcode: fields[0],             // Все свойства - строки
            city: fields[1],
            state: fields[2],
            latitude: fields[3],
            longitude: fields[4]
          };
          // Вся прелесть IndexedDB API в том, что хранилище
          // объектов *по-настоящему* просто использовать.
          // Следующая строка добавляет запись:
          store.put(record);  // Или add(), чтобы избежать затирания
        }
        
        status("Initializing zipcode database: loaded "
            + numlines + " records.");
      }
      
      if (e.type == "load") {
        // Если это было последнее событие load, значит, мы отправили в базу
        // данных все сведения о почтовых индексах. Но, так как мы только
        // что обработали порядка 40000 записей, они все еще могут записываться
        // в хранилище. Поэтому мы выполним простой запрос. Когда он будет
        // успешно выполнен, это послужит сигналом, что база данных готова
        // к работе, и наконец можно удалить строку сообщения и вызвать
        // функцию f(), которая так давно была передана функции
        lookupCity("02134", function(s) { // Allston, MA 
          document.body.removeChild(statusline);
          withDB(f);
        });
      }
    }
  }
}

</script>
</head>
<body>
<p>Введите почтовый индекс, чтобы отыскать город:
Индекс: <input onchange="displayCity(this.value)"></input> 
City: <output id="city"></output>
</div>
<div>
<p>Введите название города (с учетом регистра символов, без названия штата), 
чтобы отыскать все города с этим названием и их почтовые индексы:</p>
City: <input onchange="displayZipcodes(this.value)"></input>
<div id="zipcodes"></div>
</div>
<p><i>Этот пример работает только в Firefox 4 и Chrome 11.</i></p>
<p><i>Выполнение первого запроса может занять длительное время.</i></p>
<p><i>Вам может потребоваться запустить Chrome 
с ключом --unlimited-quota-for-indexeddb</i></p> 
</body
</html

¹ В 1972-1978 гг. телекомпания WGBH-TV в Бостоне выпускала детское телешоу «ZOOM». Детям предлагалось после шоу «выключить телевизор и сделать то, о чем рассказывалось в передаче...». Дети, обычно семеро, участвовавшие в шоу, играли в игры, ставили пьесы, рассказывали стихи, ставили научные эксперименты, вели беседы на такие темы, как больницы, предрассудки и другие, предлагаемые телезрителями. Передача имела призыв со словами: «Напишите на конверте ZOOM, Зет-Дабл-Оу-М, Бокс 3-5-0, Бостон, Масс 0-2-1-3-4 и отправьте его на ZOOM!». Весь текст проговаривался, кроме индекса, который пропевался. Описание в Википедии: http://en.wikipedia.org/wiki/ZOOM_ (1972_TV_series). - Прим. перев.

содержание 22.9. Веб-сокеты содержание

В главе 18 демонстрируется, как клиентские сценарии на языке JavaScript могут взаимодействовать с серверами по сети. Все примеры в этой главе используют протокол HTTP, а это означает, что все они ограничены исходной природой протокола HTTP: этот протокол, не имеющий информации о состоянии, состоит из запросов клиента и ответов сервера. Протокол HTTP фактически является узкоспециализированным сетевым протоколом. Более универсальные сетевые взаимодействия через Интернет (или через локальные сети) часто реализуются с использованием долгоживущих соединений и обеспечивают двунаправленный обмен сообщениями через ТСР-сокеты. Довольно небезопасно предоставлять клиентскому сценарию на языке JavaScript доступ к низкоуровневым ТСР-сокетам, но спецификация «WebSocket API» определяет безопасную альтернативу: она позволяет клиентским сценариям создавать двунаправленные соединения с серверами, поддерживающими протокол веб-сокетов. Это существенно упрощает решение некоторых сетевых задач.

Протокол веб-сокетов

Чтобы использовать веб-сокеты в сценариях на языке JavaScript, достаточно будет освоить клиентский прикладной интерфейс веб-сокетов, описываемый здесь. Не существует эквивалентного серверного прикладного интерфейса для создания серверов с поддержкой веб-сокетов; в этом разделе будет представлен простой пример сервера, основанного на использовании интерпретатора Node (раздел 12.2) и сторонней серверной библиотеки поддержки веб-сокетов. Клиент и сервер взаимодействуют через долгоживущие ТСР-сокеты, следуя правилам, определяемым протоколом веб-сокетов. Мы не будем рассматривать здесь особенности протокола, но следует отметить, что протокол веб-сокетов спроектирован очень аккуратно, благодаря чему веб-серверы могут легко обрабатывать HTTP-соединения и соединения на основе веб-сокетов через один и тот же порт.

Веб-сокеты получили широкую поддержку среди производителей браузеров. В ранней, предварительной версии протокола веб-сокетов была обнаружена серьезная брешь в системе безопасности, поэтому на момент написания этих строк в некоторых браузерах поддержка веб-сокетов была отключена, – до стандартизации безопасной версии протокола. В Firefox 4, например, может потребоваться явно включить поддержку веб-сокетов, открыв страницу about:config и установив в значение true переменную «network.websocket.override-security-block».

Прикладной интерфейс веб-сокетов удивительно прост в использовании. Сначала необходимо создать сокет с помощью конструктора WebSocket():

var socket = new WebSocket("ws://ws.example.com:1234/resource");

Аргументом конструктора WebSocket() является URL-адрес, в котором используется протокол ws:// (или wss:// - в случае с безопасными соединениями, по аналогии с https://). URL-адрес определяет имя хоста, к которому выполняется подключение, и может также определять порт (по умолчанию веб-сокеты используют тот же порт, что и протоколы HTTP и HTTPS) и путь или ресурс. После создания сокета в нем обычно регистрируются обработчики событий:

socket.onopen = function(e) { /* Соединение установлено. */ }; 
socket.onclose = function(e) { /* Соединение закрыто. */ }; 
socket.one г го г = function(e) { /* Что-то пошло не так! */ }; 
socket.onmessage = function(e) { 
var message = e.data; /* Сервер послал сообщение. */ 
}; 

Чтобы отправить данные серверу через сокет, следует вызвать метод send() сокета:

socket.send("Привет, сервер!");

Текущая версия прикладного интерфейса веб-сокетов поддерживает только текстовые сообщения и отправляет их в виде строк в кодировке UTF-8. Однако текущая версия спецификации протокола веб-сокетов включает поддержку двоичных сообщений, и будущие версии прикладного интерфейса, возможно, будут обеспечивать обмен двоичными данными с сервером.

По окончании взаимодействия с сервером сценарий может закрыть веб-сокет вызовом его метода close().

Веб-сокеты являются двунаправленными, и единственное соединение, установленное через веб-сокет, может использоваться клиентом и сервером для передачи сообщений друг другу в любой момент времени. Это взаимодействие не обязательно должно иметь форму запросов и ответов. Каждая служба, основанная на веб-со- кетах, будет определять собственный «подпротокол» передачи данных между клиентом и сервером. С течением времени эти «подпротоколы» могут развиться, и вам может потребоваться реализовать клиенты и серверы, поддерживающие несколько версий подпротокола. К счастью, протокол веб-сокетов включает механизм, дающий возможность договориться о выборе подпротокола, который поддерживается и клиентом, и сервером. Конструктору WebSocket() можно передать массив строк. Сервер получит его в виде списка подпротоколов, поддерживаемых клиентом. Сервер выберет подпротокол, поддерживаемый им, и отправит его обратно клиенту. После установления соединения клиент сможет определить, какой подпротокол можно использовать, проверив свойство protocol объекта WebSocket. В разделе 18.3 описывается прикладной интерфейс объекта EventSource и демонстрируется его применение на примере реализации клиента и сервера чата. Веб-сокеты еще больше упрощают реализацию подобных приложений. В примере 22.16 демонстрируется очень простой клиент чата: он напоминает пример 18.15, но использует веб-сокеты для двунаправленного обмена сообщениями вместо объекта EventSource для приема сообщений и XMLHttpRequest - для отправки.

Пример 22.16. Клиент чата на основе веб-сокетов

<script>
window.onload = function() {
  // Позаботиться о некоторых деталях пользовательского интерфейса 
  var nick = prompt("Enter your nickname");     // Получить псевдоним 
  var input = document.getElementById("input"); // Отыскать поле ввода 
  input.focus();                                // Установить фокус ввода 

  // Открыть веб-сокет для отправки и приема сообщений в чате. 
  // Предполагается, что HTTP-сервер, откуда загружается сценарий, также 
  // поддерживает веб-сокеты, и для связи с ним используется то же имя хоста 
  // и номер порта, но вместо протокола http:// используется протокол ws:// 
  var socket = new WebSocket("ws://" + location.host + "/"); 

  // Так через веб-сокет принимаются сообщения с сервера 
  socket.onmessage = function(event) {       // Вызывается при получении сообщения 
    var msg = event.data;                    // Получить текст из объекта события
    var node = document.createTextNode(msg); // Создать текстовый узел 
    var div = document.createElement("div"); // Создать элемент <div> 
    div.appendChild(node);                   // Добавить текстовый узел 
    document.body.insertBefore(div, input);  // и вставить div перед полем ввода
    input.scrollIntoView();          // Гарантировать видимость элемента input 
  }

  // Так через веб-сокет отправляются сообщения на сервер 
  input.onchange = function() {     // Когда пользователь нажмет клавишу Enter
    var msg = nick + ": " + input.value; // Имя пользователя и текст 
    socket.send(msg);               // Отправить через сокет 
    input.value = "";               // Подготовиться к вводу следующего сообщения
  }
};
</script>
<!-- Пользовательский интерфейс - это единственное поле ввода -->
<!-- Новые сообщения в чате будут появляться перед этим элементом -->
<input id="input" style="width:100%"/>

В примере 22.17 демонстрируется реализация сервера чата, основанного на веб-сокетах, которая предназначена для работы под управлением интерпретатора Node (раздел 12.2). Сравните этот пример с примером 18.17, чтобы увидеть, что веб-сокеты упрощают не только клиентскую часть реализации чата, но и серверную.


Пример 22.17. Сервер чата на основе веб-сокетов, выполняющийся под управлением Node

/* Этот серверный сценарий на языке JavaScript предназначен для выполнения
 * под управлением NodeJS. Он играет роль сервера веб-сокетов, реализованного
 * поверх HTTP-сервера с использованием внешней библиотеки websocket, которую
 * можно найти по адресу: https://github.com/miksago/node-websocket-server/.
 * При обращении к ресурсу "/" он возвращает HTML-файл клиента чата. В ответ
 * на обращение к любому другому ресурсу возвращается код 404. Сообщения
 * принимаются посредством протокола веб-сокетов и просто рассылаются
 * по всем активным соединениям.
 */ 

var http = require(′http′); // Использовать HTTP-сервер в Node var ws = require(′websocket-server′); // Использовать внешнюю библиотеку // Прочитать исходный файл с реализацией клиента чата. Используется ниже, var clientui = require(′fs′).readFileSync("wschatclient.html"); // Создать HTTP-сервер var httpserver = new http.Server(); // Когда HTTP-сервер получит новый запрос, будет вызвана эта функция httpserver.on("request", function (request, response) { // Если запрошен ресурс "/", отправить реализацию клиента чата, if (request.url === "/") { // Запрошена реализация клиента чата response.writeHead(200, {"Content-Type": "text/html"}); response.write(clientui); response.end(); } else { В ответ на любой другой запрос отправить код 404 "Not Found" response.writeHead(404); response.end(); } }); Обернуть HTTP-сервер сервером на основе веб-сокетов var wsserver = ws.createServer({server: httpserver}); Вызывать эту функцию при получении новых запросов на соединение wsserver.on("connection", function(socket) { socket.send("Welcome to the chat room."); Приветствовать нового клиента socket.on("message", function(msg) { Принимать сообщения от клиента wsserver.broadcast(msg); И рассылать их всем остальным }); }); Запустить сервер на порту 8000. Запуск сервера на основе веб-сокетов приводит также к запуску HTTP-сервера. Для его использования подключайтесь по адресу http://localhost:8000/. wsserver.listen(8000);