Оптимизация модулей RequireJS в Symfony2

  • Tutorial
О пользе модульного подхода в программировании на любом языке говорилось уже достаточно много, по-этому приведу кратко основные положительные моменты для JS особо не вдаваясь в подробности. Разделение приложения на модули позволяет сделать код:
  • значительно более читабельным и прозрачным для понимания
  • гораздо более простым в поддержке
  • гибким и расширяемым
  • пригодным для написания достаточно больших приложений
  • легко тестируемым и более простым в отладке

Так же документация модульного кода гораздо более эффективна.

Модульность, наряду с фреймворками, помогает вывести яваскрипт на качественно новый уровень и позволяет ему перейти из ранга вспомогательных и дополнительных в ранг серьезных самостоятельных языков программирования.

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

Существуют хорошо зарекомендовавшие себя библиотеки способные решить большинство поставленных задач и избежать отрицательных моментов. Одной из наиболее популярных библиотек для написания модульных приложений на яваскрипт является RequireJS. RequireJS хорошо документирована и касаться разработки с ее использованием в этой статье мы не будем. Рассмотрим подробнее как интегрировать RequireJS в Symfony2 с последующей оптимизацией созданных нами модулей. Для подобной цели очень кстати может оказаться HearsayRequireJSBundle, так что, какая проблема, берем бандл инсталлируем через компоузер и все! Возможно у кого-то так и получилось, однако могли возникнуть и некоторые нюансы. Чтобы максимально сгладить процесс знакомства с подобной интеграцией предлагаю прочесть то, что изложено ниже.

Установка бандла дело нехитрое:
в composer.json пропишем

"hearsay/require-js-bundle": "2.0.*@dev"

Дальше, как предлагают в документации:

$ php composer.phar update hearsay/require-js-bundle

В AppKernel добавим:

new Hearsay\RequireJSBundle\HearsayRequireJSBundle(),

Пытаемся почистить кэш — выясняется, что конфигурация не отвечает требованиям.
Во-первых, необходима установленная библиотека node.js (если нет) не беда:

sudo apt-get update; sudo apt-get install nodejs

Неплохо было бы иметь и саму Requirejs:

sudo npm install requirejs

— таким образом мы установим ее локально в папку проекта и будем соответственно использовать локальные пути к исполнимому файлу, можно, естественно и глобально с ключом «-g». Наш путь окажется такими:

%kernel.root_dir%/../node_modules/requirejs/require.js
%kernel.root_dir%/../node_modules/requirejs/bin/r.js

— соответственно к require.js и к r.js. Именно r.js и есть та самая либа, которая поможет нам из кучи различных AMD JS модулей нашего проекта создать один, может не такой красивый, но очень оптимизированный файл для продакшена.

Теперь давайте попробуем сконфигурировать наш бандл:

# app/config/config.yml
hearsay_require_js:
    require_js_src:  //cdnjs.cloudflare.com/ajax/libs/require.js/2.1.14/require.min.js
    initialize_template: HearsayRequireJSBundle::initialize.html.twig
    #папка в которую будут собираться наши, не полностью обработанные,  скрипты в «дев» версии в каталоге «web».
    base_url:  js
    #путь к папке со скриптами в бандле
    base_dir: %kernel.root_dir%/../src/Acme/DemoBundle/Resources/assets/js # Required
    #пути к основным модулям и скриптам для requirejs.config()
    #для внешних библиотек не забываем указывать «external: true»
    paths:
        main:
            location: @AcmeDemoBundle/Resources/assets/js/main
        jquery:
            location: //ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min 
            external: true
        underscore:
            location: //cdnjs.cloudflare.com/ajax/libs/underscore.js/1.6.0/underscore-min
            external: true
        backbone:
            location: //cdnjs.cloudflare.com/ajax/libs/backbone.js/1.1.2/backbone-min
            external: true
        text:
            location: @AcmeDemoBundle/Resources/assets/js/vendor/text
        bootstrap:
            location: //maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min
            external: true
    #конфиг  shim соответственно
    shim:
        bootstrap:
            deps: [jquery]
    #конфигурация оптимизатора, в нашем случае r.js
    optimizer:
        path: %kernel.root_dir%/../node_modules/requirejs/bin/r.js
        #для продакшена прячем все не оптимизированные файлы для папки web
        hide_unoptimized_assets: true
        #настройки оптимизатора (r.js можно посмотреть здесь https://github.com/jrburke/r.js/blob/master/build/example.build.js, например)
        options:
            removeCombined: true
            name: main
            #воспользуемся библиотекой uglify2 для оптимизации, она входит в стандартную сборку r.js
            optimize: uglify2
            #настройки для  uglify2
            uglify2:
                output:
                    beautify: false
                compress:
                    sequences: true
                    global_defs:
                        DEBUG: false
                warnings: true
                mangle: false

