Пользователь
0,0
рейтинг
20 марта 2014 в 12:11

Разработка → AngularJS — разделение приложения на модули и загрузка компонентов с помощью RequireJS из песочницы

Использование AngularJS в паре с RequireJS — достаточно популярный подход к разработке веб приложений в последнее время. И один из основных вопросов — структура приложения. Существует достаточно известный seed для такого приложения tnajdek/angular-requirejs-seed, но мне это не походит, так как при увеличении функционала приложения — данная структура просто будет засоряться кучей файлов, не будет никакого логического разделения скриптов и достаточно сложно будет их менеджить.

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

Модуль


В данном случае, это логически отдельная часть приложения, включающая в себя набор компонентов:
  • ngModule;
  • Controller;
  • FIlter;
  • Directive;
  • Service;
  • Template;
  • Configs — содержат config() и run() методы для текущего ngModule.




Проблема


При использовании RequrieJS, файлы приложения чаще всего подключаются как-то так:

require('modules/foo/controller/foo-controller.js');
require('modules/foo/service/foo-service.js');
require('modules/foo/directive/foo-controller.js');
require('text!modules/foo/templates/foo.html');
require('modules/bar/directive/bar-controller.js');


Здесь есть явные минусы:
  • Код очень зависит от структуры проекта;
  • Код очень зависит от названий модулей;
  • Достаточно много нужно писать руками.


Решение


Были написаны RequireJS плагины для загрузки компонентов модуля.

К примру есть такая структура приложения (кстати, очень похожая на структуру бандлов в Symfony2):
app
   |-modules
   |       |-menu
   |       |    |-controller
   |       |    |          |-menu-controller.js
   |       |    |-menu.js    
   |       |    
   |       |-user
   |             |-controllers
   |             |           |-profile.js
   |             |-resources
   |             |           |-configs
   |             |           |        |-main.js
   |             |           |
   |             |           |-templates
   |             |           |          |-user-profile.html
   |             |           |-directives
   |             |                      |-user-menu 
   |             |                                 |-user-menu.js
   |             |                                 |-user-menu.html
   |             |-src
   |             |      |-providers
   |             |      |          |-profile-information.js 
   |             |      |-factory
   |             |              |-guest.js
   |             |-user.js
   |
   |-application.js
   |-boot.js


В данном случае у нас есть 2 модуля: user и menu. Файлы /app/modules/menu/menu.js и /app/modules/user/user.js — скрипты с инициализацией angularJS модулей. Все остальное — думаю понятно.

Теперь нужно задать конфигурацию для подключения всех компонентов. Делается это с помощью requirejs.config:

requirejs.config({
  baseUrl: '/application',
  paths: {
    'text': '../bower_components/requirejs-text/text',

    // Structure plugins:   
    'base': '../bower_components/requirejs-angular-loader/src/base',
    'template': '../bower_components/requirejs-angular-loader/src/template',
    'controller': '../bower_components/requirejs-angular-loader/src/controller',
    'service': '../bower_components/requirejs-angular-loader/src/service',
    'module': '../bower_components/requirejs-angular-loader/src/module',
    'config': '../bower_components/requirejs-angular-loader/src/config',
    'directive': '../bower_components/requirejs-angular-loader/src/directive',
    'filter': '../bower_components/requirejs-angular-loader/src/filter'
  },
  structure: {
      prefix: 'modules/{module}',

      module: {
        path: '/{module}'
      },      
      template: {
        path: '/resources/views/{template}',
      },

      controller: {
        path: '/controllers/{controller}'
      },

      service: {
        path: '/src/{service}'
      },

      config: {
        path: '/resources/configs/{config}'
      },

      directive: {
        path: '/resources/directives/{directive}/{directive}'
      },

      filter: {
        path: '/resources/filters/{filter}'
      }
    }
});


Все пути каждого компонента определены в рамках модуля. Поле structure.prefix — путь к корню модуля, после baseUrl.

Теперь, если мы хотим подключить файл /app/modules/user/user.js из:
1. /app.js:
require('module!user')


2. /app/modules/user/controllers/profile.js:
require('module!@')

В рамках одного модуля — имя модуля можно не писать, достаточно символа '@'. Тем самым, если придется переименовать модуль — не нужно будет менять код.

Теперь, если мы хотим подключить файл /app/modules/user/controllers/profile.js из:
1. /app.js:
require('controller!user:profile')

До двоеточия — название модуля, после двоеточия — название контроллера.

