Возьмём с собой

Марк Пилгрим

Оригинал: http://diveintohtml5.info/offline.html

Перевод: Влад Мержевич

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

В простейшем случае оффлайновое веб-приложение представляет собой список адресов — HTML, CSS, JavaScript, изображения или любые другие ресурсы. Главная страница оффлайнового приложения получает этот список, вызывая манифест — текстовый файл, хранящийся на веб-сервере. Браузер, работающий с приложением, читает список адресов из файла манифеста, скачивает ресурсы, кэширует их локально и автоматически сохраняет локальные копии до момента их изменения. Когда в следующий раз вы попытаетесь получить доступ к веб-приложению без подключения к сети, браузер автоматически переключится на локальную копию.

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

Поддержка оффлайна
IE Firefox Safari Chrome Opera iPhone Android

Манифест кэша

Оффлайновое веб-приложение вращается вокруг файла манифеста кэша. Что это за файл манифеста? Это список всех ресурсов требуемых вашему веб-приложению, пока оно отключено от сети. Для загрузки и кэширования этих ресурсов вы должны указать файл манифеста с помощью атрибута manifest у элемента <html>.

<!DOCTYPE HTML>
<html manifest="/cache.manifest">
<body>
...
</body>
</html>

Ваш файл манифеста кэша может располагаться в любом месте веб-сервера, но вы должны обеспечить для него тип text/cache-manifest. Если вы запускаете веб-сервер под Apache, то можете добавить директиву AddType в файл .htaccess в корне сайта. 

AddType text/cache-manifest .manifest

Убедитесь, что имя вашего файла манифеста заканчивается на .manifest. Если вы используете другой веб-сервер или конфигурацию Apache, посмотрите документацию по управлению заголовком Content-Type.

Спроси профессора Маркапа

В. Мое веб-приложение содержит более одной страницы. Нужно ли указывать атрибут manifest на каждой странице или добавить его только на главную?

О. На каждой странице вашего приложения нужен атрибут manifest, который указывает на манифест кэша всего веб-приложения.

Итак, каждая из ваших HTML-страниц указывает на файл манифеста кэша, сам файл передается с правильным заголовком Content-Type. Но что происходит в файле манифеста? Здесь все намного интереснее.

Первая строка каждого файла манифеста заключается в следующем.

CACHE MANIFEST

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

Вот корректный файл манифеста. В нем перечислены три ресурса: CSS-файл, файл JavaScript и изображение в формате JPEG.

CACHE MANIFEST
/clock.css
/clock.js
/clock-face.jpg

Этот файл манифеста кэша не имеет заголовков, так что все перечисленные ресурсы по умолчанию находятся в разделе «явный». Ресурсы в этом разделе скачиваются и кэшируются локально и будут использоваться вместо онлайновых копий при отключении от сети. Таким образом, при загрузке этого файла манифеста, ваш браузер скачает clock.css, clock.js и clock-face.jpg в корневой директории веб-сервера. Теперь вы можете отсоединить сетевой кабель и обновить страницу, все эти ресурсы будут доступны в оффлайне.

Спроси профессора Маркапа

В. Нужно ли мне перечислять мои HTML-страницы в манифесте кэша?

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

Раздел NETWORK

Вот более сложный пример. Предположим, вы хотите, чтобы ваше приложение отслеживало посетителей, используя скрипт tracking.cgi, который динамически загружается из <img src>. Кэширование этого ресурса провалит отслеживание, поэтому его нельзя кэшировать и оно никогда не должно быть доступно в автономном режиме. Вот что надо сделать.

CACHE MANIFEST
NETWORK:
/tracking.cgi
CACHE:
/clock.css
/clock.js
/clock-face.jp

Этот файл манифеста включает разделы заголовков. Строка, отмеченная как NETWORK: это начало раздела «онлайновый белый список». Ресурсы в этом разделе никогда не кэшируются и не доступны в автономном режиме (попытка загрузить их в оффлайновом режиме приведет к ошибке). Строка, отмеченная как CACHE: это начало раздела «явный». Остальное в этом файла такое же, как в предыдущем примере. Каждый из трех перечисленных ресурсов будет храниться в кэше, и доступен в режиме оффлайн.

Раздел FALLBACK

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

CACHE MANIFEST
FALLBACK:
/ /offline.html
NETWORK:
*

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

