Использование RequireJS в приложениях AngularJS

Автор оригинала: Ravi Kiran
  • Перевод
  • Tutorial
При написании больших JavaScript-приложений одна из самых простых вещей, которую можно сделать, это разделить код на несколько файлов. Это улучшает поддерживаемость кода, но увеличивает шансы потерять или ошибиться со вставкой тега script в главный HTML-документ. Отслеживание зависимостей затрудняется с ростом числа файлов проекта. Эта проблема присутствует в больших AngularJS приложениях до сих пор. У нас есть целый ряд инструментов, которые заботятся о загрузке зависимостей в приложении.

В этой статье мы рассмотрим, использование RequireJS с AngularJS для упрощения загрузки зависимостей. Мы также рассмотрим, как использовать Grunt для генерации файлов, содержащих модули RequireJS.

Краткое введение в RequireJS


RequireJS это JavaScript-библиотека, которая помогает в «ленивой загрузке» JavaScript-зависимостей. Модули являются обычными JavaScript-файлами с некоторым количеством «синтаксического сахара» RequireJS. RequireJS реализует Asynchronous Module Definition, специфицированное в CommonJS. RequireJS предлагает простой API для создания модулей и обращения к ним.

Для RequireJS требуется главный файл, содержащий базовые конфигурационные данные, такие как пути к модулям и «прокладкам». Следующий фрагмент показывает каркас файла main.js:

require.config({
  map:{
    // Maps
  },
  paths:{
    // Алиасы и пути модулей
  },
  shim:{
    // Модули и их зависимости
  }
});


Нет нужды задавать все модули приложения в секции paths. Они могут быть загружены с использованием относительных путей. Для объявления модуля мы должны использовать блок define().

define([
  // Зависимости
], function(
  // Объекты зависимостей
){
 
  function myModule() {
    // Может использовать объекты зависимостей, полученные выше
  }
 
  return myModule;
});


Модуль может не иметь каких-либо зависимостей. Обычно в конце модуля возвращается объект, но это не обязательно.

Внедрение зависимостей в AngularJS против управления зависимостями в RequireJS


Один из распространенных вопросов, который я слышу от AngularJS-разработчиков, касается разницы в управлении зависимостями в AngularJS и RequireJS. Здесь важно вспомнить, что назначение обеих библиотек совершенно разное. Встроенная в AngularJS система внедрения зависимостей (dependency injection) работает с объектами, необходимыми в компонентах; в то время как управление зависимостями (dependency management) в RequireJS имеет дело с модулями или JavaScript-файлами.

Когда RequireJS пытается загрузить модуль, он сначала проверяет все зависимости и загружает их. Объекты загруженных модулей кешируются и предоставляются, когда те же модули запрашиваются снова. С другой стороны, AngularJS поддерживает инжектор со списком имет и соответствующих им объектов. Объект добавляется в инжектор, когда компонент создается и будет предоставлен, когда на него сошлются через зарегистрированное имя.

Использование RequireJS и AngularJS вместе


Код, включенный в статью, который можно загрузить здесь, является простым приложением, содержащим 2 страницы. Он имеет следующие внешние зависимости:
  • RequireJS
  • jQuery
  • AngularJS
  • Angular Route
  • Angular Resource
  • Angular UI ngGrid


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

Определение компонентов AngularJS как модулей RequireJS


Любой компонент AngularJS состоит из:
  • объявления функции
  • внедрения зависимостей
  • регистрация в модуле Angular


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

Во-первых, давайте определим блок конфигурации. Блок конфигурации не зависит ни от каких других блоков и в конце возвращает функцию config. Но, прежде чем загрузить модуль config внутри другого модуля, мы должны загрузить все, что необходимо для блока конфигурации. Следующий код содержится в файле config.js:

