Статьи

AngularJS Lifetime Management, Lazy-Loading и другие передовые методы DI

Один из аспектов Angular, который я люблю, — это внедрение зависимостей. Вопреки некоторой критике, которую я прочитал, я считаю, что она чрезвычайно гибкая и достаточно мощная, чтобы соответствовать требованиям корпоративных бизнес-приложений. Я рассмотрел общие преимущества DI в объяснении внедрения зависимостей и, в частности, реализацию Angular с использованием провайдеров, сервисов и фабрик и даже аспектно-ориентированный перехват / оформление .

В этой статье я расскажу о некоторых других функциях, общих для расширенных контейнеров Inversion of Control, а именно о отложенной загрузке, управлении временем жизни и отложенном создании / разрешении.

Ленивый-Loading

Ленивая загрузка просто относится к позднему созданию объектов, когда они вам нужны. Многие системы внедрения зависимостей создают компонент в первый раз, когда он распознается как зависимость, но в некоторых случаях вы можете не захотеть создавать экземпляр компонента до тех пор, пока в жизни приложения не будет. В Angular идеальным примером является настройка поведения на этапе настройки, которое ссылается на компоненты, которые еще не были созданы.

Предположим, вы хотите перехватить встроенную службу $ log, чтобы она сохраняла записи в $ rootScope. Я не рекомендую это, но это работает для простого, надуманного примера. Для перехвата вы ссылаетесь на $ обеспечить на этапе настройки, а затем вызываете метод декоратора. Если вы попытаетесь обратиться к $ rootScope напрямую, вы получите исключение из-за циклических зависимостей. Решение состоит в том, чтобы вместо этого загружать $ rootScope, используя $ инжектор .

Следующий код загрузит $ rootScope только при первом его использовании.

$provide.decorator('$log', ['$delegate', '$injector',
    function ($delegate, $injector) {
        var log = $delegate.log.bind($delegate);
        $delegate.log = function (msg) {
            var rs = $injector.get('$rootScope');
            if (rs.logs === undefined) {
                rs.logs = [];
            }
            rs.logs.push(msg);
            log(msg);
        };
        return $delegate;
}]);

Последующие вызовы всегда будут получать один и тот же экземпляр $ rootScope. Вот рабочая скрипка . Я слышал эту (ошибочную) критику раньше (то, что Angular поддерживает только одиночные игры)… не соответствует действительности. Методы $ инжектора — это то, что вы используете для управления временем жизни ваших компонентов.

Управление жизненным циклом

Управление временем жизни относится к тому, как вы обрабатываете экземпляры компонентов. По умолчанию, когда вы внедряете зависимость Angular, внедрение зависимости создаст одну копию и повторно использует эту копию во всем приложении. В большинстве случаев это именно то поведение, которое вы хотите. Есть некоторые решения, которые могут потребовать нескольких экземпляров одного и того же компонента. Рассмотрим на минуту встречную услугу:

function Counter($log) {
    $log.log('Counter created.');
}
 
angular.extend(Counter.prototype, {
    count: 0,
    increment: function () {
        this.count += 1;
        return this.count;
    }
});
 
Counter.$inject = ['$log'];
 
app.service('counter', Counter);

Вашему приложению может потребоваться отслеживать различные счетчики. Когда вы вводите услугу, вы всегда получаете один и тот же счетчик. Это угловое ограничение?

Не совсем. Опять же, используя сервис $ injector, вы можете создать новую копию в любое время. Этот код использует два отдельных счетчика:

app.run(['$rootScope', 'counter', '$injector',
    function (rs, c, i) {
        rs.count = c.count;
        rs.update = c.increment;
        rs.update2 = function () {
            var c = i.instantiate(Counter);
            rs.count2 = c.count;
            rs.update2 = function () {
                c.increment();
                rs.count2 = c.count;
            };
        };
    }]);

Вы можете видеть, что каждый счет отслеживается в отдельном экземпляре в рабочей скрипке . Если вы знаете, что будете часто генерировать новые экземпляры, вы можете зарегистрировать сервис следующим образом:

app.factory('counterFactory', ['$injector',
    function (i) {
        return {
            getCounter: function () {
                return i.instantiate(Counter);
            }
        };
    }]);

Тогда просто получить новый экземпляр по мере необходимости, и вы можете ссылаться на ваш фабричный компонент вместо $ injector:

app.run(['$rootScope', 'counterFactory',
    function (rs, cf) {
        var c1 = cf.getCounter(),
            c2 = cf.getCounter();
        rs.count = c1.count;
        rs.update = c1.increment;
        rs.count2 = c2.count;
        rs.update2 = function () {
            rs.count2 = c2.increment();
        };
    }]);

Вы можете проверить эту версию, запустив полную скрипку здесь . Как видите, полностью управлять жизненным циклом ваших компонентов можно с помощью встроенного в Angular внедрения зависимостей. Но как насчет отложенного разрешения — то есть тех компонентов, которые вы можете ввести после того, как Angular уже настроен, но все еще должен быть связан со своими собственными зависимостями?

Отложенное разрешение

Вы уже видели, как можно отложить разрешение зависимостей в Angular. Когда вы хотите что-то подключить, вы можете вызвать instantiate в сервисе $ injector, и он разрешит зависимости, используя либо сниффинг параметров, либо поиск статического свойства $ inject, либо проверку массива, который вы передаете. Другими словами, это совершенно верно:

$injector.instantiate(['dependency', Constructor]);

Вы также можете вызвать функцию, украшенную массивом. Если у вас есть функция, зависящая от службы $ log, вы можете вызывать ее во время выполнения с разрешением зависимости следующим образом:

var myFunc = ['$log', function ($log) {
    $log.log('This dependency wired at runtime.');
}];
$injector.invoke(myFunc);

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

Резюме

Таким образом, внедрение зависимостей Angular предоставляет много дополнительных функций, которые вы ожидаете и часто требуете для бизнес-приложений. Сокращенные методы для фабрик, сервисов и провайдеров иногда сбивают с толку разработчиков Angular, полагая, что это единственные доступные варианты. Волшебство действительно происходит в сервисе $ injector, где вы можете получить свой экземпляр-одиночку, создать новый компонент или динамически вызвать функцию с зависимостями.

В качестве последнего замечания, инжектор доступен вашему клиентскому коду даже за пределами Angular. Чтобы увидеть пример кода JavaScript, который подключен за пределами Angular, но использует инжектор для захвата службы $ log, нажмите здесь . Почему ‘ng’ передается в массив для функции? Это основной модуль Angular, который неявно добавляется при подключении ваших собственных модулей, но должен быть явно включен при непосредственном создании собственного экземпляра инжектора.