Pull to refresh

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

Reading time 13 min
Views 5.3K

При знакомстве с фреймворком, я первым делом смотрю не на его возможности, а на готовые решение, которые он предоставляет. В частности возможность удобно собирать 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, который создаст большинство необходимых кэшей.
Tags:
Hubs:
+21
Comments 23
Comments Comments 23

Articles