define([],function(){
  function config($routeProvider) {
    $routeProvider.when('/home', {
      templateUrl: 'templates/home.html', 
      controller: 'ideasHomeController'
    })
    .when('/details/:id',{
      templateUrl:'templates/ideaDetails.html', controller:'ideaDetailsController'})
    .otherwise({redirectTo: '/home'});
  }
  config.$inject=['$routeProvider'];
 
  return config;
});


Обратите внимание на способ внедрения зависимостей, использованный в этом фрагменте. Я использовал $inject, чтобы получить внедренные зависимости, так как функция config, объявленная выше, является простой JavaScript-функцией. Перед закрытием модуля мы возвращаем функцию config, так что она может быть передана в зависимый модуль для дальнейшего использования.

Мы следуем этому подходу для определения любого другого типа компонентов AngularJS, кроме того мы не имеем никакого специфичного для компонентов кода в этих файлах. Следующий фрагмент показывает определение контроллера:

define([], function() {
  function ideasHomeController($scope, ideasDataSvc) {
    $scope.ideaName = 'Todo List';
    $scope.gridOptions = {
      data: 'ideas',
        columnDefs: [
         {field: 'name', displayName: 'Name'},
         {field: 'technologies', displayName: 'Technologies'},
         {field: 'platform', displayName: 'Platforms'},
         {field: 'status', displayName: 'Status'},
         {field: 'devsNeeded', displayName: 'Vacancies'},
         {field: 'id', displayName: 'View Details', cellTemplate: '<a ng-href="#/details/{{row.getProperty(col.field)}}">View Details</a>'}
        ],
        enableColumnResize: true
        };
    ideasDataSvc.allIdeas().then(function(result){
      $scope.ideas=result;
    });
  }
 
  ideasHomeController.$inject=['$scope','ideasDataSvc'];
 
  return ideasHomeController;
});


Модуль Angular для приложения зависит от каждого из модулей, определенных до этого момента. Этот файл получает объекты от всех других файлов и цепляет их к модулю AngularJS. Этот файл может возвращать или не возвращать что-либо как результат, на него можно ссылаться из любого места с помощью angular.module(). Следующий фрагмент определяет модуль Angular:

define(['app/config',
  'app/ideasDataSvc',
  'app/ideasHomeController',
  'app/ideaDetailsController'],
 
  function(config, ideasDataSvc, ideasHomeController, ideaDetailsController){
    var app = angular.module('ideasApp', ['ngRoute','ngResource','ngGrid']);
    app.config(config);
    app.factory('ideasDataSvc',ideasDataSvc);
    app.controller('ideasHomeController', ideasHomeController);
    app.controller('ideaDetailsController',ideaDetailsController);
});


Это приложение Angular не может быть запущено с использованием директивы ng-app, так как необходимые скрипты загружаются асинхронно. Правильный подход здесь состоит в использовании ручного запуска. Это должно быть сделано в специальном файле, именуемом main.js. Здесь нужно, чтобы сначала был загружен файл с определением модуля Angular. Код для этого файла показан ниже.

require(['app/ideasModule'],
  function() {
    angular.bootstrap(document, ['ideasApp']);
  }
);


Конфигурирование Grunt для объединения модулей RequireJS


При развертывании больших JavaScript-приложений файлы скриптов следует объединять и минифицировать для оптимизации скорости их загрузки. Инструменты, подобные Grunt, могут пригодиться для автоматизации этих задач. Он имеет целый ряд задач, определенных чтобы сделать любой процесс front-end развертывания легче. У него есть задача grunt-contrib-requirejs для объединения файлов модулей RequireJS в правильном порядке и последующей минификации результирующего файла. Подобно любой другой задаче Grunt, она может быть сконфигурирована, чтобы вести себя по-разному для каждой стадии развертывания. Следующая конфигурация может быть использована в нашем демо-приложении:

requirejs: {
  options: {
    paths: {
      'appFiles': './app'
    },
    removeCombined: true,
    out: './app/requirejs/appIdeas-combined.js',
    optimize: 'none',
    name: 'main'
  },
  dev:{
    options:{
      optimize:'none'
    }
  },
  release:{
    options:{
      optimize:'uglify'
    }
  }
}


