Pull to refresh

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

Reading time3 min
Views13K
При разработке достаточно большого приложения неизбежно возникает момент, когда приложение наконец-то становится достаточно большим чтобы тормозить. Для 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');
    };
})

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

Articles

Change theme settings