Подключая внешние CSS и Javascript, мы хотим снизить до минимума лишние HTTP-запросы.
Для этого .js и .css файлы отдаются с заголовками, обеспечивающими надежное кеширование.
Но что делать, когда какой-то из этих файлов меняется в процессе разработки? У всех пользователей в кеше старый вариант — пока кеш не устарел, придет масса жалоб на сломанную интеграцию серверной и клиентской части.
Правильный способ кеширования и версионности полностью избавляет от этой проблемы и обеспечивает надежную, прозрачную синхронизацию версий стиля/скрип.
Простое кеширование ETag
Самый простой способ кеширования статических ресурсов — использование ETag.
Достаточно включить соответствующую настройку сервера (для Apache включена по умолчанию) — и к каждому файлу в заголовках будет даваться ETag — хеш, который зависит от времени обновления, размера файла и (на inode-based файловых системах) inode.
Браузер кеширует такой файл и при последующих запросах указывет заголовок If-None-Match с ETag кешированного документа. Получив такой заголовок, сервер может ответить кодом 304 — и тогда документ будет взят из кеша.
Выглядит это так:
Первый запрос к серверу (кеш чистый)
GET /misc/pack.js HTTP/1.1 Host: javascript.ru
Вообще, браузер обычно добавляет еще пачку заголовоков типа User-Agent, Accept и т.п. Для краткости они порезаны.
Ответ сервераСервер посылает в ответ документ c кодом 200 и ETag:
HTTP/1.x 200 OK Content-Encoding: gzip Content-Type: text/javascript; charset=utf-8 Etag: "3272221997" Accept-Ranges: bytes Content-Length: 23321 Date: Fri, 02 May 2008 17:22:46 GMT Server: lighttpd
Следующий запрос браузера
При следующем запросе браузер добавляет If-None-Match: (кешированный ETag):
GET /misc/pack.js HTTP/1.1 Host: javascript.ru If-None-Match: "453700005"
Ответ сервера
Сервер смотрит — ага, документ не изменился. Значит можно выдать код 304 и не посылать документ заново.
HTTP/1.x 304 Not Modified Content-Encoding: gzip Etag: "453700005" Content-Type: text/javascript; charset=utf-8 Accept-Ranges: bytes Date: Tue, 15 Apr 2008 10:17:11 GMT
Альтернативный вариант — если документ изменился, тогда сервер просто посылает 200 с новым ETag.
Аналогичным образом работает связка Last-Modified + If-Modified-Since:
1. сервер посылает дату последней модификации в заголовке Last-Modified (вместо ETag)
2. браузер посылает дату закешированной версии в заголовке If-Modified-Since(вместо If-None-Match)
3. Если скрипт не изменился — ответ содержит только код 304
Эти способы работают стабильно и хорошо, но браузеру в любом случае приходится делать по запросу для каждого скрипта или стиля.
Умное кеширование. Версионность
Общий подход для версионности — в двух словах:
1. Во все скрипты добавляется версия (или дата модификации). Например, http://javascript.ru/my.js превратится в http://javascript.ru/my.v1.2.js
2. Все скрипты жестко кешируются браузером
3. При обновлении скрипта версия меняется на новую: http://javascript.ru/my.v2.0.js
4. Адрес изменился, поэтому браузер запросит и закеширует файл заново
5. Старая версия 1.2 постепенно выпадет из кеша
Дальше мы разберем, как сделать этот процесс автоматическим и прозрачным.
Жесткое кеширование
Жесткое кеширование — своего рода кувалда которая полностью прибивает запросы к серверу для кешированных документов.
Для этого достаточно добавить заголовки Expires и Cache-Control: max-age.
Например, чтобы закешировать на 365 дней в PHP:
header("Expires: ".gmdate("D, d M Y H:i:s", time()+86400*365)." GMT"); header("Cache-Control: max-age="+86400*365);
Или можно закешировать контент надолго, используя mod_header в Apache:
Header add "Expires" "Mon, 28 Jul 2014 23:30:00 GMT" Header add "Cache-Control" "max-age=315360000"
Получив такие заголовки, браузер жестко закеширует документ надолго. Все дальнейшие обращения к документу будут напрямую обслуживаться из кеша браузера, без обращения к серверу.
Большинство браузеров (Opera, Internet Explorer 6+, Safari) НЕ кешируют документы, если в адресе есть вопросительный знак, т.к считают их динамическими. Именно поэтому мы добавляем версию в имя файла. Конечно, с такими адресами приходится использовать решение типа mod_rewrite, мы это рассмотрим дальше в статье.P.S А вот Firefox кеширует адреса с вопросительными знаками.
Автоматическое преобразование имен
Разберем, как автоматически и прозрачно менять версии, не переименовывая при этом сам файлы.
Имя с версией -> Файл
Самое простое — это превратить имя с версией в оригинальное имя файла.
На уровне Apache это можно сделать mod_rewrite:
RewriteEngine on RewriteRule ^/(.*\.)v[0-9.]+\.(css|js|gif|png|jpg)$ /$1$2 [L]
Такое правило обрабатывает все css/js/gif/png/jpg-файлы, вырезая из имени версию.
Например:
/images/logo.v2.gif -> /images/logo.gif
/css/style.v1.27.css -> /css/style.css
/javascript/script.v6.js -> /javascript/script.js
Но кроме вырезания версии — надо еще добавлять заголовки жесткого кеширования к файлам. Для этого используются директивы mod_header:
Header add "Expires" "Mon, 28 Jul 2014 23:30:00 GMT" Header add "Cache-Control" "max-age=315360000"
А все вместе реализует вот такой апачевый конфиг:
RewriteEngine on # убирает версию, и заодно ставит переменную что файл версионный RewriteRule ^/(.*\.)v[0-9.]+\.(css|js|gif|png|jpg)$ /$1$2 [L,E=VERSIONED_FILE:1] # жестко кешируем версионные файлы Header add "Expires" "Mon, 28 Jul 2014 23:30:00 GMT" env=VERSIONED_FILE Header add "Cache-Control" "max-age=315360000" env=VERSIONED_FILE
Из-за порядка работы модуля mod_rewrite, RewriteRule нужно поставить в основной конфигурационный файл httpd.conf или в подключаемые к нему(include) файлы, но ни в коем случае не в .htaccess, иначе команды Header будут запущены первыми, до того, как установлена переменная VERSIONED_FILE. br>Директивы Header могут быть где угодно, даже в .htaccess — без разницы.
Автоматическое добавление версии в имя файла на HTML-странице
Как ставить версию в имя скрипта — зависит от Вашей шаблонной системы и, вообще, способа добавлять скрипты (стили и т.п.).
Например, при использовании даты модификации в качестве версии и шаблонизатора Smarty — ссылки можно ставить так:
<link href="{version src='/css/group.css'}" rel="stylesheet" type="text/css" />
Функция version добавляет версию:
function smarty_version($args){ $stat = stat($GLOBALS['config']['site_root'].$args['src']); $version = $stat['mtime']; echo preg_replace('!\.([a-z]+?)$!', ".v$version.\$1", $args['src']); }
Результат на странице:
<link href="/css/group.v1234567890.css" rel="stylesheet" type="text/css" />
Отпимизация
Чтобы избежать лишних вызовов stat, можно хранить массив со списком текущих версий в отдельной переменной
$versions['css'] = array( 'group.css' => '1.1', 'other.css' => '3.0', }
В этом случае в HTML просто подставляется текущая версия из массива.
Можно скрестить оба подхода, и выдавать во время разработки версию по дате модификации — для актуальности, а в продакшн — версию из массива, для производительности.
Применимость
Такой способ кеширования работает везде, включая Javascript, CSS, изображения, flash-ролики и т.п.
Он полезен всегда, когда документ изменяется, но в браузере всегда должна быть текущая актуальная версия.