...

понедельник, 7 апреля 2014 г.

Разделение приложения AngularJS на изолированные модули

При разработке достаточно большого приложения неизбежно возникает момент, когда приложение наконец-то становится достаточно большим чтобы тормозить. Для AngularJS существует множество методик позволяющих добиться нужной производительности: bindonce, фильтрация списков, использование $digest вместо $apply, ng-if вместо ng-show (или наоборот), и другие. Но все они позволяют делать только локальные улучшения, не помогая в глобальном плане: избавиться полностью от вызовов $rootScope.$digest не получается, а проверка состояния всего приложения может идти очень долго.

В этой статье я хочу предложить архитектурное решение: разбиение приложения на несколько несвязанных с точки зрения фреймворка частей и самостоятельная реализация связей между ними.


В Angular существует понятие bootstrap. Это метод, который обычно вызывается после загрузки страницы, если существует элемент с атрибутом ng-app. Он связывает этот элемент c указанным в значении атрибута модулем. Такой элемент должен быть один, иначе документация ничего не гарантирует. Однако можно использовать его вручную: angular.bootstrap(element, [/*Module*/]); При этом будет запущен указанный модуль, все его зависимости, а также модуль ng и его зависимости. Поэтому у нового приложения (назовем его изолированным модулем) будет свой $injector, $rootScope, $compile и т.д. — вся внутрення кухня Angular будет создана заново. Родительский изолированный модуль не будет знать о существовании вложенных в него, между модулями не будут проходить события (emit и broadcast), а $digest, вызванный в одном изолированном модуле, не будет просачиваться в другой. Для слабо связанных компонент это то, что надо.


Для удобного создания новых приложений может быть использована следующая директива:



directive('newApp', function () {
return{
restrict: 'EA',
transclude: true,
scope: {
module: '='
},
link: function (scope, element, attr, ctrl, transclude) {
var div = document.createElement('app');
var module = angular.module(scope.$id, [scope.module]).run(['$rootScope', function ($rootScope) {
scope.$on('$destroy', function() {
$rootScope.$destroy();
angular.module(scope.$id, []);
});
transclude($rootScope, function (el) {
angular.element(div).append(el);
element.append(div);
});
}]);
angular.bootstrap(div, [scope.$id]);
}
};
});




Использование:

<body ng-app="App">
<new-app module=" 'SomeModule' ">
<some-module-directive/>
</new-app>
</body>


Для сравнения производительности до и после можно посмотреть синтетический пример: медленный вариант и быстрый вариант.


Отдельно стоит упомянуть про утечки памяти. За их отсутствие отвечает обработчик события $destroy в директиве. Он отправляет это событие внутрь изолированного модуля, чтобы все об этом узнали и перезаписывает модуль, чтобы удалить зарегистрированные директивы, контроллеры и др. Однако, память все-таки утекает, например из-за кэша элементов в angular.element.cache и много чего другого. Этот вопрос заслуживает отдельного исследования и статьи.


Еще одна обнаруженная проблема — сервис $location. Помимо прочего, он наблюдает за адресом страницы, и при его изменении делает какие-то телодвижения, например обновляет содержимое ngView. В случае нескольких изолированных модулей будет создано несколько экземпляров $location, несколько обработчиков изменения url, что не есть хорошо. Пока что придумал следующий обходной путь:



.config(function ($locationProvider) {
$locationProvider.$get = function () {
return angular.element(document).injector().get('$location');
};
})




Сейчас провожу тестирование и оформление кода в виде библиотеки, интересует мнение сообщества.

This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий