Ленивая подгрузка библиотек из CDN в Angular

Когда я интегрировал свое Angular-караоке с YouTube, мне попался официальный YouTube-компонент из Angular Material. В README прилагалась инструкция для подключения:

let apiLoaded = false; @Component({ template: '<youtube-player videoId="PRQCAL_RMVo"></youtube-player>', selector: 'youtube-player-example',
})
class YoutubePlayerExample implements OnInit { ngOnInit() { if (!apiLoaded) { const tag = document.createElement('script'); tag.src = 'https://www.youtube.com/iframe_api'; document.body.appendChild(tag); apiLoaded = true; } }
}

Почти каждая строка в этом примере сомнительна — от отсутствия раннего выхода до глобальных переменных и прямой манипуляции с DOM. Это работает, конечно, но в Angular мы можем лучше! Давайте разберемся, как можно применить имеющиеся инструменты для ленивой подгрузки библиотеки.

Dependency Injection снова выручает

Есть неплохой трюк для ленивой загрузки блока кода через токен, про который писал Reactive Fox: 

У этого подхода есть один недостаток — он работает только с локальными файлами. Так не получится загрузить библиотеку с CDN. Но мы всё равно можем применить токен для похожего эффекта. 

DI-токен отлично подходит для того, что нужно загрузить один раз во время первого запроса. Это хорошая альтернатива грязному подходу с глобальной переменной из примера выше. С помощью токена DOCUMENT мы также можем абстрагироваться от прямой манипуляции DOM, и наш код не упадет при серверном рендеринге (об этом я уже писал). Но сначала давайте добавим вспомогательный токен на CDN URL:

export const API_URL = new InjectionToken<string>('CDN URL of Youtube API', { factory: () => 'https://www.youtube.com/iframe_api'
});

Библиотеки часто регистрируют себя на глобальном объекте window. Вот так будет выглядеть типовой токен на библиотеку:

export const API$ = new InjectionToken<Observable<Interface>>( 'A stream with third party library object', { factory: () => { const documentRef = inject(DOCUMENT); const script = documentRef.createElement('script'); script.src = inject(API_URL); documentRef.body.appendChild(script); return fromEvent(script, 'load').pipe( map(() => documentRef.defaultView.libraryObject), ); } }
);

В случае YouTube API недостаточно просто дождаться загрузки скрипта. В документации написано, что нужно добавить колбэк onYouTubeIframeAPIReady на window и скрипт вызовет его, когда будет готов. Стоит заметить, что это произойдет в обход zone.js. То есть, даже если мы правильно отреагируем на вызов, Angular его не заметит и не запустит проверку изменений. Поэтому нужно обернуть вызов в NgZone.run:

export const API$ = new InjectionToken<Observable<YT>>( 'A stream with YT object', { factory: () => { const documentRef = inject(DOCUMENT); const zone = inject(NgZone); const windowRef = documentRef.defaultView; const script = documentRef.createElement('script'); const loaded$ = new ReplaySubject(1); script.src = inject(API_URL); documentRef.body.appendChild(script); windowRef.onYouTubeIframeAPIReady = () => { zone.run(() => loaded$.next(windowRef.YT)); }; return loaded$; } }
);

Это токен — Observable. В большинстве случаев удобнее работать с непосредственным значением. Поэтому давайте добавим последний токен на сам объект YT:

export const API = new InjectionToken<YT>('Youtube API object');

Работаем с написанным

Первый способ превратить Observable в значение и работать с ним дальше — Resolver. Возьмем lazy-роут, который использует YouTube, и добавим специальный ресолвер, который возьмет наш токен и дождется загрузки. Так объект YT станет доступным в data нашего роута. Затем можно положить его в токен. Что-то подобное Hien Pham описывал в своей статье про уменьшение дублирования через DI.

Второй способ — создать структурную директиву, которая будет ждать загрузки API, прежде чем инстанцировать шаблон. Сама директива будет простой:

export class WithYTDirective implements OnChanges { @Input() withYT: YT | null = null; constructor( private readonly templateRef: TemplateRef<{}>, private readonly vcr: ViewContainerRef ) {} ngOnChanges() { if (this.withYT) { this.vcr.createEmbeddedView(this.templateRef); } }
}

Поскольку шаблон создается только когда в withYT уже будет объект YT, мы можем добавить следующий провайдер:

export function extractAPI({ withYT }: WithYTDirective): YT { return withYT;
} @Directive({ selector: '[withYT]', providers: [ { provide: API, deps: [WithYTDirective], useFactory: extractAPI } ]
})
export class WithYTDirective implements OnChanges { // ...
}

Напомню, фабрика провайдера вызовется, только когда его запросят.

Теперь мы можем создать компонент YouTube, который просто берет API из DI. Описывать подробно такой компонент я не буду. Предположим, что он у нас есть. Вот так мы могли бы использовать его с нашей директивой:

@Component( selector: 'my-component', template: `<youtube-player *withYT="api$ | async"></youtube-player>`
)
export class MyComponent { constructor(@Inject(API$) readonly api$: Observable<YT>) {}
}

Итоговый вариант можно посмотреть на StackBlitz.

Вместо заключения

DI, токены и фабрики дают мощные инструменты абстракции. Мы можем вынести в них трансформацию данных или логику инициализации. Это особенно полезно для авторов библиотек, но может пригодиться и при создании приложений. 

Уделите побольше времени, чтобы ближе познакомиться с Dependency Injection, и он станет вашим верным союзником в написании надежного гибкого кода.

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

  • [Перевод] Вечеропятничное моделирование: как плавает акула-собака[Перевод] Вечеропятничное моделирование: как плавает акула-собака Катран, или морская собака (Squalus acanthias) – достаточно широко распространенная акула, относящаяся к роду колючих акул и семейству Катрановые акулы из отряда Катранообразные. Обитатель умеренных вод бассейнов всех мировых океанов, как правило, встречается на глубине не более 1460 […]
  • Facebook анонсировал новые инструменты для креаторов в регионе EMEAFacebook анонсировал новые инструменты для креаторов в регионе EMEA Meta анонсировала ряд обновлений, которые призваны поддержать креаторов Facebook в регионе EMEA. Новые функции предоставят им больше возможностей для самовыражения, создания сообществ и заработка на своих увлечениях. В их числе значатся следующие:  Новый […]
  • Автоматическое наполнение сайта контентомАвтоматическое наполнение сайта контентом Раскрытие информации: Ваша поддержка помогает поддерживать работу сайта! Мы зарабатываем реферальную плату за некоторые услуги, которые мы рекомендуем на этой странице. Узнайте больше Ведение блога занимает много времени, и писать для него-это только половина работы. Вы также должны […]
  • Яндекс 360 и Polymedia представили дисплеи для дистанционного обученияЯндекс 360 и Polymedia представили дисплеи для дистанционного обучения Яндекс 360 и системный интегратор Polymedia договорились о выпуске дисплеев TeachTouch с сервисом Телемост. С его помощью преподаватели смогут проводить занятия дистанционно. До конца года в учебных заведениях страны появятся 1500 таких устройств. Дисплеи TeachTouch установят в школах, […]