AngularJs – великолепный фреймворк для разработки web-приложений. Разработка бизнес-логики приложения полностью отделена от сопутствующей суеты вокруг DOM. Angular модульный – это замечательно, но так же является источником проблемы. Количество модулей быстро растёт. И если директивы ещё можно упаковывать в отдельные пакеты типа angular-ui, то с контроллёрами бизнес-логики всё сложнее. Всё становится ещё хуже, когда требования безопасности в принципе запрещают загрузку на клиента контроллёров с бизнес-логикой, которые недоступны текущему пользователю. При развитой ролевой системе доступа к приложению масштаб проблемы становится очевиден.
В Angular в принципе отсутствует система загрузки модулей по требованию. Но тем не менее можно самостоятельно разработать такой модуль, который будет подгружать javascript-файл. Но есть проблема. При вызове функции angular.module c которой начинается любой модуль Angular, не приводит к добавлению функционала во внутренние структуры Angular. И сделано это намеренно, чтобы можно было указывать тэги script в произвольном порядке не соблюдая зависимостей между модулями. Окончательная загрузка модулей будет произведена после того как html-документ будет полностью загружен. Собственно этим и занимается функция angular.bootstrap, которая создаёт экземпляр injector`а и инициализирует внутренние структуры фреймворка.
Итак, возникает задача:
- Обеспечить загрузку модулей с помощью директивы. Это даст возможность загружать модуль именно тогда, когда он действительно необходим.
- Обеспечить разрешение зависимостей. Т.е. если модуль имеет зависимости, то проверить все ли они удовлетворены. И если нет – то инициировать процедуру загрузки модулей, удовлетворяющих зависимость.
- Директива так же должна обеспечивать загрузку указанного шаблона, поскольку директивы в шаблоне могут иметь зависимость от загружаемого модуля (например, указание контроллёра) и модуль должен быть загружен раньше, а только потом применён шаблон.
- Ну и, естественно, компиляция и линковка загруженного шаблона.
Приступим.
Пример директивы, появление которой в коде будет инициировать загрузку модуля home:
<div load-on-demand="'home'"></div>
Помимо самой директивы load-on-demand, имеется имя загружаемого модуля. Такой вариант выбран для большей гибкости в конфигурировании загружаемых модулей. Конфигурирование обычно производится с помощью вызова функции module.config.
Пример вызова функции:
var app = angular.module('app', ['loadOnDemand']);
app.config(['$loadOnDemandProvider', function ($loadOnDemandProvider) {
var modules = [
{
name: 'home',
script: 'js/home.js'
}
];
$loadOnDemandProvider.config(modules, []);
}]);
Теперь перейдём непосредственно к директиве. В нашем случае нам не требуется тонко настраивать директиву, поэтому мы возвращаем только функцию связывания (linkFunction), которая делает всё необходимое. Псевдо-код, который демонстрирует алгоритм:
var aModule = angular.module('loadOnDemand', []);
aModule.directive('loadOnDemand', ['$loadOnDemand', '$compile',
function ($loadOnDemand, $compile) {
return {
link: function (scope, element, attr) {
var moduleName = scope.$eval(attr.loadOnDemand); // Имя модуля
// Получаем конфигурационную информацию о модуле
var moduleConfig = $loadOnDemand.getConfig(moduleName);
$loadOnDemand.load(moduleName, function() { // Загружаем скрипт
loadTemplate(moduleConfig.template, function(template) { // Загружаем шаблон
childScope = scope.$new(); // Создаём область видимости для контроллёра
element.html(template); // Вставляем сырой html в DOM
var content = element.contents(),
linkFn = $compile(content); // Преобразуем DOM-узел в шаблон angular
linkFn(childScope); // Связываем шаблон и scope
});
});
}
};
}]);
Ключевым моментом здесь является вызов функции $loadOnDemand.load(). Весь функционал по конфигурированию и загрузки скрипта находится в провайдере $loadOnDemand. Раскроем его. Я намеренно скрываю детали реализации, чтобы не захламлять код.
aModule.provider('$loadOnDemand', function(){
this.$get = [function(){ // Обязательная для провайдера функция, которая будет возвращать сервис
return {
getConfig: function (name) {}, // Получение конфигурации для загрузки модуля
load: function (name, callback) {} // Загрузка модуля
};
}];
this.config = function (config, registeredModules) {} // Функция конфигурирования провайдера
});
Каждый провайдер должен предоставить функцию $get, которая должна возвращать объект-сервис. Этот сервис будет использоваться инектором, когда он потребуется. Помимо функции $get наш привайдер предоставляет функцию config — она используется для конфигурирования загрузчика модулей (app.config выше). Дело в том, что функция module.config предоставляет только провайдеров, поэтому необходимо разделить логику конфигурирования провайдера от предоставляемого им сервиса.
Сам сервис имеет две функции: getConfig — используется для простоты получения конфигурационного объекта и, собственно, главная функция сервиса — load, которая загружает модуль. Низкоуровневая загрузка скрипта выполняется с помощью document.createScript — такая загрузка более дружественна для IDE отладчика.
И воде бы — это и всё, что нужно сделать. Но, это не будет работать. Причина указана выше — после того как скрипт будет загружен и выполнен, функционал модуля не будет размещён в инфраструктуре angular. Итак, погружаемся в angular.bootstrap.
После того как DOM загружен, запускается процедура инициализации angular. Она ищет директиву ng-app с именем главного модуля приложения. После этого создаётся инектор и выполняется компиляция DOM в шаблон angular`а. В этой цепочке нас больше всего интересует создание инектора, поскольку именно этот вызов запускает процедуру загрузки модулей — функцию loadModules. loadModules получает объект Module в котором имеется очередь команд для инектора — _invokeQueue. Эта очередь как раз и создаётся при вызове angular.module. Каждый элемент этой очереди отдаётся соответствующему провайдеру, который делает всю работу по добавлению функционала.
Нам необходимо просто повторить этот алгоритм, используя уже существующие провайдеры. Их мы получаем используя инектор.
aModule.provider('$loadOnDemand',
['$controllerProvider', '$provide', '$compileProvider', '$filterProvider',
function ($controllerProvider, $provide, $compileProvider, $filterProvider) {
. . .
loadScript(moduleName, function(){
register(moduleName);
});
. . .
}]);
Функция регистрации модуля register.
moduleFn = angular.module(moduleName);
for (invokeQueue = moduleFn._invokeQueue, i = 0, ii = invokeQueue.length; i < ii; i++) {
invokeArgs = invokeQueue[i];
provider = providers[invokeArgs[0]];
provider[invokeArgs[1]].apply(provider, invokeArgs[2]);
}
В invokeArgs[0] находится имя провайдера, invokeArgs[1] — его метод регистрации нового сервиса. invokeArgs[2] — параметры, которые передаются методу регистрации (список инекций и функция-конструктор сервиса).
Вот, пожалуй и всё, остаётся только загрузить зависимости, которые находятся в moduleFn.requires в виде простого массива имён модулей. После подключения подобного модуля к вашему проекту главная страница будет выглядеть как-то так:
<!DOCTYPE html>
<html ng-app="app">
<head>
</head>
<body>
<div ng-view></div>
<script src="js/angular.js"></script>
<script src="js/loadOnDemand.js"></script>
</body>
</html>
А главный модуль приложения, как-то так:
(function(){
var app = angular.module('app', ['loadOnDemand']);
app.config(['$routeProvider', function ($routeProvider) {
. . .
};
app.config(['$loadOnDemandProvider', function ($loadOnDemandProvider) {
. . .
};
})();
Проект лежит на github
Автор: AndyGrom