Форум программистов «Весельчак У»
  *
Добро пожаловать, Гость. Пожалуйста, войдите или зарегистрируйтесь.
Вам не пришло письмо с кодом активации?

  • Рекомендуем проверить настройки временной зоны в вашем профиле (страница "Внешний вид форума", пункт "Часовой пояс:").
  • У нас больше нет рассылок. Если вам приходят письма от наших бывших рассылок mail.ru и subscribe.ru, то знайте, что это не мы рассылаем.
   Начало  
Наши сайты
Помощь Поиск Календарь Почта Войти Регистрация  
 
Страниц: [1]   Вниз
  Печать  
Автор Тема: Динамическое формирование CSS из множества файлов.  (Прочитано 12040 раз)
0 Пользователей и 1 Гость смотрят эту тему.
RXL
Технический
Администратор

ru
Offline Offline
Пол: Мужской

WWW
« : 28-12-2011 07:19 » 

Есть следующая практическая задача.
В GeSHi (подсветка кода) десятки CSS-файлов.
С точки зрения поддержки их удобнее хранить раздельно — не сливать в один или несколько крупных.
С точки зрения пользователя наоборот — лучше, если файлов будет мало, т.к. даже проверка на обновление (ответ 304) требует кучи запросов на каждую загруженную страницу сайта. Это очень медленно.

Решение с множеством файлов: один "головной" CSS-файл, наполненный директивами @import — по одной на каждый подгружаемый файл. Плюсы: легко правится руками, обновление содержимого проверяется веб-сервером. Минусы: очень много обращений от браузера.

Простое решение: сделать php-скрипт, который прочтет все CSS-файлы и отдаст их единым блоком. Для оптимизации можно использовать кеширование и gzip-сжатие. Но есть один нюанс: некоторые браузеры (не будем указывать пальцем, но вы уже догадываетесь, какие) воспринимают только первые 64 кБ от CSS-файла.

Следующий вариант: php-скрипт должен прочесть все файлы, скомпоновать из них N файлов не более 64 кБ каждый и на выход дать CSS с директивами @import для подгрузки данных файлов. Файлы также отдавать этим же скриптом, но с параметром. Нужны будут временные файлы. Можно закешировать для ускорения.

Есть еще идеи?
Записан

... мы преодолеваем эту трудность без синтеза распределенных прототипов. (с) Жуков М.С.
Sla
Команда клуба

ua
Offline Offline
Пол: Мужской

WWW
« Ответ #1 : 28-12-2011 09:44 » 

Цитата
«никогда не переписывайте то, что можно просто вырезать и наклеить»
http://habrahabr.ru/blogs/webdev/134405/

