Kohana 3: модуль “kohana-static-files”


    При знакомстве с фреймворком, я первым делом смотрю не на его возможности, а на готовые решение, которые он предоставляет. В частности возможность удобно собирать JS/CSS файлы по частям и «отдавать» согласно рекомендациям по клиентской оптимизации (YSlow/Google PageSpeed). Ни в одном из просмотренных мной, нужной мне реализации я не увидел, даже в Django (которым, собственно, и был вдохновлен), поэтому решил сделать свое решение в виде готового к применению модуля для Kohana v.3.

    Итак, опишем основные потребности/хотелки, которые ставились перед разработкой модуля:
    1) Сборка inline CSS/JS по кусочкам
    2) Возможность отдавать п.1 путем вставки в код страницы либо сгенерировав и записав на диск файл, с уникальным именем.
    3) Возможность сборки внешних файлов CSS/JS в один билд
    4) Возможность указывать условие, при котором подключается тот или иной билд из пункта 3, а также любой другой внешний файл (
    <!--[if IE 7]>
    ).
    5) Возможность вынести статику на другой домен, главное чтобы он был на этом же физическом сервере.
    6) Использование CDN
    7) Минимизация CSS/JS.
    8) Самое важное: СПОСОБ, позволяющий включать статику (а эо обычно не только CSS/JS, но и, например. картинки) в распространяемые модули. Так как текущий способ, когда в modules/ переносится и подключается сам функционал модуля, а статика либо копируется в произвольное место DOCUMENT_ROOT, либо обязательное условие – чтобы modules находилась в DOCUMENT_ROOT.
    9) Возможность легко менять URL со статикой, чтобы он никак не конфликтовал с роутингом, например будет не хорошо, если вы захотите иметь раздел про CSS по урл ”/css/” когда до этого вы сделали это реально существующей директорией с файлами стилей.

    Т.е. нужно придумать и соблюдать в дальнейшем какие то соглашения.
    Многое из задуманного было воплощено в реально работающий модуль,.


    Чтобы показать работу модуля, а не просто дать ссылку на него, попробую поставить несложную задачу перед ним:
    1) взять за основу css-framework.ru
    2) попытаться реализовать пример css-framework.ru/demo/css-framework-layout.html на основе модуля “kohana-static-files”.

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

    Для этого скачиваем последнюю версию Kohana v.3.
    Переносим директории system, application, modules выше DOCUMENT_ROOT и, соотвественно, в index.php поменять пути:
    <?php
    $application = '../application';
    $modules = '../modules';
    $system = '../system';

    * This source code was highlighted with Source Code Highlighter.


    Далее, забираем последнюю версию с github.com/aberdnikov/kohana-static-files, копируем содержимое в modules.
    В файле application/bootstrapper.php подключаем модуль

    <?php
    Kohana::modules(array(
      // 'auth'    => MODPATH.'auth',    // Basic authentication
      // 'cache'   => MODPATH.'cache',   // Caching with multiple backends
      // 'codebench' => MODPATH.'codebench', // Benchmarking tool
      // 'database'  => MODPATH.'database',  // Database access
      // 'image'   => MODPATH.'image',   // Image manipulation
      // 'orm'    => MODPATH.'orm',    // Object Relationship Mapping
      // 'oauth'   => MODPATH.'oauth',   // OAuth authentication
      // 'pagination' => MODPATH.'pagination', // Paging of results
      // 'unittest'  => MODPATH.'unittest',  // Unit testing
      // 'userguide' => MODPATH.'userguide', // User guide and API documentation
      ' kohana-static-files' => MODPATH.' kohana-static-files ', // Static Files (JS/CSS/pictures)
      ));
    ?>


    * This source code was highlighted with Source Code Highlighter.


    В файле инициализации модуля «kohana-static-files» мы прописываем роутинг модуля, которые сам будет при первом обращении к «/!/static/style.css» находить согласно логике Kohana::find_file()
    — сперва в application/static-files/ style.css
    — затем в modules/{module_name}/static-files/style.css (где {module_name} это перебор подключенных модулей в порядке подключения в bootstrapper.php)
    — и только потом в system/static-files/style.css.

    Соответственно, вы заметили, что рядом с нативными директориями: classes, views, config, etc… появилась новый вид директорий «static-files», именно туда мы и будем складывать статику. Вспоминаем про пункт 9 хотелок в начале топика. Ведь мы понятия не имеем, какую директорию для статики выберет конкретный владелец конкретного сайта, использующего описываемый модуль.

    Соглашение №1
    Поэтому в файле «cssf-base.css» ищем строки с URL и делаем правки, заменяем абсолютные и относительные урл, а точнее их начало на подстроку для автозамены «{staticfiles_url}» и из строки типа
    .corners-2 em.tl, .corners-2 em.tr, .corners-2 em.bl, .corners-2 em.br { width: 4px; height: 4px; background-image: url(../i/corners/corners-2.png); }
    Мы должны получить
    .corners-2 em.tl, .corners-2 em.tr, .corners-2 em.bl, .corners-2 em.br { width: 4px; height: 4px; background-image: url({staticfiles_url}i/corners/corners-2.png); }
    Впоследствие
    url({staticfiles_url}i/corners/corners-2.png);
    станет чем то вроде
    url(http://static.site.ru/!/static/i/corners/corners-2.png);

    Соглашение №2
    Чтобы избежать коллизий имен статичных файлов следует размещать их по следущему принципу:
    1) для модулей следует их помещать в директорию с именем совпадающим с именем модуля, соотвественно, итоговый урл для файла стилей модуля новостей будет иметь вид: {staticfiles_url}news/style.css
    Соотвественно этот файл в файловой системе будет находиться например тут:
    MODDIR.’news/static-files/ style.css’
    2) для тем оформления текущего сайта статику следует размещать без поддиректорий, например основной файл стилей для оформления сайта будет иметь урл
    {staticfiles_url}/style.css
    и путь к нему может быть таким:
    APPDIR.’ static-files/ style.css’

    Двигаемся далее:
    Изменяем базовый контроллер Conrtoller_Welcome, который наследуем от Controller_Template, чтобы иметь возможность пользоваться кохановскими вьюхами.

    Вместо пустышки
      public function action_index()
      {
        $this->request->response = 'hello, world!';
      }


    * This source code was highlighted with Source Code Highlighter.


    Добавляем все то, что нужно для решения поставленной задачи, код понятен без объяснения:
      public function action_index() {
        StaticCss::instance()->addCss(Kohana::config('staticfiles.url').'news/style.css');
        StaticCss::instance()->addCssStatic('news/style.css');
        StaticCss::instance()->addCssStatic('css/cssf-base.css');
        StaticCss::instance()->addCssStatic('css/cssf-ie6.css', 'lte IE 6');
        StaticCss::instance()->addCssStatic('css/cssf-ie7.css', 'IE 7');
        StaticJs::instance()->addJsStatic('js/common.js');
        StaticCss::instance()->addCssInline('
          .lb-1 .corners { background: #818181; }
          .lb-2 .corners { background: #9a9a9a; }
          .lb-3 .corners { background: #b4b4b4; }
          .lb-4 .corners { background: #dadada; }
        ');
        StaticJs::instance()->addJsInline('CornersInit();');
        StaticJs::instance()->addJsOnload('alert(123)');
      }

    * This source code was highlighted with Source Code Highlighter.


    Следует лишь пояснить разницу между методами вида addJsStatic и addJs
    Они по сути одинаковы, просто первый метод внутри себя содержит обертку, добавляя в начало урл префикс из конфига. В примере с файлом стилей для новостей {staticfiles_url}news/style.css
    Можно делать либо так:
    StaticCss::instance()->addCss(Kohana::config('staticfiles.url').'news/style.css');

    * This source code was highlighted with Source Code Highlighter.

    либо более простым путем
    StaticCss::instance()->addCssStatic('news/style.css');

    * This source code was highlighted with Source Code Highlighter.


    Еще хотелось бы обратить внимание на сборку по частям скрипта, который должен будет выполнен при событии onload. Так как в модуле основным JS-фреймворком принят jQuery, то и при вызове
    StaticJs::instance()->addJsOnload('alert(123)');

    * This source code was highlighted with Source Code Highlighter.

    будет сгенерирована конструкция вида:
    <script language="JavaScript" type="text/javascript">
      jQuery(document).ready(
        function(){
          alert(123)
        }
      );
    </script>

    * This source code was highlighted with Source Code Highlighter.

    Т.е. вам не надо будет заморачиваться ни создание обертки («jQuery(document).ready»), ни подключением jQuery, по умолчанию при первом вызове addJsOnload будет подключен jQuery, для этого есть специальный метод needJquery() в классе Kohana_StaticJs. Естественно, если вы будете использовать модуль в интранете без выхода в Интернет, то просто переопределите в StaticJs метод needJquery() подключая файл с диска.

    А теперь обратим наше внимание на конфиг модуля по частям
      'js' => array(
        //минимизация скриптов
        'min' => true,
        //сборка в один файл по типу (external, inline, onload)
        'build' => false,
      ),
      'css' => array(
        //минимизация стилей
        'min' => true,
        //сборка в один файл по типу (external, inline)
        'build' => true,
      ),

    * This source code was highlighted with Source Code Highlighter.


    В этом месте вы имеете возможность управлять необходимостью умной сборки в один файл по типу содержимого (JS/CSS), виду (external, inline, onload), условий (
    <!--[if IE 7]>
    ),
    минимизации (удаление комментариев, лишних пробелов и переносов строк).

    Соглашение №3
    Если в урл до подключаемого файла есть подстрока «.min.» — он считается уже сжатым и даже при включенной опции сжатия этот файл будет проигнорирован, а если будет включена опция собирать билд. То он соотвественно будет добавлен в билд «as is».

    'path' => realpath(DOCROOT),

    * This source code was highlighted with Source Code Highlighter.

    Это директория DOCUMENT_ROOT для домена со статикой, по умолчанию это тот же самый домен.
    //сюда будут копироваться статические файлы если не требуется их сборка в билды
    'url' => '/!/static/',
    //сюда будут складываться сгенерированные скрипты и файлы стилей
    'cache' => '/!/cache/',

    * This source code was highlighted with Source Code Highlighter.

    Здесь хотелось бы прокомментировать наличие восклицательного знака: это как раз касается пункта 9 «хотелок», про коллизии и борьбу с ними.
    'host' => 'http://'.$_SERVER['HTTP_HOST'],

    * This source code was highlighted with Source Code Highlighter.

    Это собственно домен, с которого будет раздаваться статика, варианты использования:
    1) "" — ссылки будут иметь вид: "/pic.jpg"
    2) «ya.ru» — ссылки будут иметь вид: «ya.ru/pic.jpg»
    * Для использования Coral CDN
    * добавьте в имени текущего домена со статикой суффикс ".nyud.net"
    * например для домена «google.com» установите хост «google.com.nyud.net»
    * Больше информации тут: habrahabr.ru/blogs/i_recommend/82739 3) «ya.ru.nyud.net» — ссылки будут иметь вид: «ya.ru.nyud.net/pic.jpg»

    Чтобы иметь возможность использования автозамены в уже сгенерированном ответе чуть-чуть изменим метод after контроллера:
      public function after() {
        parent::after();
        $this->request->response = str_replace('{statifiles_url}',
                STATICFILES_URL,
                $this->request->response);
      }

    * This source code was highlighted with Source Code Highlighter.

    Ну и, напоследок, остается рассказать об устройстве представления (View)
    Согласно рекомендациям YSlow/GooglePageSpeed в раздел head вставляем CSS
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><br><html xmlns="http://www.w3.org/1999/xhtml"><br>    <head profile="http://gmpg.org/xfn/11"><br>        <title>css-framework / layout-box</title><br>        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /><br>        <meta http-equiv="imagetoolbar" content="no" /><br>        <link rel="icon" href="{statifiles_url}favicon.ico" type="image/x-icon" /><br>        <link rel="shortcut icon" href="{statifiles_url}favicon.ico" type="image/x-icon" /><br>        <?php echo StaticCss::instance()->getCssAll(); ?><br>    </head><br><br>* This source code was highlighted with Source Code Highlighter.

    и перед закрывающимся производим вызов JS вставок
            <?php echo StaticJs::instance()->getJsAll(); ?><br>    </body><br></html><br><br>* This source code was highlighted with Source Code Highlighter.


    С поставленными задачами, по-моему модуль справился:
    — количество HTTP-запросов уменьшил;
    — CDN опционально подключается;
    — CSS вставлен в начало страницы;
    — JS вставлен в конец страницы;
    — подключаются (по одиночке или билдом) ТОЛЬКО необходимые файлы (есть и небольшой недостаток — на все варианты использования будут созданы отдельные билды);
    — inline стили и скрипты могут быть вынесены в подключаемые файлы;
    — повторяющиеся подключения скриптов и стилей исключены.

    Сейчас нам остается только позвать на помощь nginx, чтобы он расставил правильные заголовки для отдаваемой статики (gzip, ETags, Expires).

    Еще раз напомню, что сам модуль можете забрать/форкнуть тут:
    github.com/aberdnikov/kohana-static-files

    На всякий случай покажу, что получилось в результате работы модуля (вручную внес только переносы строк, чтобы без горизонтального скроллинга показать HTML)
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><br><html xmlns="http://www.w3.org/1999/xhtml"><br>    <head profile="http://gmpg.org/xfn/11"><br>        <title>css-framework / layout-box</title><br>        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /><br>        <meta http-equiv="imagetoolbar" content="no" /><br>        <link rel="icon" href="http://static.site.ru/!/static/favicon.ico" type="image/x-icon" /><br>        <link rel="shortcut icon" <br>             href="http://static.site.ru/!/static/favicon.ico" <br>             type="image/x-icon" /><br>        <link rel="stylesheet" <br>             href="http://static.site.ru/!/cache/css/3/7/3741c0ac0c2f8409beea116d6f4d6922.css" <br>             media="all" type="text/css" />        <br>        <!--[lte IE 6]><link rel="stylesheet" <br>            href="http://static.site.ru/!/cache/css/lte-ie-6/1/6/161456642f5cfc18e731472d29293b28.css" <br>            media="all" type="text/css" /><![endif]-->        <br>        <!--[IE 7]><link rel="stylesheet" <br>            href="http://static.site.ru/!/cache/css/ie-7/c/b/cb4a089038b23dd1bfc5d0dfbfd35a68.css" <br>            media="all" type="text/css" /><![endif]--><br>        <link rel="stylesheet" <br>             href="http://static.site.ru/!/cache/css/inline/e/c/ec905aaa7ee63d90a646593b7e665936.css" <br>             media="all" type="text/css" />    <br>    </head><br>    <body><br>        ... [skip html code] ...<br>        <script language="JavaScript" <br>                type="text/javascript" <br>                src="http://static.site.ru/!/cache/js/8/e/8e022d3b6bcba59dcba5c586d408f7b2.js"></script><br>        <script language="JavaScript" <br>                type="text/javascript" <br>                src="http://static.site.ru/!/cache/js/inline/b/2/b2044b150de0ef43233d3491d060a5f6.js"></script><br>        <script language="JavaScript" <br>                type="text/javascript" <br>                src="http://static.site.ru/!/cache/js/onload/1/5/15fb097828dd52d44bf36e77a96144b6.js"></script><br>    </body><br></html><br><br>* This source code was highlighted with Source Code Highlighter.


    P.S.: главное отличие от идеи из модуля userguide по раздаче статики в том, что тут сложные билды генерируются при первом запросе сразу же еще ДО того, как будут вызваны из кода страницы отдельным запросом, а простая статика копируется в директорию, доступную по HTTP при первом запросе, а потом уже отдается веб-сервером, а не через PHP.
    Т.е. deploy производится при первом требовании.
    Из недостатков вижу только невозможность автоматического определения изменения файла, из которого был создал билд, так как имя билда получается из имен подключаемых файлов (иначе это сильно скажется на быстродействии модуля), это только в случае inline стилей/скриптов имя билда получается на основании содержимого.

    Поэтому при апдейте просто убивайте директорию /!/ с билдами и кэшами.

    В планах добавить методы для автоматического развертывания статики (та, которая не требует сборки) + совет перед большой нагрузкой разогреть кэш, погоняв по сайту например siege, который создаст большинство необходимых кэшей.

    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 23

      +1
      Хорошая реализация. Видел достаточно работ, выполняющих данный функционал, но ваша, определенно, заслуживает особого внимания.
        +1
        Только одно замечание: коменты зря вы на русском писали
          0
          Спасибо за отзыв.
          Увы, в английском не настолько силен, можете помочь?
            0
            У меня были некоторые заготовки по написанию своего велосипеда, в более ООП стиле, подробнее изучу ваш код, свяжемся. Мне кажется очень полезно было бы анонсировать модуль на оф. форуме коханы
      0
      Очень интересно, спасибо. Поковырялся в исходниках немного, вот тут наверное стоит немного по-другому сделать, чтобы обезопаситься от возможных ошибок. Вместо substr() сделайте trim() символа '/', т.е. примерно так:

      Route::set(
         'static_files', 
         trim(Kohana::config('staticfiles.url'), '/').'/<file>', 
         array('file'=>'.*')
      )->...
      


        0
        Т.е. в конфигах можно будет не париться, поставил слэши в начале/конце параметра или нет.
          0
          В данном случае не соглашусь с Вами, так как это тоже можно считать соглашением и оно используется не только для разбора входящего урл, но и для его формирования, а также для формирования путей сохранения развертываемой статики.
          Поэтому конфиг надо писать ожидаемый системой, а не в произвольном виде.
            +2
            Почему бы это значение не считать один раз из конфига, обработать (проверить на соответствие соглашению, допилить если что) и сохранить в свойствах класса? Далее юзать его, а не конфиг.
              0
              Соглашусь, спасибо за отзыв.
        +2
        я бы сделал примерно так:
        public function action_index()
        {
            Static::instance('css')
                ->css(Kohana::config('staticfiles.url').'news/style.css')
                ->static('news/style.css')
                ->static('css/cssf-base.css');
            
            Static::instance('js')
                ->static('js/common.js');
                
            Static::instance('css')
                ->inline('
                    .lb-1 .corners { background: #818181; }
                    .lb-2 .corners { background: #9a9a9a; }
                    .lb-3 .corners { background: #b4b4b4; }
                    .lb-4 .corners { background: #dadada; }
                ');
            
            Static::instance('js')
                ->inline('CornersInit();')
                ->onload('alert(123);');
        }
        
          +1
          chain-методы — это, конечно, здорово, только интерпретатор едва ли прожует такой класс
          <?php
          class Static{
              function static(){
                  
              }
          }
          ?>

          Да и не вижу смысла совмещать в одном классе белое и острое (я про CSS&JS), другое дело — совместить в одном пакете/модуле!
          И как то роднее мне, когда имя метода содержит в себе глагол addCss(), а не как в Вашем примере: css()
          0
          Для Windows пути на диске тоже будут с прямыми слэшами?
            0
            Тоже. В пхп \ и / (и в коде и в конфигах) работают одинаково.
            +1
            Имхо, статику должен все-таки обрабатывать веб-сервер, наподобие nginx, и без привлечения PHP.
            Объединять и сжимать статичные файлы можно и нужно, но явно не в момент обработки запроса от пользователя.
            • UFO just landed and posted this here
                0
                >> должен лежать
                Не должен. Дело хозяйское как хранить вендорские либы.

                >> 4. Отсутствует стандартная кохановская проверка "<?php defined('SYSPATH') or die('No direct script access.');"
                Она вообще бессмысленная. Никакой полезной нагрузке в этой строке нет.
                  –1
                  1. Давайте начнем с того, что сменим тон, если Вам не нравится оформление кода (которые, как я обратил внимание, увы, мало соблюдает) или что то еще — Ваше право не использовать его.
                  Но думаю гораздо важнее упаковки то, что находится внутри, я прав? Функциональности лично мне более чем достаточно. если это будет важно и интересно еще кому то, я выделю пару выходных и доделаю и тесты, и документацию — пока я такого не увидел.

                  2. Ниже в комментариях привели модуль с частично-подобной функциональность, как ни странно наблюдаю там путь до вендора
                  github.com/gahgneh/minify-kohana/blob/master/minify/classes/jsmin.php

                  3. Увы, тестов нет, как и банальной документации.

                  4. Не считаю особо умным решение выкладывать
                  — modules
                  — classes
                  — system
                  в DOCUMENT_ROOT, чтобы потом защищаться проверками времен phpnuke, даже если это и стандарт коханы. То, что будет вызван напрямую файл вида
                  <?php
                  class StaticJs{
                    ...
                  }
                  
                  не вижу ничего криминального. будет получена просто пустая страница, или Вы не согласны с этим?
                  Думаю дух закона важнее буквы? вот в init.php это может быть и критично, так как могут вывалиться ошибки, но там эта проверка есть:
                  github.com/aberdnikov/kohana-static-files/blob/master/init.php

                  5. Я сделал решение, которое устраивает меня на сто процентов, знаю, что многие испытывают такую же потребность — потому и выложил, если хотите помочь с недостатками модуля — документация/англ. комменты — добро пожаловать.

                  Предыдущий комментатор не прав, начиная с синтаксиса, заканчивая тем, что в его случае не работает автокомплит в IDE
                  <?php
                  Static::instance('js')
                  
                  • UFO just landed and posted this here
                    0
                    А контакты по которым обращаться? :-)
                    0
                    есть ещё minify для коханы qr.ee/minifyforcohana
                      0
                      Вы внимательно читали мои «хотелки»?
                      Не спорю, может быть модуль minify и хорош, не смотрел детально, но уже на первой моей «хотелке» он бы сдался.
                      А мой модуль устраивает меня на все 100%
                      0
                      Столкнулся с неприятной проблемой, которая не видна с первого, и даже воторго, взглада.
                      Если нет статического файла по запрашиваемому URL-у, то выполняется контроллер, который ищет нужный файл и копирует его в папку 'cache'. Так вот беда подкралась именно при попытке создать нужные папки перед тем как скопировать туда файл. Функция mkdir не часто, но с завидной периодичностью, выкидывает исключения с ошибкой 'File exists', и даже после проверки на существования этой папки. Нужно учесть то, что браузер тянет несколько статических файлов одновременно и выстреливают случаи (и довольно часто), когда file_exists вернул false, а при выполнении mkdir папка уже была создана.

                      Only users with full accounts can post comments. Log in, please.