Вот что этот манифест кэша делает. Пусть каждая HTML-страница (запись, страница обсуждения, страница правок, страница истории) в Википедии указывают на этот файл манифеста. При посещении любой страницы, которая указывает на манифест кэша, ваш браузер говорит: «Эй, эта страница является частью оффлайнового приложения, и что я о ней знаю?». Если ваш браузер еще ни разу не скачивал этот файл манифеста, будет создан новый оффлайновый  кэш приложения (appcache), скачаны все ресурсы, перечисленные в манифесте кэша, а затем текущая страница добавлена в appcache. Если ваш браузер знает об этом манифесте кэша, он просто добавит текущую страницу в существующий кэш приложения. Так или иначе, страница, которую вы только что посетили, оказывается в кэше приложения. Это важно и означает, что вы можете иметь оффлайновое веб-приложение, которое «лениво» добавляет страницы при их посещении. Вам не нужно перечислять каждую из ваших HTML-страниц в манифесте кэша.

Теперь посмотрим на резервный раздел FALLBACK: , в этом манифесте он появляется только в одной строке. Первая часть строки (до пробела) не URL, а шаблон URL. Первый символ (/) соответствует любой странице вашего сайта, не только главной. При попытке посетить страницу, пока вы находитесь в оффлайновом режиме, браузер будет искать ее в кэше приложения. Если ваш браузер находит в нем страницы (потому что вы ее посещали, пока были подключены к сети и страница в этот момент неявно добавляется в appcache), то ваш браузер будет отображать кэшированную копию страницы. Если Ваш браузер не обнаружил страницу в кэше приложения, вместо отображения сообщения об ошибке, появится страница /offline.html, указанная во второй половине строки резервного раздела.

Наконец, давайте рассмотрим сетевой раздел NETWORK:. Этот раздел в манифесте занимает одну строку, которая содержит один символ (*). Этот символ имеет особое значение в сетевом разделе, он называется «подстановочный флаг онлайнового белого списка». Это причудливый способ сказать: все, что не в appcache, должно быть загружено по исходному веб-адресу при подключении к Интернету. Это важно для «открытых» оффлайновых веб-приложений и означает, что пока вы просматриваете эту гипотетическую автономную Википедию по сети, ваш браузер будет получать изображения, видео и другие встроенные ресурсы как обычно, даже если они находятся на другом домене. Это характерно для больших сайтов, даже если они не являются частью оффлайнового веб-приложений. HTML-страницы генерируются локально, в то время как изображения и видео загружаются с другого домена.

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

Является ли это пример завершенным? Нет. Википедия это больше, чем HTML-файлы. В ней используются общие CSS, JavaScript и картинки на каждой странице. Все эти ресурсы должны быть указаны в явном виде в разделе CACHE: файла манифеста, чтобы страницы в оффлайне правильно отображались и обрабатывались. Но указание резервного раздела делает так, что вы имеете открытое оффлайновое приложение, которое выходит за рамки тех ресурсов, что мы перечислили в явном виде в файле манифеста.

Поток событий

Итак, я рассказал об оффлайновых веб-приложениях, манифесте кэша и автономном кэше приложения (appcache) в расплывчатых, полумагических терминах. Всякие штуки загружаются, браузеры принимают решения и все просто работает. Вы знаете что-то лучше этого? Я имею в виду, что мы говорим про веб-разработку. Ничего так просто не работает.

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

  1. Как только ваш браузер замечает атрибут manifest в элементе <html>, он вызывает событие checking (все события, перечисленные здесь, срабатывают на объекте window.applicationCache). Событие checking всегда срабатывает, независимо от того, посещали вы эту страницу ранее или любую другую указанную в манифесте кэша.
  2. Если ваш браузер еще не видел манифест кэша...
    • Сработает событие downloading, затем браузер начинает загружать все ресурсы, указанные в манифесте кэша.
    • Пока идет загрузка, периодически срабатывает событие progress, которое содержит информацию, сколько файлов было скачано и сколько еще в очереди загрузки.
    • После того, как все перечисленные в манифесте ресурсы были успешно загружены, в браузере срабатывает финальное событие cached. Это сигнал для вас, что оффлайновое приложение полностью кэшировано и готово к автономному использованию.
  3. С другой стороны, если вы уже посещали эту страницу или любую другую страницу, указанную в манифесте кэша, то ваш браузер уже знает об этом манифесте. Так что некоторые ресурсы возможно уже имеются в appcache или даже целиком рабочее приложение. Так что теперь вопрос в том, изменился ли кэш с момента последнего посещения браузером и как это проверить?
    • Если ответ отрицательный, то манифест кэша не изменился, браузер немедленно вызовет событие noupdate. Вот и все.
    • Если ответ «да», манифест кэша изменился, браузер вызовет событие downloading и начнет повторно загружать каждый ресурс, упомянутый в манифесте.
    • Пока идет загрузка, браузер периодически вызывает событие progress, которое содержит информацию, сколько файлов было скачано и сколько еще в очереди загрузки.
    • После того, как все ресурсы, перечисленные в манифесте кэша, были успешно загружены вновь, в браузере срабатывает заключительное событие updateready. Это сигнал, что новая версия оффлайнового веб-приложения полностью кэширована и готова к автономному использованию. Однако, новая версия еще не используется. Для «горячей замены» на новую версию без вынуждения пользователя перезагрузить страницу, вы можете вручную вызвать функцию window.applicationCache.swapCache().

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

  • манифест кэша возвращает HTTP-ошибку 404 (страница не найдена) или 410 (страница перенесена постоянно).
  • манифест кэша был найден и не изменился, но HTML-страницы указанные в манифесте не удалось загрузить правильно.
  • манифест кэша изменился, пока было запущено обновление.
  • манифест кэша был найден и не изменился, но браузеру не удалось загрузить один из ресурсов, перечисленных в нем.