Цитата
первые 64 кБ от CSS-файла
Ух-ты... не знал Жаль (не было таких больших.


К сожалению CSS это не динамический "язык", а так иногда не хватает констант/переменных

Записан

Мы все учились понемногу... Чему-нибудь и как-нибудь.
RXL
Технический
Администратор

ru
Offline Offline
Пол: Мужской

WWW
« Ответ #2 : 28-12-2011 10:23 » 

Цитата
Здесь еще одна есть хитрость, в данном случае скорее — еще одна условность – в файлах html мы будем писать запросы к css или js в следующтим виде:
«/glue/1.css—2.css—3-4-5.css», где «-» — это замена «/», а «--» – это разделитель файлов.
Кроме того в именах могут быть только английские буквы, цифры и символ «_», по мне — этого более, чем достаточно.

В моем случае комбинация файлов более недетерминирована. По этому склеиваемый список хочу составлять динамически.

Добавлено через 5 дней, 10 часов, 28 минут и 50 секунд:
Реализовал. Код здесь не привожу из-за бессмысленности его в отрыве от остального кода форума.
« Последнее редактирование: 02-01-2012 20:52 от RXL » Записан

... мы преодолеваем эту трудность без синтеза распределенных прототипов. (с) Жуков М.С.
RXL
Технический
Администратор

ru
Offline Offline
Пол: Мужской

WWW
« Ответ #3 : 03-01-2012 10:42 » 

Подчистил и прокомментировал. Надеюсь, будет понятно, что хотелось и что получилось. Возможно файловая составляющая избыточна.

Код: (PHP)
    public static function outputCSS()
    {
        global $modSettings, $vu_temp_dir, $scripturl, $boarddir, $sourcedir, $settings;

        // Хранимые данные состоят из следующих частей: блок переменных состояния (vars),
        // блок ссылок на части (head) и набор склеенных частей (part).
        $keyBase = __CLASS__ . '-css-';
        $fileBase = $vu_temp_dir . __CLASS__ . '-css-';
        $key = array(
            'vars' => $keyBase . 'vars',
            'head' => $keyBase . 'head',
            'part' => $keyBase . 'part_',
            // Многие проверка производится только в момент устаревания кеша.
            // По этому его TTL должен быть умеренной продолжительности.
            'ttl' => 120,
        );
        $file = array(
            'vars' => $fileBase . 'vars',
            'head' => $fileBase . 'head',
            'part' => $fileBase . 'part_',
            // Используется для заголовка Expires.
            'ttl' => 86400,
        );
        $path = array(
            'lock' => $fileBase . 'lock',
            'css' => $boarddir . '/Themes/default/geshi/',
            'url' => $scripturl . '?action=geshi-css',
        );
        // Максимальный размер отдаваемой части CSS-данных.
        $maxPartSize = 32767;

        $data = null;
        $content = array();
        $cacheSupported = (!empty($modSettings) && !empty($modSettings['cache_enable']));

        // Блокировка, чтобы не было конфликта при устаревании кеша.
        $lock = fopen($path['lock'], 'w+');
        flock($lock, LOCK_EX);

        if ($cacheSupported)
        {
            $data = cache_get_data($key['vars']);
            $source = 'cache';
        }

        // Если данные в кеше есть, то никаких проверок не производим.
        if ($data === null)
        {
            // Попытка считать те же данные из файла.
            $data = @file_get_contents($file['vars']);
            $source = 'none';

            if ($data !== false)
            {
                $data = @unserialize($data);

                // Как минимум, данные должны быть массивом.
                // Также проверяем соответствие количества исходных CSS-файлов в сохраненном списке и в GeSHi.
                if (is_array($data) && count($data['cssFiles']) == count(array_unique(self::$langs)))
                {
                    $source = 'file';

                    // Проверка на изменение состава исходных CSS-файлов, их mtime и размеров.
                    foreach ($data['cssFiles'] as $css)
                    {
                        if (file_exists($css['file']) == $css['exists'])
                        {

                            if ($css['exists'] === true)
                            {
                                $stat = stat($css['file']);

                                if ($stat[7] != $css['size'] || $stat[9] != $css['mtime'])
                                {
                                    $source = 'none';
                                    break;
                                }
                            }
                        }
                        else
                        {
                            $source = 'none';
                            break;
                        }
                    }
                }
            }
        }

        // Типовой хак для сброса файлового кеша: mtime текущего файла класса.
        if ($source == 'file')
        {
            if (filemtime(__FILE__) > $data['lastModified'])
                $source = 'none';
        }

        // Переходим к обработке исходных файлов.
        if ($source == 'none')
        {
            // Принудительно перечитываем список
            self::init(true);

            // Удаляем все вспомогательные файлы.
            foreach (glob($fileBase . '*') as $filename)
                @unlink($filename);

            // Создаем их заново, но пока в переменных.
            $data = array(
                'cssFiles' => array(),
                'partFiles' => array(),
            );
            $langs = array_unique(self::$langs);
            $partNumber = 1;
            $partSize = 0;
            $partContent = '';

            foreach ($langs as $lang)
            {
                $css = array(
                    'lang' => $lang,
                    'file' => $path['css'] . $lang . '.css',
                    'exists' => false,
                );

                if (file_exists($css['file']))
                {
                    $stat = stat($css['file']);
                    $css['exists'] = true;
                    $css['size'] = $stat[7];
                    $css['mtime'] = $stat[9];

                    if ($css['size'] + $partSize > $maxPartSize)
                    {
                        $content[$partNumber] = $partContent;
                        $partNumber++;
                        $partSize = 0;
                        $partContent = '';
                    }

                    $partContent .= file_get_contents($css['file']);
                    $partSize += $css['size'];
                }

                $data['cssFiles'][$lang] = $css;
            }

            if ($partSize > 0)
                $content[$partNumber] = $partContent;
            else
                $partNumber--;

            $data['partsCount'] = $partNumber;
            $content['head'] = '';

            for ($part = 1; $part <= $data['partsCount']; $part++)
            {
                $content['head'] .= '@import url("' . $path['url'] . ';part=' . $part . '") screen;' . "\n";
                $data['partFiles'][$part] = array(
                    'file' => $file['part'] . $part,
                    'size' => strlen($content[$part]),
                    'md5' => md5($content[$part]),
                );
            }

            $data['partFiles']['head'] = array(
                'file' => $file['head'],
                'size' => strlen($content['head']),
                'md5' => md5($content['head']),
            );

            $data['lastModified'] = time();

            // СБрос переменных и блоков данных в файлы.
            for ($part = 1; $part <= $data['partsCount']; $part++)
                file_put_contents($file['part'] . $part, $content[$part]);

            file_put_contents($file['head'], $content['head']);
            file_put_contents($file['vars'], serialize($data));
            $source = 'file';
        }

        // Если кеш разрешен, то копируем туда данные из переменных.
        // Данный код используется при создании данных из исходных файлов,
        // а также при устаревании кеша.
        if ($source == 'file' && $cacheSupported)
        {
            $valid = true;

            if (!isset($content['head']))
            {
                $content['head'] = file_get_contents($file['head']);
                $valid &= ($content['head'] !== false);
            }

            for ($part = 1; $part <= $data['partsCount']; $part++)
                if (!isset($content[$part]))
                {
                    $content[$part] = file_get_contents($file['part'] . $part);
                    $valid &= ($content[$part] !== false);
                }

            // Если хотя бы один блок данных не существует, то в кеш ничего не помещаем.
            if ($valid)
            {
                for ($part = 1; $part <= $data['partsCount']; $part++)
                    cache_put_data($key['part'] . $part, $content[$part], $key['ttl']);

                cache_put_data($key['head'], $content['head'], $key['ttl']);
                cache_put_data($key['vars'], $data, $key['ttl'] - 1);
                $source = 'cache';
            }
            else
            {
                // TODO: ситуация требует разбора. Вероятно кто-то удалил файл.
                // Пока пусть ошибка всплывет, но делать ничего не будем.
                // Тем более, что отсутсвие блока данных обрабатывается ниже.
                log_error("VUHighLighter::outputCSS(): набор файлов испорчен. Не хватает одного или более.", __FILE__, __LINE__);
            }
        }

        // Отпускаем блокировку.
        // Если данные были в кеше, то время блокировки очень мало.
        fclose($lock);

        // Проверка единственного параметра.
        $part = isset($_REQUEST['part']) ?
                is_numeric($_REQUEST['part']) &&
                    $_REQUEST['part'] >= 1 &&
                    $_REQUEST['part'] <= $data['partsCount'] ?
                (int)$_REQUEST['part']
                : null
            : 'head';

        // TODO: хорошо бы проверять и общий состав параметров, но для CSS-фйалов это не имеет смысла.

        // Ошибочные значения отвергаем.
        if ($part === null)
        {
            header('HTTP/1.1 404 Not Found');
            exit;
        }

        $partFile = $data['partFiles'][$part];
        $notModified = false;

        // Если запрошенный блок данных еще не подгружен.
        if (!isset($content[$part]))
        {
            $partKey = ($part == 'head') ? $key['head'] : ($key['part'] . $part);

            // Пробуем загрузить из кеша.
            if ($source == 'cache')
                $content[$part] = cache_get_data($partKey);

            // Пробуем загрузить из файла.
            if ($content[$part] === null)
                $content[$part] = file_get_contents($partFile['file']);

            // Данных нет. Аварийная ситуация.
            if ($content[$part] === false)
            {
                // Удалим файлы.
                foreach (glob($fileBase . '*') as $filename)
                    @unlink($filename);

                // Затрем данные в кеше.
                cache_put_data($key['vars'], null, 1);

                // Попробуем дать понять клиенту, что нужен повторный запрос.
                header('Connection: close');
                header('Cache-Control: no-cache');
                header('Expires: ' . gmdate('D, d M Y H:i:s', time() - 365 * 86400) . ' GMT');
                header('Refresh: 0; url=' . $path['url']);
                header('Content-Type: text/css; charset=utf-8');
                echo "/* File expired. */\n";
                exit;
            }
        }

        // Если время lastModified не больше If-Modified-Since клиента, то считаем, что данные не менялись.
        if (!empty($_SERVER['HTTP_IF_MODIFIED_SINCE']))
        {
            list($modifiedSince) = explode(';', $_SERVER['HTTP_IF_MODIFIED_SINCE']);
            $notModified = ($data['lastModified'] <= strtotime($modifiedSince));
        }

        // Проверку MD5 считаем более авторитетной.
        if (!empty($_SERVER['HTTP_IF_NONE_MATCH']))
            $notModified = ($_SERVER['HTTP_IF_NONE_MATCH'] == $partFile['md5']);

        // Если данные не менялись, то отвечаем 304.
        if ($notModified)
        {
            header('HTTP/1.1 304 Not Modified');
            exit;
        }

        // Передаем данные запрошенной части.
        header('Pragma: ');
        header('Cache-Control: public');
        header('Expires: ' . gmdate('D, d M Y H:i:s', time() + $file['ttl']) . ' GMT');
        header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $data['lastModified']) . ' GMT');
        header('Set-Cookie:');
        header('ETag: ' . $partFile['md5']);
        header('Content-Type: text/css; charset=utf-8');
        header('Content-Length: ' . $partFile['size']);
        echo $content[$part];
        exit;
    }

