Подключая внешние CSS и Javascript, мы хотим снизить до минимума лишние HTTP-запросы.
Для этого .js и .css файлы отдаются с заголовками, обеспечивающими надежное кеширование.
Но что делать, когда какой-то из этих файлов меняется в процессе разработки? У всех пользователей в кеше старый вариант - пока кеш не устарел, придет масса жалоб на сломанную интеграцию серверной и клиентской части.
Правильный способ кеширования и версионности полностью избавляет от этой проблемы и обеспечивает надежную, прозрачную синхронизацию версий стиля/скрипта.
Самый простой способ кеширования статических ресурсов - использование ETag.
Достаточно включить соответствующую настройку сервера (для Apache включена по умолчанию) - и к каждому файлу в заголовках будет даваться ETag - хеш, который зависит от времени обновления, размера файла и (на inode-based файловых системах) inode.
Браузер кеширует такой файл и при последующих запросах указывет заголовок If-None-Match с ETag кешированного документа. Получив такой заголовок, сервер может ответить кодом 304 - и тогда документ будет взят из кеша.
Выглядит это так:
GET /misc/pack.js HTTP/1.1 Host: javascript.ru
Вообще, браузер обычно добавляет еще пачку заголовоков типа User-Agent, Accept и т.п. Для краткости они порезаны.
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"
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:
Last-Modified (вместо ETag)If-Modified-Since(вместо If-None-Match)Эти способы работают стабильно и хорошо, но браузеру в любом случае приходится делать по запросу для каждого скрипта или стиля.
Общий подход для версионности - в двух словах:
Дальше мы разберем, как сделать этот процесс автоматическим и прозрачным.
Жесткое кеширование - своего рода кувалда которая полностью прибивает запросы к серверу для кешированных документов.
Для этого достаточно добавить заголовки 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"
Получив такие заголовки, браузер жестко закеширует документ надолго. Все дальнейшие обращения к документу будут напрямую обслуживаться из кеша браузера, без обращения к серверу.
Именно поэтому мы добавляем версию в имя файла. Конечно, с такими адресами приходится использовать решение типа 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
RewriteRule нужно поставить в основной конфигурационный файл httpd.conf или в подключаемые к нему(include) файлы, но ни в коем случае не в .htaccess, иначе команды Header будут запущены первыми, до того, как установлена переменная VERSIONED_FILE.
Директивы Header могут быть где угодно, даже в .htaccess - без разницы.
Как ставить версию в имя скрипта - зависит от Вашей шаблонной системы и, вообще, способа добавлять скрипты (стили и т.п.).
Например, при использовании даты модификации в качестве версии и шаблонизатора Smarty - ссылки можно ставить так:
<link href="{version src='/http/javascript.ru/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="/http/javascript.ru/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-ролики и т.п.
Он полезен всегда, когда документ изменяется, но в браузере всегда должна быть текущая актуальная версия.