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

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

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

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

Поддержка офлайна
iPhone
10.0+3.5+4.0+5.0+11.5+3.2+2.1+

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

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

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

Дальнейшее чтение

Стандарты:

Документация от разработчиков браузеров:

Учебники и демонстрации:

Инструменты для работы с манифестом кэша:

Автор и редакторы

Автор: Марк Пилгрим
Последнее изменение: 11.08.2018
Редакторы: Влад Мержевич