Bender: идейный борец за минимальность CSS / Javascript

    Не так давно на Хабре уже была статья про комбинатор CSS / Javascript файлов: плагин для Smarty — Combine. Дело это полезное, поскольку позволяет ускорить загрузку страниц и снизить нагрузку на сервер. Тогда появилась идея создать свой комбинатор + минимизатор, который можно было бы использовать не только в проектах на Smarty, а вообще в любых. Идея превратилась в Bender. Он должен удовлетворять следующим требованиям:

    • Простая и быстрая интеграция в любой готовый проект, с минимальными изменениями последнего
    • Минимальный размер кода и количество файлов
    • Универсальность и независимость от технологий и фреймворков — чтобы работал на любом хостинге, даже самом ограниченном

    Изначально задача состояла в оптимизации загрузки CSS / Javascript одного проекта, основанного на CS-Cart (он тоже использует Smarty). В проекте последовательно загружались 17 CSS файлов, и 15 Javascript, что конечно же, никуда не годится. Из них примерно половина приходилась на сам движок CS-Cart, а вторая — на установленные аддоны. Нужно было решение, которое бы позволило объединить CSS / Javascript и сделать это с минимальными изменениями оригинальных файлов проекта. Аддоны сами подгружают свои CSS и Javascript. Не хотелось лезть в логику самих аддонов, поэтому Bender позволяет делать очередь из CSS / Javascript, по той же схеме, что и Wordpress.

    Bender не претендует на уникальность и широкие возможности (как например, assetic). Здесь другая задача: минимальность (вместе с упаковщиками это всего 3 файла), и простота подключения к существующему проекту.

    Как это использовать?


    В качестве примера возьмём кусок кода, который подключает CSS и Javascript, по четыре штуки в ряд две штуки каждого:

    <html lang="en">
        <head>
            <link href="assets/css/bootstrap.css" rel="stylesheet" type="text/css" />
            <link href="assets/css/bootstrap-theme.css" rel="stylesheet" type="text/css" />
            <script type="text/javascript" src="assets/js/jquery-1.10.2.js"></script>
            <script type="text/javascript" src="assets/js/bootstrap.js"></script>
        </head>
        <body>
            ...
        </body>
    </html>
    

    Теперь добавим Bender:

    <html lang="en">
        <head>
            <?php
            require_once "Bender/Bender.class.php";
            $bender = new Bender();
            // Ставим в очередь наши файлы CSS и JS
            $bender->enqueue("assets/css/bootstrap.css");
            $bender->enqueue("assets/css/bootstrap-theme.css");
            $bender->enqueue("assets/js/jquery-1.10.2.js");
            $bender->enqueue("assets/js/bootstrap.js");
            // В качестве альтернативы, вы можете добавить все файлы в виде массива: $bender->enqueue(array("assets/css/bootstrap.css", "assets/css/bootstrap-theme.css", ...));
            // Вставляем ссылку на скомбинированный / упакованный CSS <link rel="stylesheet" href="..." /> в секцию <head>
            echo $bender->output("cache/stylesheet.css");
            ?>
        </head>
        <body>
            ...
            <?php
            // вставляем ссыку на скомбинированный / упакованный JS <script src="..."> перед </body> (для распараллеливания загрузки)
            echo $bender->output("cache/javascript.js");
            ?>
        </body>
    </html>
    

    Результатом работы Bender будет следующий код:

    <html lang="en">
        <head>
            <link href="assets/css/stylesheet.css" rel="stylesheet" type="text/css" />
        </head>
        <body>
            ...
            <script type="text/javascript" src="assets/js/javascript.js"></script>
        </body>
    </html>
    

    Если результирующие файлы уже были созданы, то они не перезаписываются. Это поведение можно изменить через свойство ttl (см.ниже).

    CSS / JS файлы на выходе мимими минимизируются. По умолчанию, для Javascript используется Dean Edwards' JavaScriptPacker, для CSS — Joe Scylla' CssMin. Они дают весьма неплохой результат, но в перспективе можно будет подключить и другие минимизаторы. Также, можно отключить минимизацию совсем — результатом будут скомбинированные, но не сжатые файлы.

    Smarty


    Bender включает плагин для Smarty. Для этого просто нужно поместить function.bender.php в директорию плагинов Smarty, не забыв заинклюдить сам класс Bender где-нибудь в скриптах инициализации проекта. Использование Bender в шаблоне Smarty:

    <html lang="en">
        <head>
            {bender src="assets/css/bootstrap.css"}
            {bender src="assets/css/bootstrap-theme.css"}
            {bender src="assets/js/jquery-1.10.2.js"}
            {bender src="assets/js/bootstrap.js"}
            {bender output="cache/stylesheet.css"}
        </head>
        <body>
            ...
            {bender output="cache/javascript.js"}
        </body>
    </html>
    

    Свойства


    У Bender пока что минимум свойств. Вот они:

    $bender->cssmin: может принимать значения "cssmin" для использования минимизатора CssMin, любое другое значение отключит минимизацию CSS.
    $bender->jsmin: "packer" для использования JavaScriptPacker, "jshrink" для JShrink, или любое другое значение отключит минимизацию JS.
    $bender->ttl: время жизни скомбинированных файлов. По умолчанию это 3600 секунд, по истечении которых, результирующие файлы будут перезаписаны. 0 - всегда перезаписывать, -1 - никогда не перезаписывать.
    

    В планах


    • Плагины для Twig и Wordpress
    • Добавить параметры для enqueue и output
    • Свойство $dev — будет отдавать файлы «as is», без комбинирования — для режима отладки

    Скачать Bender с GitHub | Инструкции по использованию на английском

    Буду рад предложениям по улучшению, отзывам и даже критике в комментариях. Я думаю вы догадались, что название Bender не имеет ничего общего с Футурамой — оно происходит от слова «комбинатор».
    Поделиться публикацией

    Комментарии 28

      +1
      Советую вам посмотреть на assetic. Из плюсов — оттестирован, совместим с композером, мощнее, много предустановленных фильтров
        –2
        assetic очень мощная вещь, я использовал его в одном из проектов. Однако делать свой велосипед причина всё же была: нужен был простой комбинатор, желательно в одном файле, ну максимум — в 2-3, который можно быстро подключить к существующему проекту, с минимальными изменениями в последнем.
        0
        Ещё один велосипед.

        Чем он отличается от других? В Symfony, есть очень крутая штука, я просто оставлю ссылку:
        symfony.com/doc/master/cookbook/assetic/asset_management.html

        $bender->enqueue("assets/css/bootstrap.css");
                $bender->enqueue("assets/css/bootstrap-theme.css");
                $bender->enqueue("assets/js/jquery-1.10.2.js");
                $bender->enqueue("assets/js/bootstrap.js");
        

        Это нынче не круто. Куда лучше было-бы что-то в этом роде $bender->enqueue("assets/css/*")->enqueue("assets/js/*");
          0
          Такой вариант тоже рассматривался, но есть одна проблема. Дело в том, что в проекте скопилось большое количество JS и CSS файлов, которые нужны не всегда, либо вообще устарели и не используются. Выискивать все такие файлы — это можно убить несколько дней. А если подключать их все по маске, то результирующие файлы будут содержать больше мусора, чем полезного. Ну и размер у них будет в несколько мегабайт.
            0
            В этом, как мне кажется, и заключается суть asset managera. Что бы умно управлять ассетами, жать что надо и когда надо. Если в проекте очень много файлов которые старые, или вообще не используются, а выискивать их трудно — это проблема. Возможно ли реализовать такую фичу, скажем как u уникальный id? подключаешь файлики — вторым параметром тыкаешь версию, или какое-то id.
            $bender->enqueue("assets/js/*", REVISION_ID);
            

            А потом дописать несколько методов, скажем, detachRevision(), removeRevision etc. Пусть это и говнокод, но я считаю что удобно было бы пользоваться такой системой. Все-равно ненужные файлы лучше удалять вообще, ИМХО.
            0
            Это не было бы лучше. В крайнем случае можно сказать, что это было бы «модно».
            А лучше была бы возможность просто один раз передать список.
              0
              Такая возможность есть:

              $bender->enqueue( array( "assets/css/bootstrap.css", "assets/js/bootstrap.js", "assets/css/bootstrap-theme.css", "assets/js/jquery-1.10.2.js" ) );
              
                0
                Согласен. С точки зрения «глобальности» — все верно. Это будет красиво, но не производительно. Но автор же делаете очередь с каких-то файлов, верно? Я просто за то что бы вызвать метод 1 раз, а не 10.
                  0
                  Но вы же вызвали его такое же количество раз, хотя и в меньшее количество строк.
                    0
                    Разве? Я вызвал метод всего два разы. Один для всех javascript файлов и один для всех css файлов. Все это заняло одну строчку. А не 4. На каждый файл — вызов метода.
                      0
                      Ну вот не надо гнать на библиотеку за кол-во кода который надо написать чтобы подключить зависимости) В твиге родная функция assetic-а ни разу не удобная, приходится писать аж 3 строки, чтобы подключить файл применив к нему фильтр.

                      Просто на мой взгляд адаптировать assetic под проект было бы выгоднее и быстрее, чем писать это) Учитывая наличие плагина под смарти github.com/pjparra/assetic-smarty
                        0
                        Изначально было задумано, что все CSS и Javascript будут добавляться одним списком (см. мой комментарий выше). А возможность добавлять по одному файлу была внесена для того, чтобы можно было просто заменять

                        <script type="text/javascript" src="js/jquery.js">
                        на
                        $bender->enqueue("js/jquery.js");

                        так как часто в существующих проектах JS / CSS файлы часто бывают не просто разбросаны в разных местах одной страницы, но и по разным инклюдам. Всевозможные аддоны также подгружают свои скрипты и стили. Так что комбинатор как раз и задумывался для наиболее лёгкой интеграции в уже готовый проект.
                +1
                А вам интересно развивать проект?)
                Я не помню где, но видел идею, или реализацию. В общем смысл такой, пишем в шаблонизаторе
                {{ asset('css://vendor/bootstrap/bootstrap.less') }}
                {{ asset('css://vendor/some_lib/lib.styl') }}
                

                Ну и так далее) Смысл в том, что выходной формат определяется схемой пути
                  0
                  Конечно, интересно — и я надеюсь, что проект будет развиваться. Главное, чтобы он продолжал соответствовать основным требованиям:

                  • Простая и быстрая интеграция в любой готовый проект, с минимальными изменениями последнего
                  • Минимальный размер кода и количество файлов
                  • Универсальность и независимость от технологий и фреймворков — чтобы работал на любом хостинге, даже самом ограниченном
                  +2
                  Что сподвигло вас использовать глобальные переменные в коде?

                  global $_javascripts, $_stylesheets;
                  

                  Почему не в static или обычном свойстве хранить список ресурсов?
                    –2
                    Я знал, что рано или поздно это кто-нибудь заметит. Не люблю глобальные переменные не меньше, чем любой другой разработчик. Но engueue() может вызываться из разных экземпляров Bender (например, в плагинах), чтобы не хранить глобальный экземпляр всего класса и не париться, был он создан или нет. Просто создаём его заново в функции плагина, и добавляем файлы в очередь.
                      +2
                      В таком случае можно использовать статические свойства.
                        –1
                        Ну это осталось с тех времён, когда Bender был просто функцией (плагином к Smarty), а не классом — в следующей ревизии будет исправлено.
                      0
                      Там автор еще переменные инициализирует через var и вместо __construct() у него там название класса :)
                      Не в обиду конечно, но возникает только один вопрос… Зачем?!

                      … а я то правда надеялся что php4 не увижу больше…
                        –1
                        Дело привычки, так сказать — скорее всего, в следующей ревизии это будет заменено, чтобы не мозолило глаза. Дело в том, что конструкторы вида ClassName() и __construct() являются полными синонимами (просто __construct() появился в PHP 5), но я так и не нашёл ни одного весомого аргумента, почему нельзя использовать ClassName(), а нужно использовать только __construct(). Есть один аргумент — например, вы захотите в дальнейшем переименовать класс, и чтобы не забыть переименовать конструктор. Но мне он кажется недостаточно весомым :) Аналогично, var $foo и public $foo являются синонимами.
                          0
                          Мне не надо это объяснять. Я разбираюсь что и где появилось и что и как объявляется. Дело привычки, или устаревший хлам? Весомый аргумент в том, что стабильная версия php 5.5, и пора ее использовать. А не писать в стиле php4, да и классы так называть еще…
                            0
                            Даже не в этом дело… Там была вообще смесь двух версий… Это привычка версии смешивать такая? :)

                            P.S Еще совет: Не засоряйте файл ненужными комментариями, например такими:
                            // Constructor
                            // Get root dir
                            // Get extension in lowercase

                            Там итак понятно что делают функции. Эти комментарии — мусор. А если уж и комментируете функции, то комментируйте их через block comments. Прокомментируйте что принимает, что возвращает итп.
                        0
                        На мой взгляд, втыкать ассеты непосредственно в шаблон — это только для мелких проектов годится. Для проектов более-менее серьезных — не подходит. Потому что там требуется более гибкое управление — включение/отключение механизма сборки, включение/отключение механизма компрессии, создание нескольких наборов, добавление/удаление файлов в/из набора и т.д.
                          0
                          Согласен с предыдущим комментатором. Вещи связанные со сборкой проекта должны быть вынесены из кода проекта в отдельную утилиту, осуществляющую эту сборку. Мы в свое время для подготовки js и css к публикации использовали (и сейчас используем) минификатор от Microsoft, он был сделан ими в свободное время и для себя (наверное потому и работает хорошо). Его достаточно поставить на компьютер, осуществляющий сборку продакшн версии проекта, а затем подцепить к утилите сборки. Конечно машина разработчика должна работать под Windows, но большинство все-равно ее используют.
                            0
                            Мы пробовали разные минификаторы, в том числе и Microsoft Ajax Minifier. В своих проектах у нас используется Minify. Но помимо своих и крупных клиентских проектов, у нас есть ещё много мелких, написанных на чём угодно (WordPress, CS-Cart и куча других CMS), и нужно максимально быстро, и по возможности — с минимальным вмешательством в код проекта, минимизировать их. Раньше приходилось тратить очень много времени, «отлавливая», где же в скриптах, шаблонах и плагинах подключаются какие JS и CSS. Вот для этого и был написан Bender.
                              0
                              Насколько я понимаю по приведенному примеру, вы все-равно вынуждены ставить в очередь все файлы подлежащие минификации, а для этого вы их должны знать.
                              Есть еще одна причина, по которой стоит понимать что где подключается — периодические конфликты между разными частями кода js. Они, как правило, возникают из-за пересечения одноименных глобальных переменных. Если отдать сборку полностью на откуп утилите, без четко выстроенного процесса сборки, то есть вероятность возникновения ошибок js в самых неожиданных местах.

                              Вынос операции минификации во вне также полезен с точки зрения кроссплатформенности решения. Мы, например, имеем отношение проектов на php к проектам на .NET равное 30% к 70%. Поэтому для нас актуален единый процесс сборки для всех проектов.

                              В любом случае, лишний велосипед кому-нибудь пригодится, а если он (Bender) будет доступен сообществу, я уверен, он найдет применение во многих проектах.
                                0
                                Спасибо за положительный отзыв. Да, уже нашлись люди, которым этот проект понравился. Я только что сделал очередной коммит, который содержит некоторые оптимизации и улучшения (буду стараться описывать все изменения в Changelog секции README на гитхабе).
                            0
                            Правильно ли я понимаю, что если на одной странице подключается 5 js-файлов, а на другой — эти же 5 и еще 1,
                            то бендер, сгенерирует для каждой страницы свои минифицированные версии, хотя, по факту, различия только в одном файле?

                            И вместо того, чтобы на второй странице браузеру забрать эти 5 файлов (или один минифицированный) из кэша и 1 маленький из сети, он будет тянуть один новый большой, 90% которого, по сути, и так уже есть в кэше?

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

                            Самое читаемое