Объединение CSS-файлов. Автоматическое объединение текстовых файлов. Уменьшение количества запросов
Объединение JavaScript-файлов
Заключение
Несмотря на более простой и поистине академический синтаксис 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-запросов к минимуму.
Объединяем
После того, как мы разобрались с массивом файлов и научились получать полное их содержимое, нам нужно корректно их объединить. Как уже описывалось в книге «Разгони свой сайт» (http://speedupyourwebsite.ru/books/speed-up-your-website/) для этого лучше всего применять конструкцию @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-спецификацией. Здесь разумнее всего воспользоваться один из трех путей: Набор простых регулярных выражений. Он был описан еще в книге «Разгони свой сайт». Ниже приведен его код на Perl$data =~ s!\/\*(.*?)\*\/!!g; # удаляем комментарии
$data =~ s!\s+! !g; # сжимаем пробелы
$data =~ s!\} !}\n!g; # добавляем переводы строки
$data =~ s!\n$!!; # удаляем последний перевод строки
$data =~ s! \{ ! {!g; # удаляем лишние пробелы внутри скобок
$data =~ s!; \}!}!g; # удаляем лишние пробелы и синтаксис
# внутри скобок CSS Tidy (http://csstidy.sourceforge.net/). наиболее мощная библиотека для разбора CSS-правил. Для ее использования необходимо загрузить ее в папку проекта, внести изменения в настройки по умолчанию (находятся в файле 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):
|{||{|||;|}||}||{|| YUI Compressor (http://developer.yahoo.com/yui/compressor/). Эта библиотека требует установленной java на сервере и запускается еще проще. Необходимо из командной строки выполнить:
java -jar yuicompressor.jar -o output.css input.css
Результат произведенных действий будет сохранен в файле output.css.
Полный код для CSS Tidy и все аспекты практической реализации можно почерпнуть из исходного кода Web Optimizer (http://www.web-optimizer.ru/).
Сразу стоит оговориться, что в Intenet Explorer (по 8 версию включительно) есть проблема с отображением более 4096 (по сведениям из MSDN, http://msdn.microsoft.com/en-us/library/aa358796(VS.85).aspx) CSS-селекторов из одного файла (и ограничение в 32 на число @import). При разработке грамотного процесса объединения CSS-файлов этот момент стоит учитывать.