AngularInDepth уходит от Medium. Более свежие статьи размещаются на новой платформе inDepth.dev. Спасибо за то, что участвуете в глубоком движении!

Angular обновил журнал изменений, где упомянул о критических изменениях, касающихся поставщиков платформ и компиляторов, и представил статический инжектор. Это замена существующего Reflective Injector, поскольку последний теперь не рекомендуется. Не многие из нас использовали Reflective Injector в своей базе кода напрямую, и еще меньше людей понимают тонкости его внутреннего устройства. Поэтому не сразу понятно, что делать с таким изменением.

Почему лучше? Как это повлияет на мой код? Мне нужно мигрировать? Вот вопросы, которые вы можете задать себе. Но не волнуйтесь. Я и Алексей Зуев позаботились обо всем. Эта статья даст вам ответ, который вам нужен.

Почему ReflectiveInjector отражающий?

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

class B {}

class A {
  constructor(@Inject(B) b) { }
}

const i = ReflectiveInjector.resolveAndCreate([A, B]);
const a = i.get(A);

У нас есть две службы A и B, и служба A зависит от службы B. И когда мы передаем поставщиков методу resolveAndCreate , мы нигде не указываем, что A зависит от B. Так откуда инжектор это знает?

Вы, наверное, догадались (или знали), что секрет в использовании Inject декоратора. Этот декоратор использует библиотеку Reflect для присоединения некоторых метаданных к классу A. В статье Реализация декоратора нестандартных компонентов в Angular я писал, как декораторы в Angular используют эту библиотеку.

Вот суть реализации декоратора Inject:

function ParamDecorator(cls: any, unusedKey: any, index: number) {
  ...
  // parameters here will be [ {token: B} ]
  Reflect.defineMetadata('parameters', parameters, cls);
  return cls;
}

Итак, как вы можете видеть, когда выполняется Inject, у нас есть информация о зависимостях, прикрепленных к классу.

В свою очередь, когда мы передаем поставщиков методу resolveAndCreate, инжектор проходит через каждого поставщика и пытается собрать все зависимости, используя один и тот же объект Reflect. Все это происходит внутри resolveReflectiveFactory функции, которая принимает поставщика и разрешает его зависимости:

function resolveReflectiveFactory(provider) {
  // ...
  if (provider.useClass) {
    var useClass = resolveForwardRef(provider.useClass);
    factoryFn = reflector.factory(useClass);
    resolvedDeps = _dependenciesFor(useClass);
  }
}

export class ReflectionCapabilities {
  ...
  private _ownParameters(type, parentCtor) {
     ...
     // R is Reflect
     const paramAnnotations = R.getOwnMetadata('parameters', type);
  }
}

Теперь это показывает, что ReflectiveInjector полагается на возможности отражения, которые в настоящее время предоставляет Reflect прокладка для извлечения неявных зависимостей.

Чем отличается StaticInjector

Новый Static Injector вообще не разрешает неявные зависимости. Вместо этого он требует, чтобы разработчик явно указал их для каждого поставщика. Итак, если бы с ReflectiveInjector у нас было следующее:

class B {}
class A { constructor(@Inject(B) b) {} }

const i = ReflectiveInjector.resolveAndCreate([A, B]);
const a = i.get(A);

В новом StaticInjector нам нужно реорганизовать приведенный выше код на следующий:

class B {}
class A { constructor(b) {} }
const i = Injector.create([{provide: A, useClass: A, deps: [B]]};
const a = i.get(A);

Вы можете видеть здесь, что я явно указываю зависимость B для поставщика A, используя свойство объекта deps. Этот новый тип поставщика называется StaticClassProvider и определяется следующим интерфейсом:

export interface StaticClassProvider {
  provide: any;
  useClass: Type<any>;
  deps: any[];
  multi?: boolean;
}

Теперь существует статическая версия классов провайдеров для всех возможных рецептов:

export type StaticProvider = ValueProvider | 
                             ExistingProvider |
                             StaticClassProvider |  
                             ConstructorProvider |
                             FactoryProvider | any[];

Чем StaticInjector лучше?

Главное преимущество статического инжектора - скорость. Выше я показал, что ReflectiveInjector полагается на возможности отражения. Поскольку браузеры не поддерживают такие возможности, библиотека Reflect поддерживает эту возможность и использует для этого карты JavaScript.

Он хранит все классы, для которых вы используете декораторы на карте, что можно увидеть из этого кода:

// core-js/library/modules/_metadata.js
var Map     = require('./es6.map')
  , $export = require('./_export')
  , shared  = require('./_shared')('metadata')
  , store   = shared.store || (shared.store = new (require('./es6.weak-map')));
var getOrCreateMetadataMap = function(target, targetKey, create){
  var targetMetadata = store.get(target);
  if(!targetMetadata){
    if(!create)return undefined;
    store.set(target, targetMetadata = new Map);
  }

Это означает, что если вы используете тысячи классов, у вас будут довольно большие карты. И поиск в этих картах может быть довольно дорогим. Статический инжектор, как вы видели, не использует Reflect, и, следовательно, поиск не требуется.

Помимо скорости, статический инжектор также устраняет зависимость от возможностей отражения и, следовательно, от Reflect библиотеки. Поскольку инжекторы модулей и компонентов не используют ReflectiveInjector, единственное, что не позволяет полностью удалить зависимость от Reflect, - это декораторы. Но декораторы поддерживают несколько параметров, помимо метаданных, поэтому весьма вероятно, что Reflect вообще не понадобится в будущем.

Что это изменение означает для вас?

В версии 5 Angular внесены некоторые критические изменения, но большинству разработчиков не нужно ничего делать для успешного перехода. Это связано с тем, что большинство разработчиков озабочены инжектором, созданным для модулей и компонентов. И ReflectiveInjectors там не используется. Но Angular также создает три других инжектора для Platform, Compiler и NgZone. И они используют ReflectiveInjector, и это изменение повлияет на них.

Но где разработчик с ними взаимодействует? Хорошо, запомните эту строку, которую вы обычно используете в своем main.ts:

platformBrowserDynamic().bootstrapModule(AppModule);

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

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

class B {}
class A { constructor(@Inject(B) b) {} }

platformBrowserDynamic([A, B])

А к компилятору вот так:

class B {}
class A { constructor(@Inject(B) b) {} }
bootstrapModule(AppModule, {providers: [A, B]});

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

class B {}
class A { constructor(b) {} }
platformBrowserDynamic([{ provide: A, useClass: A, deps: [B] }, B])

и платформа:

class B {}
class A { constructor(b) {} }
bootstrapModule(AppModule,
  {
    providers: 
      [
        {provide: A, useClass: A, deps: [B]},
        B
      ]
  });

Опять же, как я уже сказал выше, критическое изменение затрагивает только платформу и инжектор компилятора.

Поскольку ReflectiveInjector устарел, рекомендуется начать переход на новый статический инжектор, если вы использовали его в своем коде.

Спасибо за прочтение! Если вам понравилась эта статья, нажмите кнопку хлопка ниже 👏. Это очень много значит для меня и помогает другим людям увидеть историю.

Чтобы узнать больше, подпишитесь на меня в Twitter и Medium.