Инфраструктура для разработки приложений Angular была задумана как платформенно-независимая технология (далее по тексту — фреймворк). Такой подход позволяет запускать приложения на Angular в разных средах: в браузере, сервере, веб-воркере и даже на мобильных устройствах.

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

Содержание

  • Angular — это кроссплатформенный фреймворк
  • Что такое платформы Angular?
  • Как платформы Angular делают возможным кроссплатформенный запуск приложений?

Angular — это кроссплатформенный фреймворк

Как было сказано выше, Angular была задумана платформенно-независимой инфраструктурой, в которую была заложена возможность вариативного применения. Благодаря этому, Angular является кроссплатформенным фреймворком, не ограничивающимся только браузером. Единственное, что необходимо для запуска приложений на Angular, — это движок JavaScript. Рассмотрим самые популярные рабочие среды Angular.

Браузер

Когда мы создаём новое приложение на Angular, используя Angular CLI ng new MyNewApplication, в качестве среды для нашего приложения по умолчанию используется браузер.

Сервер

Приложения на Angular могут компилироваться и запускаться на серверной стороне. В этом случае мы можем компилировать Angular-приложение в статические HTML-файлы и затем отправлять эти файлы клиентам.

Благодаря этому, мы можем ускорить загрузку приложений и заодно позаботиться о правильной индексации приложений всеми поисковыми системами.

Веб-воркер

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

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

Web worker с самого начала была экспериментальной средой, и к моменту появления Angular 8 она устарела.

NativeScript

Кроме того, существует множество сторонних библиотек, позволяющих запускать Angular-приложения в разных средах. Например, NativeScript, который делает возможным запуск Angular на мобильных устройствах с использованием всей функциональности их собственных платформ.

Но как это вообще возможно — запускать Angular-приложения в разных средах?

Ответ — платформы!

Что такое платформы Angular?

Чтобы разобраться, что такое платформы Angular, надо обратиться к точке входа любого Angular-приложения — файлу main.ts:

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

platformBrowserDynamic().bootstrapModule(AppModule);

Здесь для нас важны две части:

  • platformBrowserDynamic() функция, вызывающая и возвращающая некий объект.
  • Этот объект используется для начальной загрузки нашего приложения.

Если мы слегка её перепишем, обнаружится одна интересная деталь:

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { PlatformRef } from '@angular/core';
 
 
// Создать браузерную платформу
const platformRef: PlatformRef = platformBrowserDynamic();
 
// Начальная загрузка приложения
platformRef.bootstrapModule(AppModule);

platformBrowserDynamic —это фабрика платформ, функция, создающая новые экземпляры платформ. Результатом вызова функции platformBrowserDynamic является PlatformRef. PlatformRef — это простой сервис Angular, который умеет производить начальную загрузку наших приложений. Для лучшего понимания того, как этот экземпляр PlatformRef создаётся, давайте повнимательнее проследим за реализацией этой функции platformBrowserDynamic:

export const platformBrowserDynamic = createPlatformFactory(
  
  // Родительская фабрика платформ
  platformCoreDynamic,
  
  // Название для новой платформы
  'browserDynamic',
  
  // Дополнительные провайдеры
  INTERNAL_BROWSER_DYNAMIC_PLATFORM_PROVIDERS,
);

Здесь мы можем видеть, что функция platformBrowserDynamic — это всего лишь результат функции createPlatformFactory, которая принимает следующие параметры:

  • Родительская фабрика платформ — platformCoreDynamic
  • Название для новой платформы — ‘browserDynamic’
  • Дополнительные провайдеры — INTERNAL_BROWSER_DYNAMIC_PLAFORM_PROVIDERS

Здесь platformCoreDynamic — это функция родительской фабрики платформ. Мы можем рассматривать platformCoreDynamic и platformBrowserDynamic как состоящие в иерархии наследования. Поэтому функция createPlatformFactory просто помогает нам получить из одной фабрики платформ другую. Всё просто.

Самое интересное происходит в этой иерархии наследования чуть дальше. На самом деле, platformCoreDynamic наследует platformCore, у которой, в свою очередь, нет родителя.

Так что полная иерархия для platformBrowserDynamic выглядит следующим образом:

Однако фабрики платформ Angular не меняют поведения родительских фабрик платформ в процессе наследования. Они обеспечивают наши приложения дополнительными токенами и сервисами.

Кажется, немного сложновато. Попробуем разобраться с функцией createPlatformFactory и понять, как именно создаются фабрики платформ Angular.

Вот суперупрощённый алгоритм для createPlatformFactory:

type PlatformFactory = (extraProviders?: StaticProvider[]) => PlatformRef;

export function createPlatformFactory(
  parentPlatformFactory: PlatformFactory,
  name: string,
  providers: StaticProvider[] = [],
): PlatformFactory {

  return (extraProviders: StaticProvider[] = []) => {
    const injectedProviders: StaticProvider[] = providers.concat(extraProviders);

    if (parentPlatformFactory) {
      return parentPlatformFactory(injectedProviders);
    } else {
      return createPlatform(Injector.create({ providers: injectedProviders }));
    }
  };
}