Искусство отладки или «Убей меня! Убей меня сейчас!»

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

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

  1. Посредством HTTP-семантики ваш браузер проверяет, что срок действия манифеста кэша истек. Так же, как и любой другой файл, работающий через HTTP, веб-сервер, как правило, включает мета-информацию о файле в HTTP-заголовок ответа. Некоторые из этих HTTP-заголовков (Expires и Cache-Control) говорят браузеру, как ему кэшировать файлы без всякого запроса сервера. Этот вид кэширования не имеет ничего общего с оффлайновым веб-приложением и происходит практически со всеми HTML-страницами, таблицами стилей, скриптами, изображениями или другими ресурсами в Интернете.
  2. Если срок манифеста кэша истек (в соответствии с его HTTP-заголовком), то ваш браузер запрашивает сервер, есть ли новая версия, и если да, то браузер скачает ее. Для этого ваш браузер отправит HTTP-запрос, который включает дату последнего изменения манифеста кэша. Если веб-сервер определяет, что файл манифеста не поменялся с того времени, он просто вернет статус 304 (не изменен). Опять же это не относится к оффлайновым веб-приложениям. Это происходит по существу со всеми ресурсами в Интернете.
  3. Если веб-сервер считает, что файл манифеста изменился с того времени, он вернет код состояния 200 (все в порядке), отправит содержимое нового файла с новым заголовком Cache-Control и датой последнего изменения, так что шаги 1 и 2 в следующий раз будут работать правильно. HTTP крут; веб-серверы всегда планируют на будущее и если вашему веб-серверу необходимо отправить вам файл, он сделает все возможное, чтобы не отправлять его дважды без всякой причины. Как только скачан новый файл манифеста кэша, ваш браузер будет сравнивать содержимое с копией скачанной ранее. Если содержимое файла манифеста кэша такое же, как оно было в последний раз, браузер не станет повторно загружать любой из ресурсов, перечисленных в манифесте.

Любой из этих шагов может сбить вас с толку, пока вы разрабатываете и тестируете ваше оффлайновое веб-приложение. Например, у вас используется одна версия файла манифеста кэша, через 10 минут вы понимаете, что нужно добавить еще один ресурс. Не проблема, верно? Просто добавьте еще одну строку. Бдыщь. Вот что случится: вы обновите страницу, браузер обнаружит атрибут manifest, сработает событие checking, а затем... ничего. Ваш браузер упорно настаивает на том, что файл манифеста не изменился. Почему? Потому что ваш веб-сервер, скорее всего, по умолчанию настроен сообщать браузеру кэшировать статичные файлы в течение нескольких часов (используюя HTTP-заголовок Cache-Control). Это означает, что браузер никогда не пройдет первый шаг этого трехэтапного процесса. Конечно, веб-сервер знает, что файл был изменен, но ваш браузер даже не догадается спросить об этом сервер. Почему? Потому что в последний раз ваш браузер скачал манифест кэша, веб-сервер сказал ему кэшировать ресурсы несколько часов (используюя HTTP-заголовок Cache-Control). И теперь, спустя 10 минут, браузер так и делает.