Где-нибудь в layout.twig вызов наших яваскриптов может выглядеть таким образом:

{% javascripts filter='requirejs'
    '@AcmeDemoBundle/Resources/assets/js/main.js'
%}
    {{ require_js_initialize({ 'main' : asset_url }) }}
{% endjavascripts %}

Если хотим, чтобы наши скрипты оптимизировались только на продакшене, а в «дев» версии были видны не оптимизированными, в определение фильтра добавим знак вопроса {% javascripts filter='?requirejs'… А в файле app/config/config_dev.yml, пропишем:
hearsay_require_js:
    optimizer:
        hide_unoptimized_assets: false

И, напоследок, раз уж заговорили про оптимизацию скриптов, оптимизация для css. Воспользуемся библиотекой uglifycss. Установка так же достаточно простая, как и с предыдущим модулем, из корневой директории сайта:

sudo npm install uglifycss

После успешной установки исполняемый файл найдем по адресу:

%kernel.root_dir%/../node_modules/uglifycss/uglifycss

В конфигурации объявим фильтр:

# app/config/config.yml
assetic:
    filters:
        uglifycss:
            bin: %kernel.root_dir%/../node_modules/uglifycss/uglifycss

Созданный таким образом фильтр можно использовать в twig, например, так:

{% stylesheets '@AcmeDemoBundle/Resources/assets/css/*' filter='?uglifycss' filter='cssrewrite' %}
        <link rel="stylesheet" href="{{ asset_url }}"/>
{% endstylesheets %}

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

//src/Acme/DemoBundle/Resources/assets/js/
bundles/
| --    user/
|       | --    collections/
|               | --    users.js
|       | --    models/
|               | --    user.js
|       | --    templates/
|               | --    user.html
|               | --    users.html
|       | --    main.js
vendor/
| --    text.js
app.js
main.js
router.js

//src/Acme/DemoBundle/Resources/assets/css/
style.css
user.css

В продакшен версии в папке «web» после выполнения команды

app/console assetic:dump --env=prod

появятся папки js/ и css/ и выглядеть они будут, как-то так:

web/
| --    css/
|        | --    ecde8f1.css
| --    js/
|        | --    b50c14d.js 


Вторая часть статьи: Тестирование модулей RequireJS в Symfony2

P.S.. В нашей школе вот-вот стартует пятимесячный курс обучения от автора статьи «Хочу стать Junior PHP Developer!» и «Symfony 2. Гибкая разработка». Чтобы записаться пишите на info@digitov.com

P.P.S. Чтобы получать наши новые статьи раньше других или просто не пропустить новые публикации — подписывайтесь на нас в Facebook, VK и Twitter.

Авторы:
Сергей Харланчук, Senior PHP Developer, Компания «SECL GROUP» / «Internet Sales Technologies»
Никита Семенов, президент, Компания «SECL GROUP» / «Internet Sales Technologies»
SECL Group
47,00
Делаем стартапы успешными!
Поделиться публикацией

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

    0
    Вот теперь я видел всё.
      0
      То то еще будет! У нас тут один сотрудник пилит искусственный интеллект ;)
        0
        Для сборки js-очек? Да, это будет похлеще. Вообще соль в том что если у вас есть необходимость собирать сложный фронтэнд. то можно смело выкидывать assetic и заменять на нормальные системы сборки. Ассетика хватает только на что-то простое. В целом же бесполезная штука. Есть правда еще альтернативы, типа gasettic и ему подобные, но я пока не баловался.
          0
          Возможно Вы в чем-то правы, есть более продвинутые системы сборки и выбор определенного варианта обычно зависит от конкретного случая. В статье описан один из таких вариантов сборки и он вполне, на мой взгляд, имеет право на существование.
          В статье, главным образом, рассматривается не assetic, как таковой, а интеграция requirejs и r.js с symfony2.
            0
            Честно говоря, я поддерживаю идею Fesor с тем, что сложный фронт должен отделяться от бэка. В конечном счёте, гораздо проще заточить бэк предоставления API к SPA-like архитектуре на фронте. Конечно, можно пользоваться и вашей схемой, но я не вижу в этом выигрыша.
              0
              Я понимаю, но вы бы статью тогда сдобрили какими-нибудь кукбуками, как это все удобнее поддерживать потом… Ибо так это просто чуть расширеное ридми бандла. Я использовал его как-то раз года полтора назад, но мне показалось это не столь удобно.
                0
                Статья и не претендует на всеобъемлющий туториал — просто информация к размышлению.
              0
              И еще — использовались ли всякие bower (Да, есть для symfony и с bower интеграция), покрывался ли js код тестами и т.д. Как в этом случае вы бы организовывали проект?
                0
                Bower мы не использовали — не было необходимости.
                  0
                  Тестирование однозначно необходимо, хотя это и не совсем относится к теме статьи, добавить тестовое окружение к проекту не состовляет большого труда.
                    0
                    Сборка у вас завязана на php уже, а как вы тесты будете запускать к примеру?
                      0
                      Конечно Вы правы, есть некоторые особенности у этого процесса, и это, скорее тема для следующей статьи. Постараюсь это осветить в ближайшем будущем.
          +1
          Честно говоря, не пользовался Require.js, потому простите мне мой скептицизм. Я вполне доволен assetic, в связке с yuicompressor

          привожу конфиг
          assetic:
              debug:          %kernel.debug%
              use_controller: false
              bundles:        [FMElfinderBundle,CMSBundle,FOSCommentBundle]
              filters:
                  compass: 
                      bin: /usr/local/bin/compass
                      apply_to: "\.s[ac]ss$"
                  sass:
                      bin: /usr/local/bin/sass
                  scss: ~
                  cssrewrite: ~
                  uglifycss: ~
                  uglifyjs2: ~
                  yui_css:
                      jar: %kernel.root_dir%/../vendor/packagist/yuicompressor-bin/bin/yuicompressor.jar
                      apply_to: "\.css$"
                  yui_js:
                      jar: %kernel.root_dir%/../vendor/packagist/yuicompressor-bin/bin/yuicompressor.jar
                      apply_to: "\.js$"
          
          и
          шаблон
                  {% stylesheets
          		"@CMSBundle/Resources/assets/css/*"
          		"@BraincraftedBootstrapBundle/Resources/sass/form.scss"
          		"css/compiled/*"
          	%}
          		<link rel="stylesheet" href="{{ asset_url }}" />
          	{% endstylesheets %}
                  {% javascripts
          		'@CMSBundle/Resources/public/js/*'
          		'@CMSBundle/Resources/public/original_js/*'
          	%}
          		<script type="text/javascript" src="{{ asset_url }}"></script>
          	{% endjavascripts %}
          

          Результат тот же, а промежуточных действий — меньше (установить yuicompressor, и добавить пару строк в конфиг). Наверное, я что-то упускаю, не могли бы вы привести какие-нибудь конкретные аргументы в пользу вашего предложения?
            0
            RequireJS — это не компилятор и не минификатор. Это удобная штука для разруливания зависимостей в JS (что-то типа use и DI). Оптимизатор r.js всего лишь позволяет собирать всё в один файл с учётом зависимостей и минифицировать этот файл.

            Имхо, вместо всяких ассетиков проще на девелопменте использовать неминифицированный код, а на продакшн (стейджинг и т.п.) деплоить чем-то вроде Grunt. Я, например, использую Grunt с тремя модулями — grunt-contrib-less, grunt-contrib-watch и grunt-contrib-requirejs. Задача local (она же является default-задачей) состоит из grunt-contrib-watch, подключается неминифицированная версию JS-фреймворка. Задача deploy состоит из grunt-contrib-less и grunt-contrib-requirejs, подключается минифицированный склеенный файл с JS-фреймворком. В идеале хорошо иметь отдельные склеенные файлы для каждой страницы сайта (контроллеры с их зависимостями).

            В итоге у меня для деплоя используется Capistrano (с модулем Capifony — набором задач для деплоя проекта на Symfony 2), который на завершающих шагах (перед перенаправлением симлинка на новый релиз) вызывает Grunt для компиляции JS и LESS.
            0
            Есть 2 вопроса по вашей конфигурации:

            1. При такой конфигурации (используя filter="?requirejs") в получившемся минифицированном файле у меня в prod режиме падает ошибка: ReferenceError: jQuery is not defined
            Зависимость от jquery везде указана, вот конфиг:
            конфиг
            hearsay_require_js:
                require_js_src: //cdnjs.cloudflare.com/ajax/libs/require.js/2.1.14/require.min.js
                initialize_template: HearsayRequireJSBundle::initialize.html.twig
                base_url: js
                base_dir: %kernel.root_dir%/scripts
                optimizer:
                    path: %kernel.root_dir%/scripts/r.js
                    hide_unoptimized_assets: false
                paths:
                    # RequireJS plugins
                    text: %kernel.root_dir%/scripts/requirejs/plugins/text
            
                    # Application scripts
                    app: @AppBundle/Resources/public/js/app
                    user: @AppBundle/Resources/public/js
                    platform: @AppBundle/Resources/public/js
            
                    # Other libs and plugins
                    underscore: %kernel.root_dir%/scripts/underscore
                    chosen: %kernel.root_dir%/scripts/jquery/plugins/chosen/chosen.jquery.min
            
                    # Externals
                    jquery:
                        location: //ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min
                        external: true
                    bootstrap:
                        location: //maxcdn.bootstrapcdn.com/bootstrap/3.3.1/js/bootstrap.min
                        external: true
                options:
                    locale: %locale%
                    enforceDefine: true
                    i18n:
                        locale: %locale%
                shim:
                    bootstrap:
                        deps: [jquery]
                        exports: jQuery
                    underscore:
                        exports: _
                    chosen:
                        deps: [jquery]
                        exports: 'jQuery.fn.chosen'
            



            2. Не менее важный вопрос: а как вы подключаете в шаблонах файлы, которые нужны только на этой странице? Конкретно код интересует.
            Я попробовал подключить через
            {% block javascripts %}
            <script>
                    require(['user/login/helper'], function (LoginHelper) {
                        LoginHelper.init();
                    });
                </script>
            {% endblock%}
            


            но при этом они уже не минифицируются, а загружаются отдельным http запросом.

              0
              Первая проблема частична решается путём переноса jquery в локальный файл. Остается такая проблема
              Error: No define call for /js/0d5e2b3.js для файла requirejs.min.js
              По ссылке requirejs.org/docs/errors.html#nodefine ни одна причина не подходит на мой взгляд. При этом dev режим работает отлично
              +1
              habrahabr.ru/company/SECL_GROUP/blog/259901 — продолжение єтой статьи, Тестирование модулей RequireJS в Symfony2

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

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