Когда мы вызываем эту функцию, она возвращает функцию фабрики платформ, которая принимает дополнительные StaticProviders для наших приложений. И если мы используем родительскую фабрику платформ, функция createPlatformFactory вызовет её и вернёт её значение или же просто создаст и вернёт новую платформу. Для лучшего понимания рассмотрим процесс создания platformBrowserDynamic шаг за шагом:

  1. platformBrowserDynamic создаётся в результате вызова функции createPlatformFactory с platformCoreDynamic в качестве родительской платформы.
  2.  Для создания новой платформы вызываем функцию platformBrowserDynamic.
  3. Она проверяет, существует ли parentPlatformFactory, и вызывает её с помощью ряда дополнительных провайдеров, а затем просто возвращает её значение:
if (parentPlatformFactory) { 
  return parentPlatformFactory(injectedProviders); 
}

4. На этом этапе можно заметить, что результатом функции platformBrowserDynamic на самом деле является результат функции platformCoreDynamic со всеми сервисами, используемыми platformBrowserDynamic.

5. platformCoreDynamic создаётся так же, как platformBrowserDynamic, но с двумя отличиями — она расширяет platformCore и использует собственные провайдеры.

export const platformCoreDynamic = createPlatformFactory(
  platformCore,
  'coreDynamic', 
  CORE_DYNAMIC_PROVIDERS,
);

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

platformCore([ ...CORE_DYNAMIC_PROVIDERS, ...BROWSER_DYNAMIC_PROVIDERS ]);

6. Но внутри platformCore у нас несколько другая ситуация.

export const platformCore = createPlatformFactory(
  null,
  'core',
  CORE_PLATFORM_PROVIDERS,
);

Здесь в CORE_PLATFORM_PROVIDERS содержится самый важный провайдер — сервис PlatformRef. Когда мы используем null в качестве родительской фабрики платформ, функция createPlatformFactory просто возвращает результат функции createPlatform.

7. Функция createPlatform, в свою очередь, будет просто извлекать PlatformRef из injector. И возвращать её к источнику вызова.

function createPlatform(injector: Injector): PlatformRef {
  return injector.get(PlatformRef);
}

8. Теперь у нас есть PlatformRef:

const ref: PlatformRef = platformBrowserDynamic();

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

Здесь можно заметить, что platformCore не похож на другие платформы. platformCore в каком-то роде особенный, потому как он отвечает за использование PlatformRef для процесса создания платформ и служит корневой платформой для всех платформ в экосистеме Angular.

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

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

Как Angular-платформы делают возможным кроссплатформенный запуск

На этом этапе мы узнали, что такое Angular-платформы и как они создаются. Теперь обсудим, каким образом Angular-платформы делают из Angular кроссплатформенный фреймворк.

Всё дело в абстракции. Как известно, Angular в значительной степени основана на системе внедрения зависимостей. Именно поэтому довольно большая часть самой Angular представлена абстрактными сервисами:

  • Renderer2
  • Compiler
  • ElementSchemaRegistry
  • Sanitizer
  • и др.

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

Но этим абстрактным классам не хватает реализации или функциональности. Когда мы используем браузерную платформу, она даёт собственные средства реализации для этих сервисов:

Когда же мы используем, скажем, серверную платформу, она даёт уже свои собственные средства реализации этих абстрактных базовых сервисов:

Теперь приведём конкретный пример.

Предположим, Angular использует абстракцию DomAdapter для обработки данных объектной модели документа DOM независимо от среды. Здесь имеет место упрощённая версия абстрактного класса DomAdapter.

export abstract class DomAdapter {
  abstract setProperty(el: Element, name: string, value: any): any;
  abstract getProperty(el: Element, name: string): any;
  abstract querySelector(el: any, selector: string): any;
  abstract querySelectorAll(el: any, selector: string): any[];
  abstract appendChild(el: any, node: any): any;
  abstract removeChild(el: any, node: any): any;
  
  //... и т.д.
}

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

export class BrowserDomAdapter extends DomAdapter { ... }

BrowserDomAdapter взаимодействует с браузерным DOM непосредственно, и поэтому не может быть использован где-то ещё, кроме браузера.

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

export class DominoAdapter extends DomAdapter { ... }

DominAdapter не взаимодействует с DOM, так как у нас нет DOM на серверной стороне. Вместо этого он использует библиотеку domino, которая имитирует DOM для node.js.

В результате имеем следующую структуру:

Заключение

Поздравляем! Вы добрались до конца статьи, в которой мы осветили ряд вопросов: что такое платформы Angular, как они создаются — а также прошли шаг за шагом процесс создания platformBrowserDynamic. И, наконец, разобрались, как концепция платформы делает из Angular кроссплатформенный фреймворк.

Читайте также:


Перевод статьи Nikita Poltoratsky: Angular Platforms in depth. Part 1. What are Angular Platforms?

Предыдущая статья30 полезных сниппетов на Python, которые можно освоить за 30 секунд
Следующая статьяПлатформы Angular в деталях. Часть 2. Процесс начальной загрузки приложения