Вспомогательные файлы:

$ ls -l VUHighLighter-css-*
-rw-r--r-- 1 apache apache   240 Янв  3 14:27 VUHighLighter-css-head
-rw-r--r-- 1 apache apache     0 Янв  3 14:44 VUHighLighter-css-lock
-rw-r--r-- 1 apache apache 32319 Янв  3 14:27 VUHighLighter-css-part_1
-rw-r--r-- 1 apache apache 31518 Янв  3 14:27 VUHighLighter-css-part_2
-rw-r--r-- 1 apache apache 25242 Янв  3 14:27 VUHighLighter-css-part_3
-rw-r--r-- 1 apache apache 13752 Янв  3 14:27 VUHighLighter-css-vars

Прошу заметить, что полный набор CSS-файлов GeSHi весит примерно 230 кБ. Мой код работает только с теми типами, которые "одобрены" другой частью кода, здесь не показанной. Список одобренных типов в переменной self::$langs и создается методом self::init().

Добавлено через 41 минуту и 48 секунд:
И прошу критику. Желательно конструктивную.

Диаграмма загрузки.



Тоже, с принудительным обновлением кеша.


* firebug__css_load.png (9.53 Кб - загружено 2697 раз.)
* firebug__css_load__no-cache.png (9.24 Кб - загружено 2671 раз.)
« Последнее редактирование: 05-01-2012 20:23 от RXL » Записан

