Инфраструктура для разработки приложений Angular была задумана как платформенно-независимая технология (далее по тексту — фреймворк). Такой подход позволяет запускать приложения на Angular в разных средах: в браузере, сервере, веб-воркере и даже на мобильных устройствах.
В данной серии статей я опишу, как это вообще возможно — запускать Angular-приложения в разных средах. Мы также научимся создавать пользовательскую платформу Angular, с помощью которой можно визуализировать приложения из терминала, используя графику ASCII.
Как было сказано выше, 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-приложения — файлу 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
шаг за шагом:
platformBrowserDynamic
создаётся в результате вызова функции createPlatformFactory
с platformCoreDynamic
в качестве родительской платформы.platformBrowserDynamic
.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
:
Обратите внимание: в процессе наследования платформы не меняют поведения PlatformRef
явным образом. Вместо этого они дают новые наборы сервисов, которые использует PlatformRef
в процессе начальной загрузки.
Здесь можно заметить, что platformCore
не похож на другие платформы. platformCore
в каком-то роде особенный, потому как он отвечает за использование PlatformRef
для процесса создания платформ и служит корневой платформой для всех платформ в экосистеме Angular.
В итоге мы можем сказать, что каждая платформа состоит из двух важных частей:
PlatformRef
— сервис, который производит начальную загрузку приложения Angular.На этом этапе мы узнали, что такое Angular-платформы и как они создаются. Теперь обсудим, каким образом Angular-платформы делают из Angular кроссплатформенный фреймворк.
Всё дело в абстракции. Как известно, Angular в значительной степени основана на системе внедрения зависимостей. Именно поэтому довольно большая часть самой Angular представлена абстрактными сервисами:
Все эти сервисы и многие другие представлены абстрактными классами внутри 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?
Комментарии