На тему автоматической «склейки» стилей и скриптов написано уже довольно много статей, но нигде не было описано полное решение, учитывающее подводные камни, связанные с браузерами, и различными способами использования указанных файлов. Ниже я хочу рассмотреть то практическое решение, которое реализовано в Web Optimizer и обкатано уже на нескольких сотнях сайтов.
Несмотря на более простой и поистине академический синтаксис CSS-файлы довольно сложно объединять в силу разных причин. Тут и различные атрибуты media
(указывающие на устройства, для которых предназначен данный файл), и возможность сделать «вложенную» загрузку стилей при помощи @import
и т. д. Для начала рассмотрим процесс получения ссылок и содержимого самих файлов из исходной структуры веб-страницы.
Если в CMS у нас предусмотрена возможность вставки CSS-файла как отдельного объекта в head
-страницы, то это ограждает от множества проблем по «вычленению» этих объектов из готового HTML-кода. В противном случае нам придется использовать примерно следующий вариант:
/* регулярное выражение для нахождения всех <link rel="stylesheet"> и <style type="text/css"> внутри head-секции */ $regex = "!(<link[^>]+rel\\s*=\\s*(\"stylesheet\"|'stylesheet'|stylesheet)([^>]*)>|<style\\s+type\\s*=\\s*(\"text/css\"|'text/css'|text/css)([^>]*)>(.*?)</style>)!is"; preg_match_all($regex, $this->head, $matches, PREG_SET_ORDER); if (!empty($matches)) { foreach($matches as $match) { $file = array(); $file['tag'] = 'link'; $file['source'] = $match[0]; /* вырезаем из найденного куска HTML-кода обрамляющие теги, чтобы идентифицировать внутренние стилевые правила */ $file['content'] = preg_replace("/(<link[^>]+>|<style[^>]*>[\t\s\r\n]*|[\t\s\r\n]*<\/style>)/i", "", $match[0]); /* определяем все дополнительные атрибуты */ preg_match_all("@(type|rel|media|href)\s*=\s*(?:\"([^\"]+)\"|'([^']+)'|([\s]+))@i", $match[0], $variants, PREG_SET_ORDER); if(is_array($variants)) { foreach($variants AS $variant_type) { $variant_type[1] = strtolower($variant_type[1]); $variant_type[2] = !isset($variant_type[2]) ? (!isset($variant_type[3]) ? $variant_type[4] : $variant_type[3]) : $variant_type[2]; switch ($variant_type[1]) { /* выставляем источник для файла стилей */ case "href": $file['file'] = trim($this->strip_querystring($variant_type[2])); $file['file_raw'] = $variant_type[2]; break; default: /* пропускаем media="all|screen" для предотвращения некорректного поведения Safari при @media all{} или @media screen{} */ if ($variant_type[1] != 'media' || ($variant_type[1] == 'media' && !preg_match("/all|screen/i", $variant_type[2]))) { $file[$variant_type[1]] = $variant_type[2]; } break; } } } $this->initial_files[] = $file; } }
Подавая на вход данного алгоритма код head-секции нашего документа ($this->head
), на выходе мы получаем готовый массив $this->initial_files
. Стоит сразу отметить, что в массиве для файлов стилей атрибуте media
не выставляется, если он равен all
(в этом случае он просто бесполезен) либо screen
(по умолчанию у нас все стилевые правила применяются для отображения сайтов на мониторах, поэтому данное значение также можно безболезненно опустить).
Получить ссылки на используемые файлы мало. Нам необходимо полное содержимое этих файлов. Нужно иметь в виду, что нам нужно распознать все внутренние конструкции @import
(подключающие дополнительные файлы стилей) в порядке их появления в исходных файлах. Проще всего с данной проблемой может разобраться рекурсивная функция resolve_css_imports
:
function resolve_css_imports($src) { $content = file_get_contents($src); /* удаляем из первоначального содержимого@import
внутри комментариев */ $content = preg_replace("!/\*\s*@import.*?\*/!is", "", $content); /* выбираем все@import
*/ preg_match_all('/@import\\s*(url)?\\s*\\(?([^;]+?)\\)?;/i', $content, $imports, PREG_SET_ORDER); if (is_array($imports)) { foreach ($imports as $import) { $src = false; /* очищаем найденный путь к файлу от пробелов и кавычек */ if (isset($import[2])) { $src = $import[2]; $src = trim($src, '\'" '); } if ($src) { /* запускаем рекурсию для обнаруженного файла, чтобы разрешить все@import
уже внутри него */ $content = str_replace($import[0], $this->resolve_css_imports($src), $content); /* изменяем все пути для CSS-изображений и ресурсов (относительно заданного файла) на абсолютные (относительн корня документа) */ $content = $this->resolve_relative_paths($src, $content); } } } return $content; }
Задав полный путь к файлу стилей для функции resolve_css_imports
мы полностью разрешим все внутренние включения, чем сведем число HTTP-Запросов к минимуму.
После того, как мы разобрались с массивом файлов и научились получать полное их содержимое, нам нужно корректно их объединить. Как уже описывалось в книге "Разгони свой сайт" для этого лучше всего применять конструкцию @media
. Предположим, что в результирующем массиве у нас объект имеет следующий формат:
$this->initial_files = array( array( 'content' => 'полное содержимое файла', 'media' => 'print|handheld|etc', 'file_raw' => 'исходный код файла в head-секции' ), ... )
Тогда нам нужно просто объединить весь CSS-код в соответствие со спецификацией:
foreach ($this->initial_files as $file) { if (!empty($file['media'])) { $full_content .= '@media '. $file['media'] . '{'; } $full_content .= $file['content']; if (!empty($file['media'])) { $full_content .= '}'; } }
На выходе мы получим весь CSS-код, обнаруженный внутри head-секции, объединенный в одну строку, которую можно записать в один кэшированный файл. Далее, используя свойство file_raw
, удалить исходные файлы и внутренний код из документа и вставить (например, сразу же после <head>
) вызов этого кэшированного файла.
А что, если мы хотим не только объединить файлы, но и уменьшить их в размере? Gzip-компрессию здесь рассматривать не будем: она достаточно тривиальна в реализации (и может сводиться к нескольким правилам в конфигурационном файле сервера). Нам более интересен вопрос про уменьшение CSS-кода в соответствии с CSS-спецификацией. Здесь разумнее всего воспользоваться один из трех путей:
$data =~ s!\/\*(.*?)\*\/!!g; # удаляем комментарии $data =~ s!\s+! !g; # сжимаем пробелы $data =~ s!\} !}\n!g; # добавляем переводы строки $data =~ s!\n$!!; # удаляем последний перевод строки $data =~ s! \{ ! {!g; # удаляем лишние пробелы внутри скобок $data =~ s!; \}!}!g; # удаляем лишние пробелы и синтаксис # внутри скобок
class.csstidy.php
) и осуществить минимизацию простыми вызовами:При этом для максимального сжатия лучше использовать следующий шаблон ($css = new csstidy(); $css->load_template($root_dir . 'css.template.tpl'); $css->parse($css_code); echo $css->print->formatted();
css.template.tpl
):|{||{|||;|}||}||{||
java
на сервере и запускается еще проще. Необходимо из командной строки выполнить:Результат произведенных действий будет сохранен в файлеjava -jar yuicompressor.jar -o output.css input.css
output.css
.Полный код для CSS Tidy и все аспекты практической реализации можно почерпнуть из исходного кода Web Optimizer
Для JavaScript-файлов весь описанный механизм повторяется, за исключением небольших деталей.
Во-первых, получать код мы будем уже немного другим методом, и нам будет не существенен атрибут media
:
$regex = "!<script[^>]+type\\s*=\\s*(\"text/javascript\"|'text/javascript'|text/javascript)([^>]*)>(.*?</script>)!is"; preg_match_all($regex, $this->head, $matches, PREG_SET_ORDER); if (!empty($matches)) { foreach($matches as $match) { $file = array(); $file['tag'] = 'script'; $file['source'] = $match[0]; /* вырезаем из найденного куска HTML-кода обрамляющие теги, чтобы идентифицировать внутренние скрипты */ $file['content'] = preg_replace("/(<script[^>]*>[\t\s\r\n]*|[\t\s\r\n]*<\/script>)/i", "", $match[0]); $file['file'] = ''; preg_match_all("@(type|src)\s*=\s*(?:\"([^\"]+)\"|'([^']+)'|([\s]+))@i", $match[0], $variants, PREG_SET_ORDER); if(is_array($variants)) { foreach($variants AS $variant_type) { $variant_type[1] = strtolower($variant_type[1]); $variant_type[2] = !isset($variant_type[2]) ? (!isset($variant_type[3]) ? $variant_type[4] : $variant_type[3]) : $variant_type[2]; switch ($variant_type[1]) { case "src": $file['file'] = trim($this->strip_querystring($variant_type[2])); $file['file_raw'] = $variant_type[2]; break; default: $file[$variant_type[1]] = $variant_type[2]; break; } } } $this->initial_files[] = $file; } }
Тут нас ждет еще одно отличие: разные куски JavaScript-кода лучше объединять через точку с запятой с переводом строки. Ибо предыдущая часть кода может не оканчиваться на точку с запятой, потому мы обязаны как-то ее отделить от последующей.
Далее в ходе объединения было установлено, что файлы библиотек для визуального форматирования кода (в силу своей сложности) мало приспособлены к объединению с другими файлами. Поэтому рекомендуется при объединении избегать следующих файлов: tiny_mce.js
и fckeditor.js
. Во всем остальном механизм абсолютно тот же самый, что и для CSS-файлов (за исключением отсутствия необходимости разрешить @import и необходимости заменять пути для фоновых изображений и ресурсов).
Для минимизации JavaScript-кода лучше всего использовать уже имеющиеся на рынке решения: JSMin (который портирован в том числе и на PHP) или YUI Compressor. Про последний уже было написано чуть выше (параметры для запуска те же самые). В случае с JSMin все тоже довольно просто: нам нужно загрузить последнюю версию, подключить ее и просто вызвать минимизацию заданного файла:
require 'jsmin-1.1.1.php'; echo JSMin::minify(file_get_contents('example.js'));
Тут стоит только упомянуть, что классический JSMin не поддерживает условную компиляцию для IE. Поэтому тут нужно воспользоваться модифицированным решением (например, из исходников Web Optimizer).
Объединение текстовых файлов способно значительно ускорить загрузку вашего сайта, не причиняя вреда качеству разработки (вы можете разрабатывать отдельно, а на сам сайта выкладывать уже готовые версии файлов в автоматическом режиме). Как показывают данные с webo.in на происзольном сайте в Рунете используется 2,7 файлов стилей (средний размер 5,5Кб) и 5 JavaScript-файлов (средний размер ). Просто их объединение позволит выиграеть 0,5-1с при загрузке страницы. А минимизации (вместе с gzip-сжатием, уменьшающим размер на 85%) — еще 60 Кб, что составит 0,6с при скорости подключения 100Кб/с.
Как мы видим, совершенно простые действия способны значительно ускорить загрузку вашего сайта.