Собрание ваших сочинений на 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, плюс добавил возможность асинхронной работы. На мой взгляд апи должно быть понятным, но если кому-то вдруг понадобиться и возникнут вопросы, пожалуйста, можете обращаться в личку.На этом все.