2. /app/modules/user/user.js:
require('controller!profile')

В рамках одного модуля — имя модуля можно не писать, достаточно указать только название контроллера. Так же, если контроллер лежит на уровень ниже, то возможно подключать так:
require('controller!additional/path/to/profile')


Точно так же и для всех других компонентов.

Результат


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

Так же не возникает никаких проблем при сброке проекта. В тестовом приложение есть пример собранного проекта в папке /build и Gruntfile для сборки, но в нем нету ничего не обычного.

Ссылки:


Данный подход используем в большом корпоративном приложении, поддержка и развитие данного подхода будет поддерживаться и развиваться.
@tuchk4
карма
11,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

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

  • 0
    Просто оставлю это тут github.com/marcoslin/angularAMD
    • 0
      Да только смысла особого в этой прослойке нет, и без нее получается не больше кода и при этом сохраняется исходный API самого Angular.
    • +4
      Это же совершенно не то, о чем статья. Очень сильная зависимость кода от структуры проекта. angularAMD — ленивая загрузка модулей приложения, где модуль — это контроллер, в котором описаны все зависимости. Реализован как обертка над angular, что не очень хорошо.
      В статье описан способ как сделать структуру приложения гибче, и уменьшить зависимость кода от структуры приложения и названия модулей.
      Тем более плагины, о которых написано в статье могут использоваться в паре с angularAMD. Получится ленивая загрузка + гибкость структуры и уменьшение кода в описании зависимостей.
  • +4
    А какой вообще смысл от использования RequireJS если у вас уже есть AngularJS? В нем уже реализованы модули, и при использовании AngularJS вы от них никуда не денетесь. Вот и получается лишняя обертка для каждого скрипта. А что касается асихронной загрузки, то когда дело дойдет до продакшена вы все равно будете исользовать RequireJS Optimizer чтобы получить один js файл.
    • +1
      Когда продакшн — да, проект собирается. А во время разработки очень удобно хранить все в отдельный файлах, грубо говоря — один класс = один файл. Тем более загрузка файлов в с помощью RequireJS — на много удобнее, чем менеджить все нужные скрипты в head теге. Так ведь будет всего лишь один подключенный скрипт, а все другие зависимости будут указаны уже внутри скриптов.

      Так ведь нету никаких противоречий с модулями AngularJS. Каждый модуль (как на диаграмме в начале статьи) — и есть AngularJS модуль. Суть в том, что он находится в отдельной директории вместе со всеми своими компонентами. А плагины для RequireJS упрощают загрузку модулей, компонентов, уменьшают зависимость кода от структуры проекта и названия модулей, и структура становится более гибкой.
      • +2
        А во время разработки очень удобно хранить все в отдельный файлах, грубо говоря — один класс = один файл.

        Само собой, и Вам ничего не мешает так же делать при помощи AngularJS.

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

        Вы явно забыли про конфиг для RequireJS, в не работы не намного меньше.

        Так ведь нету никаких противоречий с модулями AngularJS.

        Никто не говори про противоречия, всего лишь лишний код, в который нужно теперь обарачивать каждое объявление любой сущности AngularJS, которые и так принадлежат какому-либо модулю AngularJS.
  • 0
    Само собой, и Вам ничего не мешает так же делать при помощи AngularJS.

    Каждый файл и содержит какой либо из компонентов ангулара. Вас смущает то, что он обернут в define функцию?

    Вы явно забыли про конфиг для RequireJS, в не работы не намного меньше.

    В котором чаще всего описываются пути для вендоров. Пути для юзер сркиптов — очень редко.

    Это же на на много удобнее, писать в коде require('relative/path/to/script'); чем писать все это в head теге, да еще и учитывать последовательность подключения.

    К примеру у нас приложение есть несколько модулей. Каждый модуль — отдельная часть приложения: projects, emails, advertising и так далее. И уже в самом модуле описаны все зависимости. В RequireJS есть два способ описания зависимостей — передавать параметры в define функцию, или просто реквайрить скрипты внутри функции, в таком случае это будет похоже на CommonJS. И так явно удобнее, чем пихать все head или через менеджить через какие либо утилиты бекенда.

    И я еще раз говорю, модуль — логически отдельная часть, которая содержит ngModule и его компоненты — контрллеры, директивы, фильтры, сервисы и так далее. Плагины для RequireJS позволяют максимально отвязать код, от структуры проекта и возможность переносить один компонент из одного модуля в другой без особых изменений в коде (но такие сутации достаточно редкие).
    • 0
      Вы путаете систему модулей и ленивую их подрузку. Это все же разные функции. Что бы не париться о том, в каком порядке подключать файлы, у все уже в самом angular есть ленивое подключение модулей. Поэтому настроить сборку проекта даже при условии что у вас даже фильтры каждый будет в своем файле не так уж и проблематично. Опять же можно автоматизировать все настолько, что сборщик будет сам определять зависимости модулей, выставлять все в нужном порядке и собирать. Причем для разработки можно пересобирать только измеенный модуль и использовать кэш. Хотя на самом деле даже если вы будете собирать все просто конкатенацией всех js файлов в один, то тоже ничего не поломается.

      Лично я считаю использование require.js в angular-based проектах избыточным. А по поводу ленивой подгрузки модулей, эта фича обещает быть реализованной в 2-ой версии angular (хотя договориться как и зачем пока не могут) и будет использовать систему модулей из ES6. Для ветки же 1.1 же ленивая подгрузка бесполезна, так как на момент запуска приложение оно должно быть полностью загружено.
      • 0
        А по поводу ленивой подгрузки модулей, эта фича обещает быть реализованной в 2-ой версии angular

        Да, вот как раз пост. Там есть куча ссылок на интересные гугл доки.

        We’ll see a less complex DI by eliminating the config phase, simplifying the syntax by using declarative-style ES6+ annotations instead of imperative. We’ll enjoy greater capabilities by integrating DI with module loading through ES6 Modules. We’ll also see the ability to lazily-load parts of our JS through child injectors.

        Вот ссылка на пример.
      • 0
        Вы путаете систему модулей и ленивую их подрузку

        Путаница скорее произошла в том, что ленивая подгрузка != ленивая инициализация.

        Лично я не вижу ничего избыточного в использовании AngularJS в паре с RequireJS. И скорее всего, это тема для отдельного хабра топика и дискасса под ним.

        А по поводу ленивой подгрузки модулей, эта фича обещает быть реализованной в 2-ой версии angular

        И скорее всего, будет не совместимо с AngularJS v1.*
      • 0
        Часто в проекте не весь код для Angular. Могут быть какие-то модели, дополнительные сервисы и просто код, который необходимо использовать в приложении, но при этом код не должен быть зависим от Angular, а должен оставаться переносимым. В стратегической перспективе — это большое преимущество. В общем случае, require.js отвечает за внедрение каких-то общих, независимых от Angular, сервисов, а также «классов» и управление зависимостями на самом общем уровне, а Angular DI отвечает за внедрение «объектов», «экземпляров».

        Использование require.js (ну и вообще, JS-модулей, будь то AMD, LMD, CommonJS modules, ES6 Harmony modules) и необходимость в нем никак не меняется с использованием модулей Angular.
  • 0
    Как я понял все модули подключаются сразу? То есть нет возможности в зависимости от роута грузить определенные директивы, фильтры, сервисы?
    • 0
      Да, здесь нету ленивой загрузки. Но она и не нужна, тем более в продакшене.
      Во время разработки — все грузится сразу. С этим работают только разработчики.
      В продакшене — весь проект минифицируется. Это в любом случае будет лучше.
      • 0
        Делал для себя небольшой загрузчик для angular с помощью requirejs. Он как раз работает на основе роутов. Надо попробоать сжать с помощью RequireJS Optimizer и посмотреть как он будет работать)

        Но код для подключения зависимостей примерно так выглядит:

        .state('docs',
              lazyLoadProvider.lazy({
                url: '/',
                templateUrl: '/core/views/docs_list.html',
                controller:  'controllers/DocsListCtrl',
                dependencies: [
                  'directives/sidebar/sidebar',
                  'directives/sidebar/sidebar-scroll-wrapper',
                  'directives/breadcrumb/breadcrumbs',
                  'directives/add-form/add-form',
        
                  'services/sidebarListService'
                ]
              }))
        


        На выходе он генерирует обычный для ngRoute, uiRoute — объект, с методом resolve в котором и происходит разрешение зависимостей, и пока они не разрешены, роут не срабатывает.

        Правда есть минус, что приходится описывать все зависимости в параметре dependencies и нельзя сделать:

        
        define(['myDirective', 'myService'], ....)
        
        • 0
          Второй раз промахнулся веткой. Хотя точно нажимал «ответить». Ответил Вам ниже.
      • 0
        Ну иногда, в больших приложениях, целые модули подгружаются в момент необходимости — таким образом, после оптимизации получается не один, а несколько файлов, представляющих целые куски системы. Но этот сценарий, скорее, исключение, большинство приложений — это просто один файл и все тут.
  • 0
    Надо попробоать сжать с помощью RequireJS Optimizer

    Скорее всего ен выйдет. Оптимайзер ищет строки:
    require('/path/to/script');
    

    И подключает их в проект.
    Вот разница наглядно:
    var scripts =[
      'modules/foo/foo',
      'modules/bar/bar',
      'modules/application/application'
    ];
    
    for (var i =0 ; i < scripts.length; i++){
      require(scripts[i]);
    }
    

    И
    require('modules/foo/foo');
    require('modules/bar/bar');
    require('modules/application/application');
    


    А у Вас как раз зависимости описаны как первом примере.
    • 0
      Может получится если их все прописать в paths

      paths: {
          sidebar: 'directives/sidebar/sidebar',
          sidebarScrollWrapper: 'directives/sidebar/sidebar-scroll-wrapper',
          breadcrumbs: 'directives/breadcrumb/breadcrumbs',
          add-form: 'directives/add-form/add-form',
      }
      
      • 0
        Там есть то ли баг, то ли фича, но когда прописываешь компоненты в paths, и внутри этого компонента рекйварить другой компонент — но не будет работать относительный путь. Точнее будет, но считаться он будет от файла, где описан конфиг с paths.
        • 0
          Ох, благодаря этой фиче немало матов было во время первых внедрений require.js :)
  • +2
    Я дико извиняюсь, ну просто совсем мне не понятно для чего же нужно такое.

    Допустим я разрабатываю приложение, состоящие из одного модуля. Я вполне себе не привязан к структуре директорий, так как DI в ангуляре по алиасам. В index.dev.html мне совсем не сложно добавить новый script, а в прод уйдет собранная и сжатая версия.

    Допустим я разрабатываю многомодульное приложение. В принципе ситуация абсолютно такая же. Для модулей в ангуляре есть DI.

    Неужели RequireJS стоит использовать только, чтобы не впихивать при разработке каждый раз новое вхождение script. Все же для меня RequireJS — это впервую очередь разруливание зависимостей, а уже только потом тулза, которая освобождает меня от захламления условного index.dev.html

    Другое дело Backbone. Там наверняка must have.

    Заранее спасибо тому, кто разъяснит :)
    • 0
      Для Backbone есть Marionette или Chaplin, каждый из которых имеет свою модульную систему. Думаю, не стоит путать модули конкретного приложения и модули JS в целом. Смысл есть для более-менее сложных проектов, при небольшом количестве зависимостей или когда приложение — всего один модуль — выигрыша либо не будет, либо будет незаметным. С другой стороны, привыкнув просто весь код аккуратно заворачивать в модули — обратно уже не тянет, код воспринимается более структурированным и контролируемым.
  • 0
    В добавок к предыдущим комментаторам, предлагаю взглянуть на Yeoman, а именно на его генератор для AngularJS, который в том числе умеет без проблем за вас вставлять вхождение нового скрипта в index.html. При этом, когда выполняется `grunt build`, все автоматически в 1 файл обфусцируется, конкатенируется и т.д.

    Просто попробуйте. Использовали RequireJS на проекте с Backbone.js, здесь с AngularJS + Yeoman + Bower + Grunt это действительно не дает никакого выигрыша.

    За статью спасибо.
  • 0
    С недавних пор использую browserify. Пишу весь клиентский код в стиле nodejs с его require-ами, а затем browserify пакует все это в один файл. Для dev режима есть поддержка sourcemaps. Таким образом одним пакетным менеджером (npm) убиваем двух зайцев.
    • +1
      У RequireJS один из способов подключения файлов как раз очень похож на CommonJS. Для поиска всех require-ов используется Function.prototype.toString(). То есть перед вызовом функции define — все зависимоти будут подключены. Этот способ на много приятнее, чем описывать все зависимости первым аргументом в define функции.
      • 0
        Плюс 1 за такой подход. Используем исключительно такой метод — на дистанции поддерживать куда проще и исключаются баги с неверной последовательностью параметров в модуле.
    • 0
      Ну тут не суть важно, что использовать. Суть в том, что JS-модули в целом (AMD/CommonJS/ES6) вполне сочетаются с ng-модулями.

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