# ‘Expires’ in the past
header(«Expires: Mon, 26 Jul 1997 05:00:00 GMT»);
# Always modified
header(«Last-Modified: „.gmdate(„D, d M Y H:i:s“).“ GMT»);
# HTTP/1.1
header(«Cache-Controclass="underline" no-store, no-cache, must-revalidate»);
header(«Cache-Controclass="underline" post-check=0, pre-check=0», false);
# HTTP/1.0
header(«Pragma: no-cache»);
Это годится для контента, который мы не хотим кэшировать, но если контент не меняется при каждом запросе, нам нужно добиться от браузера обратного поведения. Для этого в заголовке запроса используется конструкция If-Modified-Since. Получив такой запрос, Apache (или любой другой веб-сервер) может выдать код 304 (Not Modified), тем самым сообщая браузеру, что у того в кэше уже находится актуальная версия документа. Благодаря этому механизму, нам не приходится пересылать файл заново, однако лишний запрос обрабатывать все же пришлось. Гм.
Использование entity tags похоже на работу с конструкцией if-modified-since. Apache на запрос к статическому ресурсу может отдавать заголовок Etag, содержащий контрольную сумму, сгенерированную из размера файла, времени последнего изменения и номера индексного дескриптора. Браузер может запросить заголовок файла, чтобы проверить e-tag документа перед загрузкой. Очевидно, что использование e-tag сопряжено с теми же накладными расходами, что и механизм if-modified-since, — клиент все еще вынужден делать лишний HTTP-запрос, чтобы определить валидность локальной копии.
Кроме того, нужно соблюдать осторожность с if-modified-since и e-tags, если выдача контента идет с нескольких серверов. В системе из двух хорошо сбалансированных серверов любой документ может быть запрошен одним и тем же агентом с любого из двух серверов — или с каждого (не одновременно). Это нормально. Для этого мы и выравнивали нагрузку. Однако если серверы генерируют разные e-tags или разные даты изменения документов, браузер не сможет нормально поддерживать актуальный кэш. По умолчанию e-tag генерируются с использованием индексных дескрипторов, которые на разных серверах разные. Это можно запретить с помощью следующей опции в настройках Apache:
FileETag MTime Size
Теперь Apache для генерации e-tag будет использовать только время последнего изменения и размер файла. Это, к сожалению, приводит нас к другой проблеме использования e-tag, которая тоже актуальна для if-modified-since (хоть и в меньшей степени). Поскольку e-tag зависит от времени последнего изменения, нам необходимо следить за синхронизацией. Если мы распределяем файлы по разным веб-серверам, всегда остается шанс, что на один из серверов файл попадет на секунду или две позже, чем на другой. В этом случае e-tag, сгенерированные серверами, будут отличаться. Мы можем изменить настройки так, чтобы генерировать e-tag только на основании размера файла, но это означает, что файл не обновится в браузере, если мы изменим его содержимое, а размер останется неизменным. Тоже не идеально.
Дело в том, что мы подходим к проблеме не с той стороны. Все возможные стратегии кэширования отталкиваются от того, что клиент спрашивает сервер, насколько актуальна копия, хранимая в кэше. Если бы сервер сам, без запроса, сообщал клиенту об изменениях файлов, то клиент в любой момент времени знал бы, что кэшированная копия валидна. Но веб устроен иначе — клиент запрашивает сервер, и никак иначе.
Или все же слегка иначе? Ведь перед отправкой любых JavaScript— или CSS-файлов клиент запрашивает страницу, которая на них ссылается с помощью тегов <script> и <link>. И мы можем использовать реакцию сервера для информирования клиентов о любых изменениях, произошедших с этими ресурсами. Звучит немного загадочно, поэтому скажу прямо: если мы будем изменять названия JavaScript— и CSS-файлов при каждом изменении их содержания, то сможем разрешить клиенту хранить их в кэше вечно (ведь содержимое файла по отдельно взятому адресу не меняется).
Если мы уверены в том, что конкретный ресурс никогда не изменится, то можем отправить несколько по-настоящему агрессивных заголовков. В PHP нам потребуется всего пара строк:
header(«Expires: „.gmdate(„D, d M Y H:i:s“, time()+315360000).“ GMT»);
header(«Cache-Controclass="underline" max-age=315360000»);
Так мы говорим браузеру, что содержимое файла останется неизменным в течение десяти лет (т.е. плюс-минус 315 360 000 секунд), поэтому, единожды загрузив файл, браузер может следующие десять лет использовать локальную копию. Конечно, необязательно использовать для отправки JavaScript и CSS именно PHP. Мы перейдем к этому через несколько минут.
Ручное изменение названий файлов при изменении их содержимого — опасное занятие. Что произойдет, если вы переименуете файл, но забудете переименовать шаблоны, указывающие на него? Что произойдет, если вы поменяете не все шаблоны, а только часть? Что случится, если вы измените шаблоны, но сам файл не переименуете? И наиболее вероятный вариант: что произойдет, если вы измените содержимое, но забудете переименовать файл или изменить ссылки на него? В лучшем случае, пользователи будут работать на старой версии сайта. В худшем — сайт перестанет работать совсем. В общем, ручное изменение названий — дурацкая идея.
На наше счастье, компьютеры с такими задачами — тупым повторением одних и тех же операций при совпадении определенных условий — справляются отменно.
Чтобы сделать этот процесс максимально безболезненным, следует для начала уяснить, что нам вообще не нужно переименовывать файлы. Между реальным расположением файла на диске и URL, с которого он доставляется пользователю, нет ничего общего. Так что в случае Apache мы можем использовать mod_rewrite, создав простое правило для редиректа определенных URL к нужным файлам:
RewriteEngine on
RewriteRule ^/(.*.)v[0-9.]+.(css|js|gif|png|jpg)$ /$1$2 [L]
Это правило обрабатывает все URL с указанными расширениями, которые также содержат суффикс версии. В процессе обработки правило переписывает URL так, чтобы он указывал на путь к нужному файлу (исключая при этом суффикс). Например:
foo.v2.gif foo.gif
/css/main.v1.27.css css/main.css
/javascript/md5.v6.js /javascript/md5.js
Когда это правило работает, мы можем менять URL (добавляя к нему суффикс версии), не меняя расположения файла на диске. Обнаружив, что URL изменился, браузер считает, что ему нужно обратиться к новому ресурсу.
Вы можете поинтересоваться, почему мы просто-напросто не использовали динамические ссылки (например, /css/main.css?v=4)? Дело в том, что, согласно спецификации HTTP, пользовательские агенты вообще не должны кэшировать такие URL. И хотя IE и Firefox это игнорируют, Opera и Safari точно следуют букве — поэтому, чтобы гарантировать корректную работу всех браузеров при кэшировании наших ресурсов, мы избегаем использовать динамические ссылки.
Теперь, когда мы научились менять URL без перемещения файла, было бы неплохо автоматизировать обновление URL. В небольшой рабочей среде (или в среде разработки, для тех, кому приходится иметь дело с большими рабочими средами) это довольно легко осуществить с помощью функций шаблона. Следующий пример сделан на основе Smarty, но может быть реализован и на основе других подобных движков:
<link class="underline" href="{version src=’/css/group.css’}" rel="stylesheet" type="text/css" />
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 class="underline" href="/css/group.v1234567890.css" rel="stylesheet" type="text/css" />
Для каждого залинкованного ресурса мы определяем местоположение файла на диске, проверяем его mtime (дату последнего изменения) и вставляем эту информацию в URL в виде номера версии. Это прекрасно работает на сайтах с низким трафиком (где метод stat, возвращающий информацию о файлах и каталогах, обходится довольно дешево) и в среде разработчика, однако плохо масштабируется на большие сайты, поскольку каждый вызов stat означает обращение к диску на чтение.
Решить эту проблему нетрудно. В больших системах у нас изначально есть номер версии для каждого ресурса в виде номера, присвоенного системой контроля версий (вы же используете систему контроля версий, правда?). На этапе финального построения нашего сайта требуется лишь проверить номера версий всех нужных файлов и записать их в статический конфигурационный файл.