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

  • Рекомендуем проверить настройки временной зоны в вашем профиле (страница "Внешний вид форума", пункт "Часовой пояс:").
  • У нас больше нет рассылок. Если вам приходят письма от наших бывших рассылок mail.ru и subscribe.ru, то знайте, что это не мы рассылаем.
   Начало  
Наши сайты
Помощь Поиск Календарь Почта Войти Регистрация  
 
Страниц: [1]   Вниз
  Печать  
Автор Тема: Динамическое формирование CSS из множества файлов.  (Прочитано 5190 раз)
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 Кб - загружено 928 раз.)
* firebug__css_load__no-cache.png (9.24 Кб - загружено 899 раз.)
« Последнее редактирование: 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 » 

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


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




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


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

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

Хз, я не очень просто не очень во всё это верю, во всякие там сатурны и прочую поебень.
Страниц: [1]   Вверх
  Печать  
 

Powered by SMF 1.1.21 | SMF © 2015, Simple Machines