Собрание ваших сочинений на Angular.js

    Данный пост будет посвящён вопросу сборки Angularjs приложений. Я рассмотрю возможные пути решения и объясню, почему в итоге решил написать несколько собственных плагинов.

    Итак, а как вообще принятно решать проблему сборки в последнее время? Grunt/Gulp плагины, require.js, browserify — самые популярные варианты.

    Но есть нюанс. Когда вы имеете дело с Angular-приложением, вы сталкиваетесь с необходимостью декларирования зависимостей между модулями для их правильной сборки. Поясню на примере.

    Если у вас простейшее приложение, которое состоит из нескольких файлов:

    app.js
    controllers.js
    services.js
    directives.js
    filters.js
    

    То никаких проблем нет. Вы можете вручную прописать порядок их подключения в том же Grunt/Gulp.

    Но, допустим, если вы захотите реализовать такую архитектуру, при которой каждый отдельный сервис, контроллер, директива и т. д. находятся в отдельных файлах, то есть:

    app.js
    controllers/
    	FirstCtrl.js
    	SecondCtrl.js
    services/
    	FirstSrv.js
    	SecondSrv.js
    

    То вы столкнётесь с рядом трудностей. Главным образом, трудности будут с тем, как сшить все файлы так, чтобы при этом Angular работала без ошибок. Итак, что можно сделать?


    Основная проблема в том, что при объявлении контроллера, сервиса, директивы и т. д. сперва должен быть задан модуль, которому они принадлежат.

    angular.module('App').controller('FirstCtrl',function($scope){...});
    

    В данном случае, чтобы создать контроллер FirstCtrl необходимо, чтобы сперва был подключен файл, в котором объявляется модуль App:

    angular.module('App',[]);
    


    А вообще, по-хорошему, из соображений удобства тестирования компоненты модуля должны собираться в отдельные модули:

    angular.module('App',['App.controllers','App.services','App.directives','App.filters']);
    

    А это значит, что, если вы по-прежнему хотите хранить все по отдельности, то нужно создавать промежуточные файлы с объявлением этих модулей:

    app.js
    controllers/
    	module.js
    	FirstCtrl.js
    	SecondCtrl.js
    services/
    	module.js
    	FirstSrv.js
    	SecondSrv.js
    

    где файлы module.js содержат соответственно:

    angular.module('App.controllers',[]);
    

    и

    angular.module('App.services',[]);
    


    А дальше ведь могут быть ещи и самостоятельные модули со своей структурой.

    Таким образом решить проблему голым concat не получится (из комментариев). Но это можно сделать с помощью библиотек Require.js и Browserify.

    Пример того, как выглядит такое приложение, написанное с применением Require.js, здесь. С одной стороны, явным плюсом подхода является — гибкость, которую он дает при проектировании. Однако неизбежно код мусорится AMD обертками и множественными require().

    И здесь чуть более выигрышно смотрится browserify со своим CommonJs. Но CommonJs на то он и CommonJs, что в случае с браузерными AMD библиотеками у вас возникнут сложности при настройке (обсуждение подхода в комментариях). Но и опять же ручное объявление зависимостей с помощью require никто не отменяет.

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

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

    angular.module('App','/controllers.js','/services.js')
    


    после обработки получится следующее

    angular.module('App.controllers',[]);//Содержимое controllers.js
    angular.module('App.services',[]);//Содержимое services.js
    angular.module('App',['App.controllers','App.services']);//Вместо путей будут подставлены названия модулей, которые находятся в файлах
    


    В приниципе, остальные примеры и возможности описаны на github, поэтому не буду на этом останавливаться. Тем более, что минус такого подхода очевиден — необходимо прямое вмешательство в родной синтаксис Angular.js

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

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

    При этом внутри проекта вы можете раскладывать модули и компоненты, как вам угодно, и при этом нет никаких лишних вкраплений в коде.

    Пример задачи для Gulp:

    gulp.task('concat',function(){
        gulp.src('/**/*.js')
            .pipe(concat('app.js'))
            .pipe(gulp.dest('./build/'));
    });
    


    и для Grunt:

    grunt.initConfig({
    
            concat: {
                default_options: {
                    files: {
                        'build/app.js': 'test/src/complex/**/*.js'
                    }
                }
            }
    
        });
    


    Пока что не хватает поддержки sourcemap, есть большое желание свести в одну библиотеку возможность добавление аннотаций и сборки шаблонов. Над чем я и буду работать в ближайшее время.

    В качестве итога еще раз пробегусь по возможным вариантам:
    простой concat — сложно реализовать,
    Require.js — стабильная библиотека, но требуется дополнительный код,
    Browserify — кода чуть меньше, чем в Require.js, но возникает проблема интеграции AMD модулей,
    NgBuild — по сути тот же Require/Browserify, но библиотека еще не обросла пользователями, поэтому возможны подводные камни и баги,
    NgConcat – проще и удобнее всех остальных, но пока что то же молода и может ломаться, поэтому пока, что я бы не рекомендовал брать на вооружение в крупный проект, либо брать, но иметь запасной вариант и в случае чего жаловаться на Github, автор обещает своих не бросать и безжалостно карать любые баги.

    P.S. При написании библиотек пришлось так же реализовать модуль для работы с синтаксическим деревомAstra. API которого я постарался сделать более удобным, чем у Astral, плюс добавил возможность асинхронной работы. На мой взгляд апи должно быть понятным, но если кому-то вдруг понадобиться и возникнут вопросы, пожалуйста, можете обращаться в личку.
    На этом все.
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

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

      0
      yeoman.io/ + github.com/yeoman/generator-angular

      Уже все придумали
        0
        Если точнее, в генераторе есть grunt-ngmin, а grunt-ngmin использует ngmin, который есть и под gulp.
          +2
          А что для сборки-то придумали? Это генератор с возможностью автоматичкого подключения файлов в html-страницу.
            0
            Он генерит неведомо что. В сгенерированном grunt-файле не рабоатет часть тасков. Например, cdnify. Переводить всё это дело на gulp — боль отдельная.
            0
            я использую обыкновенный build.json с таким содержимым:
            {
               "application":"App",
               "controllers":["Controller1","Controller2"],
               "modules":["Module1","Module2",..],
               ...
            }
            

            а потом собираю по нему php скриптом. Мартышкин труд вроде, а с другой стороны один раз написал и забыл…
              +3
              Если бы я был Кипеловым, то посвятил бы вам песню «Беспечный ангел», настолько я восхищаюсь вашей аутентичностью.
                0
                Мы применили аналогичный подход, только собираем не php скриптом, а задачей на grunt.
                +3
                А в чем проблема сразу написать, скажем, bootrstrap.js:

                angular.module('App.controllers',[]); 
                angular.module('App.services',[]);
                angular.module('App.myModule',[]);
                angular.module('App',['App.controllers','App.services', 'App.myModule']);
                

                И подключать его первым?
                  0
                  Потому что в этому случае, не нужно будет писать дополнительную библиотеку. Очевидно же.
                    0
                    Не вижу причин, почему бы не делать так, как вы предлагаете. Единственное, если у вас модули регулярно гуляют между проектами, то, наверное, будет удобнее хранить их объявление вместе с остальным кодом, но это, конечно, слабый аргумент. Поэтому добавил в пост ссылку на комментарий.
                      0
                      А где гарантия того, что файл bootrstrap.js будет исполнен до какого либо контроллера?
                        0
                        И подключать его первым
                          0
                          Не заметил. В статье автор рассматривает такую проблему
                          Основная проблема в том, что при объявлении контроллера, сервиса, директивы и т. д. сперва должен быть задан модуль, которому они принадлежат.

                          Ваш способ отлично решает эту проблему. Но это не единственная проблема при определении порядка загрузки/объединения скриптов. Допустим, есть файл router.js, в котором прописаны роутинги. Ему для работы нужны некие провайдеры. Нужно чтобы провайдеры были загружены раньше роутера.
                          Как я понимаю эту проблему не решает предложенный в статье NgConcat.
                            0
                            Если производить настройку провайдеров в файле объявления модуля, решает.
                              0
                              Тогда хотелось бы более подробной документации для NgConcat на гитхабе
                      0
                      Использую с browserify, полёт нормальный. На предмет возможных косяков проанализировал, не смог придумать.
                      Можно использовать обычный require модуля, можно использовать совместно с ангуларовским DI.

                      Есть презентация на эту тему:
                      benclinkinbeard.com/talks/2014/ng-conf/
                      benclinkinbeard.com/talks/2014/ng-conf/#/20 — на 20-ом слайде конкретный пример.
                        0
                        Вы browserify с shim используете?
                          0
                          Нет.
                            0
                            А как вы сторонние библиотеки подключаете?
                              0
                              Делаю require. Как правило, всё, что мне нужно, есть в npm, и оно работает через browserify без напильника (почти всегда). Однажды у меня была проблема с tls, но я его просто проигнорировал в browserify, и пакет прекрасно работал в браузере. Иногда приходится использовать не модуль, а его dist-версию, но редко.
                              О каких проблемах вы говорите?
                                0
                                Я обычно использую bower для фронтенда и здесь возникает проблема, что большинство библиотек AMD, а не CommonJs. И даже, если в файле сборки прописать порядок подключения этих файлов, browserify может собрать их произвольно, поэтому приходится отдельно указывать зависимости в shim.
                                  0
                                  Да, это проблема. Тут нужно выбирать, если bower, то bower. Я как-то пробовал совмещать их, потом понял, что могу целиком перейти на CommonJS-модули. bower, как инфраструктура, мне симпатичен, но я уже не могу отказаться от плюсов npm-инфраструктуры.
                        0
                        Почему-то никто не прокомментировал про структуру проекта.

                        app.js
                        controllers/
                            FirstCtrl.js
                            SecondCtrl.js
                        services/
                            FirstSrv.js
                            SecondSrv.js
                        


                        Окей, хорошо, если вы делаете калькулятор. При условии, что вы пишете production code для большого проекта с парой тысячей контроллеров, вы просто утонете во всем этом. На хабре был разбор полетов на эту тему, главным смыслом которого была группировка не по типу, как в angular seed (контроллеры/директивы/сервисы), а по entities. Например:

                        app.js 
                        user
                        - UserModule.js
                        - UserCtrl.js
                        - AuthCtrl.js
                        - UserService.js
                        dashboard
                        - DashboardModule.js
                        - DashboardCtrl.js
                        - DashboardDirectiveThatShowsNeatCats.js ;)
                        


                        Соответственно, проблемы как собрать собрать подобный проект без всяких примочек, у нас не возникало.
                          0
                          А дальше ведь могут быть ещи и самостоятельные модули со своей структурой.


                          Эх, чуть-чуть не дочитали
                            0
                            Дочитали:) Я просто не понял, зачем все эти решения, если делать сразу правильную структуру проекта на вырост, Он и так неплохо собирается
                              0
                              А как вы собираете?
                            +1
                            А почему, скажем, ваш раздел user содержит только один контроллер? Если их будет больше, то вы получите все те проблемы, которые описаны в статье.

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

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