Система сборки фронтенда в CleverStyle Framework или почему вам может быть не нужна кастомная

    CleverStyle Framework всячески помогает разработчику не только на сервере, но и на фронтенде. Я об этом несколько раз упоминал в прошлых статьях, но никогда не вдавался в подробности того, как именно всё устроено под капотом.


    Данная статья будет погружением в подробности работы со статикой для фронтенда, начиная от того как определяются файлы нужные на странице и заканчивая оптимизациями доставки статики вроде HTTP/2 Server Push. Не забудем и о том, почему с использованием CleverStyle Framework можно обойтись без кастомной системы сборки и как при желании интегрировать такую систему сборки в процессы фреймворка.


    Данная статья специально упускает из внимания интеграцию Bower/NPM и RequireJS, это будет тема отдельной статьи в недалеком будущем.


    Что подключать


    Первая задача — определить какие именно файлы (CSS/JS/HTML) нужны на конкретной странице, и дзесь есть две условные группы:


    • общие файлы для всех страниц сайта
    • файлы, которые специфичны для данной страницы (или для нескольких страниц)

    Общие файлы


    Общие файлы в свою очередь так же можно разделить на три подгруппы.


    В первую подгруппу попадают файлы ядра системы из директории assets. Там находится тот базовый минимум служебных файлов, которые нужны самому фреймворку (на самом деле это можно обойти, но об этом дальше). Файлы данной подгруппы загружаются в первую очередь.


    Во вторую подгруппу попадают файлы текущей темы оформления интерфейса themes/тема/{css|js|html}, подключаются сразу после файлов ядра.


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


    Файлы для конкретных страниц


    Конкретные страницы генерируются установленными модулями.


    В типичном сценарии при установке модуля Module_name он будет обрабатывать страницы, начинающиеся с /Module_name. Для того, чтобы указать, что на страницах этого модуля должны использоваться конкретные файлы составляется карта таких файлов ассоциированных с путями в файле с мета-информацией модуля.


    Пример для модуля Blogs:


    {
        "package"      : "Blogs",
        "category"     : "modules",
    ...
        "assets"       : {
            "admin/Blogs" : [
                "cs-blogs-admin-*"
            ],
            "Blogs"       : [
                "cs-blogs-*"
            ]
        },
    ...
    }

    В модуле Blogs нет CSS и JS файлов для подключения на страницу, только веб-компоненты (которые в свою очередь могут содержать CSS/JS, но это уже другая история, о которой чуть дальше).


    В примере выше мы видим, что на всех страницах модуля будут подключены файлы, пути которых начинаются с префикса cs-blogs- (звёздочка отбрасывается и поддерживается исключительно в целях улучшения читабельности), а файлы с префиксом cs-blogs-admin- будут подключаться только на страницах администрирования (этот ключ в списке идет первым, так что сначала отфильтруются файлы с данным префиксом, а остальные попадут в следующий ключ, не смотря на одинаковое начало).


    На счёт путей нужно уточнить что они отсчитываются от следующих корней: modules/Modules_name/assets/{css|js|html}, из формата файла ясно в которой директории он находится, то есть в modules/Modules_name/assets/html будет производиться поиск только HTML файлов, но не CSS и/или JS.


    Прямые зависимости


    Файлы самого модуля это только часть истории, важным звеном в данной системе являются зависимости между модулями.


    Посмотрим на модуль Photo gallery для примера и одну из его зависимостей, модуль Uploader:


    {
        "package"          : "Photo_gallery",
        "category"         : "modules",
    ...
        "assets"           : {
            "admin/Photo_gallery" : "admin.css",
            "Photo_gallery"       : "general.*"
        },
    ...
        "provide"          : "photo_gallery",
        "require"          : [
            "System>=6.25",
            "System<7.0",
            "Composer",
            "Fotorama>=4.4.9",
            "file_upload",
            "composer_assets"
        ],
    ...
    }

    {
        "package"      : "Uploader",
        "category"     : "modules",
    ...
        "assets"       : {
            "admin/Uploader" : "admin.css",
            "Uploader"       : "script.js"
        },
    ...
        "provide"      : "file_upload",
    ...
    }

    Здесь мы видим что модуть Photo gallery имеет прямую зависимость от file_upload. file_upload в свою очередь является не названием модуля, а его функциональностью (подробнее об этом в документации). С точки зрения обработки статики это значит, что файлы предназначенные для модуля Uploader (для модуля в целом, не для подстраниц) будут так же подключены на страницах модуля Photo gallery, при чём перед собственными файлами этого модуля (это важно если зависимость имеет некоторый синхронный код, используемые по зависимости дальше).


    Нужно заметить, что не только обязательные (ключ require), но и опциональные (ключ optional) зависимости учитываются в данном сценарии.


    Обратные зависимости


    Для фронтенда так же важной концепцией являются обратные зависимости. Это такие зависимости, которые указывает не целевой модуль, а сторонний модуль самостоятельно. Вместе с возможностью переопределять веб-компоненты альтернативными реализациями это достаточно мощный инструмент. По сути это предоставляет возможность сказать фреймворку, что вот этот модуль предоставляет дополнительную функциональность для другого модуля (к примеру, меняет облик блога без необходимости редактировать сам модуль блога). Это достаточно тонкий момент, им не стоит злоупотреблять, но иногда бывает полезным.


    Вложенные зависимости


    Зависимости могут иметь собственные зависимости, в том числе зависимости могут повторяться. Фреймворк это понимает и превращает дерево зависимостей в плоскую структуру с учетом этой особенности (не пытайтесь создавать циклические зависимости, результат будет неопределённым).


    Важно заметить, что зависимость от версии системы (модуль System) не влияет на цикличность зависимостей, эффективно можете считать что она будет проигнорирована во время обработки статики.


    Интеграция в процесс сбора файлов для подключения


    Во время сборки файлов для подключения система генерирует событие System/Page/assets_dependencies_and_map, подписавшись на которое имеется возможность манипулировать собранными системой файлами а так же построенной структурой зависимостей.


    В данную структуру можно добавлять собственные файлы, организовывать их в такие себе виртуальные модули и достраивать зависимости. Таким образом модуль Composer assets интегрирует подключение файлов из Bower/NPM пакетов во фреймворк чтобы воспользоваться его внутренними механизмами для обработки файлов (о которых немного дальше).


    Собственно подключение файлов на страницу


    Фреймворк не всегда просто подключает все нужные для конкретной страницы файлы, а поступает более продвинутым образом в зависимости от конфигурации системы.


    На то, как подключать файлы влияют следующие параметры:


    • кэширование и сжатие
    • оптимизация загрузки фронтенда (зависит от предыдущего параметра)
    • вулканизация (также зависит от первого параметра)
    • перемещение JavaScript и HTML за тело страницы
    • отключение поддержки веб-компонентов

    Перемещение JavaScript и HTML за тело страницы


    Это самое простое, JS/HTML файлы будут подключены перед </body> вместо помещения в <head>, в общем рекомендуется к использованию.


    CSS специально подключается в <head> всегда, поскольку базовая стилизация обычно небольшая и задержка в рендеринге странице получается не существеенная (особенно с использованием HTTP/2 и Server Push).


    Отключение поддержки веб-компонентов


    Тоже достаточно простая настройка, которая, впрочем, работает только для кастомных тем (чтобы не сломать админку с темой оформления по-умолчанию). Приводит к полной фильтрации любых HTML файлов, так же отключает загрузку полифиллов для веб-компонентов и JS файлов из assets/Polymer, что в целом отключает всё что связано с веб-компонентами во фреймворке если оно вам не нужно. Важно понимать, что многие готовые модули перестанут работать, поскольку их интерфейс построен целиком на веб-компонентах.


    Кэширование и сжатие


    Во-первых, фреймворк берет все собранные файлы для подключения на страницах с учётом зависимостей и пакует файлы в кэш. Для общих файлов создается тройка кэшированных файлов с форматами css/js/html, аналогичные файлы создаются для каждого ключа assets в каждом установленном модуле + отдельно создаются кэши языковых переводов интерфейса + отдельно кэшируется полифилл для веб-компонентов и сохраняется новая структура зависимостей с указанием уже на данные собранные кэшированные файлы.


    Во-вторых файлы не просто попадают в кэш напрямую — этому предшествует некоторая обработка. Фреймворк не пытается хватать звёзд с неба в этом вопросе, зато работает исключительно быстро.


    Обработка CSS заключается в:


    • удалении лишних пробелов и переводов строк
    • оптимизации некоторых конструкций вроде сокращения цветовых HEX значений с 6 до 3 символов и конвертации RGB цветов в HEX
    • встраивании небольших изображений и подобных файлов (до 4 КиБ) в base64 виде, не встроенные файлы записываются в отдельный список и будут использованы позже
    • корректировке относительных путей с учетом нового целевого размещения кэшированного CSS

    Обработка JS гораздо более скудная, поскольку тащить полноценный минификатор желания нет, а иначе легко сломать код простым поиском и заменой, так что ограничиваемся следующим:


    • удаление гарантировано безопасных переводов строк
    • удаление однострочных комментариев, которые начинаются с начала строки
    • удаление многострочных комментариев, которые начинаются с начала строки
    • звмена </script> на <\/script>, поскольку данный JS код может впоследствии быть использован для вставки в HTML
    • игнорируются многострочные шаблоны из ES2015 (начиная от первого и заканчивая последним — всё внутри исключается из обработки)

    Данные минификации CSS/JS достаточно базовые, но позволяют за десяток миллисекунд перебрать весь проект, в то время как полноценные минификаторы требуют несколько секунд и с Gzip дают выигрыш всего лишь до 5% (данные минификаторы вы можете запускать поверх построенного кэша).


    HTML обрабатывается ещё меньше — по сути весь JS код собирается вместе и прогоняется через упомянутый минификатор, CSS для каждого веб-компонента так же прогоняется через упомянутый минификатор, во избежания проблем вырезается <link rel="import" href="../polymer/polymer.html"> из HTML.


    После построения кэша генерируется событие System/Page/rebuild_cache — кастомные минификаторы и прочие вещи вроде построения кастомного кэша можно производить в обработчике этого события.


    В структуре кэша каждый файл сопровождается хэшем в части параметров (примеры ниже в части про HTTP/2 Server Push). Хэш считается от содержимого файла, так что при изменении одного файла и перестроения кэша изменится хэш соответствующего кэшированного файла, но не всего кэша сразу.


    Почитать подробнее о том, какую структуру имеют вспомогательные JSON файлы с описанием структуры кэша, зависимостями и прочим, можно в документации.


    Вулканизация


    Вулканизация это процесс обработки HTML файла, при котором CSS/JS код встраивается в итоговый HTML. Для CSS это не имеет никакого значения, поскольку Polymer пока не поддерживает CSP-совместимые конструкции во всех ситуациях, так что CSS всегда встраивается в итоговый HTML.


    Реализация во фреймворке собственная, в целом об этом можно почитать в соответствующем проекте на GitHub.


    Оптимизация загрузки фронтенда


    Это интересный механизм, призвание которого ускорить изначальную отрисовку страницы.


    Как было описано выше есть общий CSS/JS/HTML код и специфический для отдельных страниц. Так вот этот общий код можно представить себе как App shell. Оптимизация загрузки фронтенда заключается в том, что изначально на странице подключается только общий JS/HTML, и только после того как он был загружен и отработан начинается асинхронная загрузка и последовательное выполнение остальной части JS/HTML кода (модули дожны быть готовы к тому, что HTML код, который будет общим для всех страниц, может выполниться раньше чем JS код зависимостей данного модуля).


    Данный подход позволяет значительно ускорить первую отрисовку страницы и таким образом дать пользователю если не интерактивную страницу, то что-то полезное и максимально быстро.


    Для оптимизированной загрузки строится отдельный файл с дополнительной структурой оптимизированного кэша.


    HTTP/2 Server Push


    При включенном кэшировании нужные странице файлы будут сопровождены соответствующими 'Link:` заголовками, что прокси-сервера (вроде CloudFlare, nghttpx) обычно превращают в Server Push, пример заголовков:


    Link:</storage/public_cache/CleverStyle:TinyMCE.html?602ca>; rel=preload; as=document
    Link:</storage/public_cache/CleverStyle:System.css?fecf7>; rel=preload; as=style
    Link:</storage/public_cache/CleverStyle:Uploader.js?2167a>; rel=preload; as=script
    Link:</storage/public_cache/CleverStyle:System.html?14dd7>; rel=preload; as=document
    Link:</storage/public_cache/CleverStyle:System.js?a0e9d>; rel=preload; as=script
    Link:</storage/public_cache/CleverStyle:Static_pages.html?a292d>; rel=preload; as=document

    Ещё интереснее становится если к этому добавить оптимизацию загрузки фронтенда — тогда Server Push будет применяться только для тех файлов, которые подключаются изначально, пример той же страницы что и выше:


    Link:</storage/public_cache/CleverStyle:System.html?14dd7>; rel=preload; as=document
    Link:</storage/public_cache/CleverStyle:System.js?a0e9d>; rel=preload; as=script
    Link:</storage/public_cache/CleverStyle:System.css?fecf7>; rel=preload; as=style

    В Server Push так же попадут файлы, которые не были встроены в CSS из-за их размера, но которые нужны для отрисовки страницы (подробнее в документации).


    Ну и, конечно же, Server Push срабатывает только однажды, после чего выставляется cookie во избежание деградации производительности.


    Почему вам может быть не нужна кастомная система сборки фронтенда


    Если у вас процесс разработки похож на мой: LiveScript->JavaScript, SCSS->CSS, Jade->HTML — всё с помощью File Watchers автоматически генерируется при каждом изменении и целиком попадает в Git, то вы можете обойтись без кастомной системы сборки. Любое изменение любого файла приводит к генерации соответствующего артефакта и пока вы переключаетесь в браузер чтобы нажать F5 (без включенного кэширования во фреймворке) у вас всё готово к работе.


    Что если у вас есть кастомная система сборки, вы хотите добавить кастомную систему сборки или заменить то, что делает фреймворк полностью?


    Ваши лучшие друзья это два системых события:


    • System/Page/assets_dependencies_and_map
    • System/Page/rebuild_cache

    С их помощью вы можете интегрироваться в работу фреймворка и пользоваться всеми упомянутыми преимуществами имея/используя дополнительные артефакты/инструменты так, как вам удобно.
    Также в документации описана структура кэша, так что вы можете подготовить артефакты вашей системы сборки в таком формате, в каком это будет понятно фреймворку и таким образом не использовать систему сборки фреймворка, но скармливать ему собственные артефакты в съедобном для него формате.


    Напоследок


    Всегда рад новым идеям и конструктивным комментариям.
    » Репозиторий на GitHub

    Поделиться публикацией
    Похожие публикации
    Ой, у вас баннер убежал!

    Ну. И что?
    Реклама
    Комментарии 13
      0
      мне больше нравится подход как здесь — https://laravel.com/docs/5.3/elixir
      И как это совместимо будет с подходом
      TypeScript->JavaScript, Less->CSS, SCSS->CSS?
        0

        Я с Elixir не работал, но исходя из того, что это надстройка над Gulp — его можно использовать и с CleverStyle Framework без особых сложностей. Можно либо складывать артефакты туда, откуда их автоматически подхватит фреймворк, либо написать кастомную небольшую интеграцию с помощью обработчиков упомянутых в статье событий.


        Я пытался сделать работу со статикой максимально декларативной, то есть вы указываете какой файл (или группу файлов, которая в продакшене конкатенируется в один файл) нужен на какой странице, а обо всём остальном, включая версии и зависимости побеспокоится фреймворк. То есть вам никогда не нужно писать нечто подобное:


        <link rel="stylesheet" href="{{ elixir('css/all.css') }}">
        <script src="{{ elixir('js/app.js') }}"></script>
          0
          мне кажется что
          <link rel="stylesheet" href="{{ elixir('css/all.css') }}">
          <script src="{{ elixir('js/app.js') }}"></script>
          

          удобнее, чем
          "assets"       : {
                  "admin/Uploader" : "admin.css",
                  "Uploader"       : "script.js"
              },
          

          в куче модулей.
          Даже написание задачи gulp для нужных файлов все равно выйдет короче. Я б на вашем месте сделал gulpfile.js с заданием для скриптов/стилей модулей, которые у вас в комплекте идут, может кому понадобится
            0

            А как там с зависимостями, нужно в повторять подключение множества файлов раз за разом?
            Вот есть у меня один модуль, в котором нужно загружать котиков, и второй модуль, который непосредственно реализует загрузку файлов в общем виде. В каждом из модулей есть набор CSS/JS/HTML файлов, но перед тем как грузить котиков мне нужен сам функционал загрузки.


            В моем случае это будут два модуля, каждый из который имеет в простейшем виде следующее:


            {
                "package" : "Kitty",
                "require" : "Uploader",
                "assets" : {"Kitty": "*"},
            ...
            }

            {
                "package" : "Uploader",
                "assets" : {"Uploader": "*"}
            ...
            }

            gulpfile.js не подходит по идеологическим причинам — CleverStyle Framework не требует на сервере ничего кроме PHP, к тому же все модули опциональные, они в репозитории, но не обязательно в комплекте.


            Использование только PHP позволяет делать удобные интеграции всего вместе в том числе с клиентского интерфейса, когда нет доступа к CLI в принципе. Например, установка Bower/NPM пакетов с помощью Composer плагина прямо из админки с интерактивным прогрессом, хотя для самого Composer в обычном режиме нужен доступ к CLI, а плагин должен быть установлен глобально.

              0
              А как там с зависимостями, нужно в повторять подключение множества файлов раз за разом?

              такое разруливать самому, но случай какой-то странный.
              Например, установка Bower/NPM пакетов с помощью Composer плагина прямо из админки с интерактивным прогрессом

              это конечно круто, но я б отнес к разряду «свистелко-перделок».
              Если нет ноды на серваке — то закидывать в cvs скомпилленные ассеты и не придется ничего запускать.
              Даже если не используется никакая csv — по фтп закинуть скомпилленные файлы будет проще.
              Вот есть у меня один модуль, в котором нужно загружать котиков, и второй модуль, который непосредственно реализует загрузку файлов в общем виде.

              мы все еще про зависимости фронтэнда или про модули в вашей системе? Если модули в вашей системе — то что-то тут нечисто
                0
                мы все еще про зависимости фронтэнда или про модули в вашей системе? Если модули в вашей системе — то что-то тут нечисто

                Зависимости между модулями используются и для бекенда, и для фронтенда, так что, полагаю, ответ зависимости фронтенда. В примере выше я имел ввиду именно его.

                  0
                  если про фронтэнд — то ничего такого нет в этом — зависимость для модуля так же включаем в gulp-задачу, а на выходе будут склеенные скрипты.
                  Самый простой случай — разные модули зависят от чего-то одного — например, от загрузчика в админке, для которого свой кусок собирается, и на публичной части сайта — +1 строка в каждую секцию, не особо критично и просто
                  а если у вас такое в бекэнде модуля — то что-то у вас нечисто
                    0

                    А если зависимость необязательная? К примеру, WYSIWYG редактор — на базовую функциональность не влияет, может как быть, так и отсутствовать. Криво как-то вписывать это вручную в gulp-задачу, да ещё с проверками на наличие файлов.


                    В том-то и преимущество в CleverStyle Framework — вы не прописываете вручную ничего для использования. Разработчик модуля объявляет всё декларативно во время разработки, после чего достаточно установить модуль, и на основании декларативной конфигурации нужное странице будет подключено и закэшировано как положено.

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

                        Советов несколько:


                        • делите на самостоятельные куски, чтобы подключать только нужное
                        • если это JS — выделяйте в AMD модуль и грузите только его (об этом будет отдельный пост)
                        • если первые два варианта не подходят — есть вероятность, что лишнего кода не так много и с этим можно жить

                        Если совсем кастомная ситуация — вы всегда можете дополнить встроенную функциональность фреймворка и интегрировать туда в том числе gulp, если контролируете окружение. Фреймворк не покрывает 100% ситуаций самостоятельно, но оставляет возможности интеграции для покрытия 100% ситуаций при необходимости. Нужно следить за балансом предоставляемой из коробки функциональности.


                        Вдруг я захочу вместо скрипта от модуля использовать свой?

                        Вот здесь не совсем понятно что имеется ввиду. Модуль это самостоятельная единица, перезаписывать отдельные файлы тоже в теории можно (подписаться на события установки/обновления модулей и перезаписывать нужный файл поверх, либо подписаться на упомянутое в статье событие и редактировать путь к исходному файлу в маппинге ресурсов), но это уже как-то совсем дико. Какой сценарий имеется ввиду?

                          0
                          редактировать путь к исходному файлу в маппинге ресурсов

                          это и имеется в виду. Вдруг разработчик модуля слегка криворукий, а модуль все же полезный, мало ли что может быть. Пока работал с вордпрессом — такое бывало.
                          А так в gulpfile переписал путь и все, исходный файл не тронут
                          За советы спасибо, но я наверно не так объяснил что имел в виду — как раз визуальный редактор к примеру не нужен в публичной части сайта, а в админке норм
        0

        webpack + npm… не надо писать свои менеджеры модулей на php.

          0

          Bower/NPM используются и так, а вот Webpack зачем? Node.js на сервере может не быть, работает он медленнее, для интеграции в проект (таким образом, как это описано в статье) всё равно придется написать кучу кода. А так никаких императивных конфигураций не нужно и всё шустро работает. А если Webpack для чего-то нужен, то его можно несколькими строчками интегрировать как готовый сторонний сервис вместо того, чтобы требовать на уровне фреймворка.

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

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