Эта конфигурация будет создавать несжатый файл, когда Grunt будет запущен с опцией dev, и минифицированный файл в случае запуска Grunt с опцией release.

Заключение


Управление зависимостями становится сложным, когда размер приложения превышает определенной количество файлов. Библиотеки, подобные RequireJS, позволяют легче определять зависимости и не беспокоиться о порядке загрузки файлов. Управление зависимостями становится неотъемлемой частью приложений JavaScript. AngularJS 2.0 будет иметь встроенную поддержку для AMD.

UPDATE: Было бы интересно услышать в комментариях, какими менеджерами зависимостей вы пользуетесь и что считаете лучшим вариантом.

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

Используете ли вы упомянутые в статье программные продукты?

Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

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

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

    +2
    Все нужные js собираются grunt'ом в общий js. Тащить зоопарк мелких файлов клиенту не было необходимости.
      +2
      поддержу
      только вместо grunt использую gulp
        +2
        в продакшене да, но при разработке удобнее пользоваться каким нибудь загрузчиком модулей. так же при сборке решается проблема ресолва зависимосткй.

        лично я последний месяц использую es6 модули.
          0
          А когда JS-кода под ~100Кб или больше? Тут целесообразнее подключать только то, что нужно на конкретной странице, и при этом иметь возможность отдавать HTML быстрее.
            0
            100кб это мало для SPA, а какой-то прелоадер можно отображать сразу по domready и запускать boostrap приложения вручную по завершению загрузки. Более того, озвученный выше метод не решает основной проблемы жирных приложений — ленивая подгрузка модулей. То есть приложению всеравно нужно загрузить все модули. В Angular2 эту проблему собираются решить использованием ES6 модулей, но думаю никто не мешает заменить реализацию $inject на свою, с ленивой подгрузкой на промисах, возможно с каким-то ранним механизмом для префетча. Но поскольку модуль $inject не «поправить» без патчинга самого angular, красивой реализации нет (а может и есть, ткните носом если так).

            В целом как я понял в большинстве случаев приложения на angular либо весят мало, либо никого не парит такие штуки (все же очень небольшой процент разработчиков пишут жирные приложения на angularjs не под гибриды к примеру а именно под web).
              0
              можно реализовать ленивую подгрузку с помощью resolve в роутере, но у меня не получилось красиво описывать зависимости и приходилось на каждый роут писать руками все что нужно ему и всем его зависимостям.
                0
                ресолвы в раутере отрабатывают после этапа конфигурации, так что добавить сервисы или директивы уже не выйдет.
                    0
                    я не вижу смысла грузить отдельные контроллеры. Они не должны быть сильно жирными по хорошему. Имеет смысл догружать скоупом модули отдельные. Но в целом конечно же от задачи зависит.

                    Ну и да, это кастыль а не красивое решение.
                      0
                      Ангуляр жесткий и поэтому любой шаг в сторону будет костылем. Выше было заявление, что в рантайме не загрузить директиву или сервис, ссылка это опровергает.
                      Смысла грузить в резолве я тоже не вижу. Чаще бывает необходимость догрузить на лету что-то.
                        0
                        Посмотрел как именно там это реализовано — они подменяют $injector у $rootElement при инициализации приложения на свой враппер.
                          0
                          Именно так.
                    0
                    я уже точно не помню как я делал, но возможно добавить директивы и сервисы на лету
                      0
                      $compileProvider можно дергать только на этапе конфигурации, регистрация директив происходит через него. То есть чисто теоритически можно в config сохранить ссылку на $compileProvider и после попытаться добавить директиву, но не уверен будут ли проблемы с этим.
            0
            Совершенно непонятен профит от такого уродования кода (лишний код + лишняя вложеность). В продакшене это бессмысленно, так как пока все файлы не загрузятся, приложение не стартанет.

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

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

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

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