... мы преодолеваем эту трудность без синтеза распределенных прототипов. (с) Жуков М.С.
Sla
Команда клуба

ua
Offline Offline
Пол: Мужской

WWW
« Ответ #4 : 03-01-2012 11:43 » 

бр...
а почему 304-й ответ медленнее 200-го?
Записан

Мы все учились понемногу... Чему-нибудь и как-нибудь.
RXL
Технический
Администратор

ru
Offline Offline
Пол: Мужской

WWW
« Ответ #5 : 03-01-2012 11:45 » new

Слав, запрос через Инет. Флуктуации...
Возможно свою долю вносит Nginx. Попробую подавить gzip-сжатие и проверю еще раз. А может это браузер вносит, т.к. для no-cache ему не нужно проверять файл в своем кеше.


Добавлено через 27 минут и 12 секунд:
Отключил сжатие только для данного URL. Несколько раз принудительно освежился:




После обычная загрузка 304:


* firebug__css_load__no-cache.png (9.59 Кб - загружено 2649 раз.)
* firebug__css_load.png (9.66 Кб - загружено 2684 раз.)
« Последнее редактирование: 03-01-2012 12:14 от RXL » Записан

... мы преодолеваем эту трудность без синтеза распределенных прототипов. (с) Жуков М.С.
Страниц: [1]   Вверх
  Печать  
 

Powered by SMF 1.1.21 | SMF © 2015, Simple Machines