Для ясности, это не ошибка, это особенность. Все работает именно так, как и предполагалось. Если у веб-сервера нет способа сказать браузерам (и промежуточным прокси) про кэширование, веб рухнет в одночасье. Но это не успокоит после того, как вы потратите несколько часов, пытаясь выяснить, почему ваш браузер не заметил обновленный кэш манифест. Еще лучше, если вы ждали достаточно долго и все таинственным образом заработает снова! Потому что срок кэша истек! Так, как и должно быть! Убей меня! Убей меня сейчас!

Так вот одну вещь вы должны сделать точно: перенастроить ваш веб-сервер так, чтобы файл манифеста не кэшировался по HTTP. Если вы используете Apache в качестве веб-сервера, эти две строки в вашем файле .htaccess сделают свое дело.

ExpiresActive On
ExpiresDefault "access"

Фактически это отключит кэширование для всех файлов в этом каталоге и всех подкаталогах. Вероятно это не то, что вы хотели бы в действительности, так что вы должны либо связать строки с директивой <Files>, чтобы влиять только на файл манифеста, либо создать подкаталог, который содержит только .htaccess и файл манифеста. Как обычно, сведения о конфигурации зависят от веб-сервера, поэтому обратитесь к его документации о том, как управлять HTTP-заголовками кэширования.

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

CACHE MANIFEST
# rev 42
clock.js
clock.css

Если вы измените clock.css, то не увидите изменений, потому что файл манифеста не поменялся. Каждый раз, когда вы вносите модификации в один из ресурсов вашего оффлайнового веб-приложения, вам необходимо менять и сам файл манифеста. Это может быть всего лишь замена одного символа. Я обнаружил простой метод, это включить комментарий с номером ревизии. Меняем номер ревизии в комментарии и веб-сервер возвращает новый измененный файл манифеста. Браузер замечает, что содержимое файла изменилось и запустит процесс повторной закачки всех ресурсов перечисленных в манифесте.

CACHE MANIFEST
# rev 43
clock.js
clock.css

Давайте построим одно!

Помните игру Уголки, которая была представлена в главе про холст, а затем улучшена за счет сохранения состояния в локальном хранилище? Давайте перенесем Уголки в оффлайн.

Чтобы сделать это, нам нужно составить список всех ресурсов этой игры. Итак, есть главная HTML-страница, один файл JavaScript, содержащий весь код игры, и... все. Картинок нет, потому что все рисуется программно через API Canvas. Все необходимые стили находятся внутри <style> в верхней части страницы. Вот наш манифест кэша.

CACHE MANIFEST
halma.html
../halma-localstorage.js

Несколько слов о путях. Внутри каталога examples я создал подкаталог offline и файл манифеста живет в нем. Из-за того, что HTML-страницам необходимо одно небольшое дополнение для работы в оффлайне (об этом чуть позже) я создал отдельную копию HTML-файла, который также живет внутри offline. Поскольку нет никаких изменений в коде JavaScript, я повторно использую тот же файл .js, который хранится в родительском каталоге (examples). Все файлы выглядят следующим образом.

/examples/localstorage-halma.html
/examples/halma-localstorage.js
/examples/offline/halma.manifest
/examples/offline/halma.html

В файле манифеста (/examples/offline/halma.manifest) мы хотим указать два файла. Во-первых, оффлайновую версию HTML-файла (/examples/offline/halma.html). Поскольку эти файлы хранятся в одном каталоге, они указаны в файле манифеста без каких-либо префиксов пути. Во-вторых, файл JavaScript, который живет в родительском каталоге (/examples/halma-localstorage.js). Перечисление в файле манифеста включает относительный путь: ../halma-localstorage.js. Это похоже на использование относительных путей в атрибуте src тега <img>. Как вы увидите в следующем примере, вы также можете использовать абсолютные пути (которые начинаются с корня текущего домена) или даже абсолютный адреса (которые указывают на ресурсы других доменов).

Теперь в HTML-файл мы должны добавить атрибут manifest, который указывает на файл манифеста кэша.

<!DOCTYPE html>
<html manifest="halma.manifest">

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

Не выкладывайте свой код напрямую в комментариях, он отображается некорректно. Воспользуйтесь сервисом cssdeck.com или jsfiddle.net, сохраните код и в комментариях дайте на него ссылку. Так и результат сразу увидят.