Разделение приложения 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');     }; }) 

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

ссылка на оригинал статьи http://habrahabr.ru/post/213705/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *