[JavaScript, Node.JS, Angular, TypeScript] Angular Universal: проблемы реального приложения
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Angular Universal — это опенсорсный проект, который расширяет функциональность @angular/platform-server. Он делает возможным Server Side Rendering в Angular.Angular Universal поддерживает несколько бэкендов:
Еще один пакет Socket Engine — это фреймворк-агностик, который теоретически позволяет подключать к SSR-серверу любой бэкенд. В этой статье мы разберем проблемы, с которыми мы столкнулись при разработке реального приложения с Angular Universal и Express, и их решения.Как работает Angular UniversalДля рендеринга на сервере Angular использует имплементацию DOM для node.js — domino. На каждый GET-запрос domino создает объект, аналогичный браузерному документу.В контексте этого объекта Angular инициализирует приложение. Приложение делает необходимые запросы на бэкенд, выполняет различные асинхронные задачи и мутирует DOM, находясь в серверном окружении. Затем движок рендера сериализует DOM в строку и отдает эту строку серверу. Сервер отправляет HTML в качестве ответа на GET-запрос. Angular-приложение на сервере после рендера разрушается.Проблемы SSR в Angular1. Бесконечная загрузка страницыСитуацияПользователь открывает страницу вашего сайта и видит белый экран. Другими словами, время до первого байта стремится к бесконечности. Браузер очень хочет получить ответ от сервера, но запрос завершается таймаутом.Почему так происходитСкорее всего, проблема кроется в специфичном для Angular механизме SSR. Прежде чем понять, в какой момент происходит рендеринг страницы, дадим определение Zone.js и ApplicationRef.Zone.js — это инструмент, который позволяет отслеживать асинхронные операции. С его помощью Angular создает свою зону и запускает в ней приложение. При завершении каждой асинхронной операции в зоне Angular запускается обнаружение изменений.ApplicationRef — это ссылка на запущенное приложение (docs). Из всего функционала этого класса нас интересует свойство ApplicationRef#isStable. Это Observable, который испускает boolean. isStable равен true тогда, когда в зоне Angular отсутствуют выполняющиеся асинхронные задачи, а false — когда таких задач нет.Из этого следует, что стабильность приложения — это состояние приложения, которое зависит от наличия асинхронных задач в зоне AngularИтак, в момент первого наступления стабильности Angular рендерит текущее состояние приложения и дестроит платформу. А платформа дестроит приложение.Теперь мы можем сделать предположение, что пользователь пытается открыть приложение, которое не может достичь стабильности. setInterval, rxjs.interval или любая другая рекурсивная асинхронная операция, запущенная в зоне Angular, сделают наступление стабильности невозможным. HTTP-запросы так же влияют на стабильность. Затянувшийся запрос на сервере оттягивает момент рендеринга страницы.Возможное решениеЧтобы избежать ситуации с долгими запросами, используйте оператор timeout:
import { timeout, catchError } from 'rxjs/operators';
import { of } from 'rxjs/observable/of';
http.get('https://example.com')
.pipe(
timeout(2000),
catchError(e => of(null))
).subscribe()
Оператор выбросит исключение через заданный промежуток времени, если ответ от сервера не будет получен.У такого подхода есть два минуса:
- нет удобного разделения логики по платформам;
- оператор timeout нужно прописывать руками для каждого запроса.
Как более простое решение можно использовать модуль NgxSsrTimeoutModule из пакета @ngx-ssr/timeout. Импортируйте модуль со значением таймаута в корневой модуль приложения. Если модуль будет импортирован в AppServerModule, то таймауты HTTP-запросов будут работать только для сервера.
import { NgModule } from '@angular/core';
import {
ServerModule,
} from '@angular/platform-server';
import { AppModule } from './app.module';
import { AppComponent } from './app.component';
import { NgxSsrTimeoutModule } from '@ngx-ssr/timeout';
@NgModule({
imports: [
AppModule,
ServerModule,
NgxSsrTimeoutModule.forRoot({ timeout: 500 }),
],
bootstrap: [AppComponent],
})
export class AppServerModule {}
Для выведения асинхронных операций из зоны Angular используйте сервис NgZone.
import { Injectable, NgZone } from "@angular/core";
@Injectable()
export class SomeService {
constructor(private ngZone: NgZone){
this.ngZone.runOutsideAngular(() => {
interval(1).subscribe(() => {
// somo code
})
});
}
}
Для решения этой задачи можно использовать оператор tuiZonefree из пакета @taiga-ui/cdk:
import { Injectable, NgZone } from "@angular/core";
import { tuiZonefree } from "@taiga-ui/cdk";
@Injectable()
export class SomeService {
constructor(private ngZone: NgZone){
interval(1).pipe(tuiZonefree(ngZone)).subscribe()
}
}
Но есть нюанс. Любая задача должна обязательно прерываться при разрушении приложения, иначе можно поймать утечку памяти (см. проблему № 7). Также нужно понимать, что выведенные из зоны задачи не будут запускать обнаружение изменений.2. Нет кэша «из коробки»СитуацияПользователь загружает главную страницу сайта. Сервер запрашивает данные для главной и рендерит ее, потратив на это 2 секунды. Затем пользователь уходит с главной в дочерний раздел. Потом пытается вернуться обратно и ждет те же 2 секунды, что и в первый раз.Если допустить, что данные, от которых зависит рендер главной, не поменялись, то получается, что html с этим набором уже рендерился. И в теории мы можем переиспользовать html, полученный ранее.Возможное решениеНа помощь приходят разные техники кэширования. Мы рассмотри две: in-memory cache и браузерный кэш.Браузерный кэш. При использовании браузерного кэша все сводится к установке правильных заголовков ответа на сервере. В них указывается время жизни кэша и политика кэширования:
Cache-Control: max-age=31536000
Этот вариант подойдет для неавторизованной зоны и при наличии долго не меняющихся данных.Более подробно о браузерном кэше можно почитать тут.In-memory cache. In-memory cache можно использовать как для отрендеренных страниц, так и для API-запросов в самом приложении. Обе возможности предоставляет пакет @ngx-ssr/cache.Для кэширования API-запросов и на сервере, и в браузере добавьте модуль NgxSsrCacheModule в AppModule.Свойство maxSize отвечает за максимальный размер кэша. Значение 50 говорит о том, что кэш будет содержать не больше 50 последних GET-запросов, совершенных из приложения.Свойство maxAge отвечает за срок хранения кэша, указывается в миллисекундах.
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { NgxSsrCacheModule } from '@ngx-ssr/cache';
import { environment } from '../environments/environment';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
NgxSsrCacheModule.configLruCache({ maxAge: 10 * 60_000, maxSize: 50 }),
],
bootstrap: [AppComponent],
})
export class AppModule {}
Можно пойти дальше и кэшировать сам html.Например, все в том же пакете @ngx-ssr/cache есть сабмодуль @ngx-ssr/cache/express. Он импортирует единственную функцию withCache. Функция представляет собой обертку над движком рендера.
import { ngExpressEngine } from '@nguniversal/express-engine';
import { LRUCache } from '@ngx-ssr/cache';
import { withCache } from '@ngx-ssr/cache/express';
server.engine(
'html',
withCache(
new LRUCache({ maxAge: 10 * 60_000, maxSize: 100 }),
ngExpressEngine({
bootstrap: AppServerModule,
})
)
);
3. Ошибки на сервере типа ReferenceError: localStorage is not definedСитуацияРазработчик обращается к localStorage прямо в теле сервиса. Он достает из локального хранилища браузера данные по ключу. Но на сервере этот код падает с ошибкой: ReferenceError: localStorage is not defined.Почему так происходитПри запуске Angular-приложения на сервере в глобальном пространстве отсутствует привычный браузерный API. Например, в node.js нет глобального объекта document. Но его можно получить по токену DOCUMENT через DI.Возможное решениеНе используйте браузерное API через глобальное пространство. Для этого есть DI. Через DI можно подменять или выключать браузерные имплементации для их безопасного использования на сервере.Для решения этой проблемы можно обратиться к Web APIs for Angular.Например:
import {Component, Inject, NgModule} from '@angular/core';
import {LOCAL_STORAGE} from '@ng-web-apis/common';
@Component({...})
export class SomeComponent {
constructor(@Inject(LOCAL_STORAGE) localStorage: Storage) {
localStorage.getItem('key');
}
}
В примере выше использован токен LOCAL_STORAGE из пакета @ng-web-apis/common. Но при запуске этого кода на сервере мы получим ошибку из описания. Просто добавьте UNIVERSAL_LOCAL_STORAGE из пакета @ng-web-apis/universal в провайдеры AppServerModule — и по токен LOCAL_STORAGE вы будете получать имплементацию localStorage для сервера.
import { NgModule } from '@angular/core';
import {
ServerModule,
} from '@angular/platform-server';
import { AppModule } from './app.module';
import { AppComponent } from './app.component';
import { UNIVERSAL_LOCAL_STORAGE } from '@ngx-ssr/timeout';
@NgModule({
imports: [
AppModule,
ServerModule,
],
providers: [UNIVERSAL_LOCAL_STORAGE],
bootstrap: [AppComponent],
})
export class AppServerModule {}
4. Неудобное разделение логикиСитуацияЕсли необходимо рендерить определенный блок только в браузере, то нужно написать примерно следующий код:
@Component({
selector: 'ram-root',
template: '<some-сomp *ngIf="isServer"></some-сomp>',
styleUrls: ['./app.component.less'],
})
export class AppComponent {
isServer = isPlatformServer(this.platformId);
constructor(@Inject(PLATFORM_ID) private platformId: Object){}
}
Компонент должен получить PLATFORM_ID, понять целевую платформу и добавить публичное свойство класса. Это свойство будет использовано в шаблоне в связке с директивой ngIf.Возможное решениеC помощью структурных директив и DI можно сильно упростить вышеописанный механизм.Для начала завернем в токен определение сервера.
export const IS_SERVER_PLATFORM = new InjectionToken<boolean>('Is server?', {
factory() {
return isPlatformServer(inject(PLATFORM_ID));
},
});
Создаем структурную директиву с использованием токена IS_SERVER_PLATFORM с одной простой задачей: рендерить компонент только на сервере.
@Directive({
selector: '[ifIsServer]',
})
export class IfIsServerDirective {
constructor(
@Inject(IS_SERVER_PLATFORM) isServer: boolean,
templateRef: TemplateRef<any>,
viewContainer: ViewContainerRef
) {
if (isServer) {
viewContainer.createEmbeddedView(templateRef);
}
}
}
Аналогично выглядит код для директивы IfIsBowser.Теперь рефакторим компонент.
@Component({
selector: 'ram-root',
template: '<some-сomp *ifIsServer"></some-сomp>',
styleUrls: ['./app.component.less'],
})
export class AppComponent {}
Из компонента удалены лишние свойства. Шаблон компонента стал немного проще. Такие директивы помогут декларативно скрывать и отображать контент в зависимости от платформы. Чтобы не таскать реализацию токенов и директив между проектами, мы собрали их в пакет @ngx-ssr/platform.5. Memory leakСитуацияСервис при инициализации запускает interval и выполняет некоторые действия.
import { Injectable, NgZone } from "@angular/core";
import { interval } from "rxjs";
@Injectable()
export class LocationService {
constructor(ngZone: NgZone) {
ngZone.runOutsideAngular(() => interval(1000).subscribe(() => {
...
}));
}
}
Этот код не влияет на стабильность приложения, но при разрушении приложения на сервере колбэк, переданный в subscribe, продолжит вызываться. Каждый запуск приложения на сервере оставит за собой артефакт в виде интервала. А это потенциальная утечка памяти.Возможное решениеВ нашем случае проблема решается использованием хука ngOnDestoroy. Он работает как для компонентов, так и для сервисов. Нам нужно лишь сохранить подписку и завершить ее при дестрое сервиса. Есть много техник по отписке, но здесь мы приведем лишь одну:
import { Injectable, NgZone, OnDestroy } from "@angular/core";
import { interval, Subscription } from "rxjs";
@Injectable()
export class LocationService implements OnDestroy {
private subscription: Subscription;
constructor(ngZone: NgZone) {
this.subscription = ngZone.runOutsideAngular(() =>
interval(1000).subscribe(() => {})
);
}
ngOnDestroy(): void {
this.subscription.unsubscribe();
}
}
6. Нельзя прервать рендерСитуацияМы перехватываем критическую ошибку. Дальнейший рендеринг и ожидание стабильности не имеют смысла. Нужно прервать процесс и отдать клиенту дефолтный index.html.Почему так происходитЕще раз обратимся к моменту рендеринга приложения. Он происходит при наступлении стабильности приложения. Мы можем ускорить наступление стабильности решениями из проблемы № 1. Но что, если мы хотим прервать процесс рендеринга при первой перехваченной ошибке? А если мы хотим установить лимит времени на попытку отрендерить приложение?Возможное решениеРешения этой проблемы сейчас не существует.7. Нет регидрацииСитуацияПри загрузке в браузере пользователя отображается страница, полученная с сервера, на мгновение мелькает белый экран, а затем приложение начинает функционировать и выглядеть нормально.Почему так происходитAngular не умеет переиспользовать то, что он отрендерил на сервере. Он вырезает весь html из корневого элемента и начинает рисовать все заново.Возможное решениеЕго все еще не существует. Но есть надежда, что решение все же будет. В roadmap Angular Universal есть пункт «Full client rehydration strategy that reuses DOM elements/CSS rendered on the server».ВыводыФактически Angular Universal — единственное поддерживаемое и самое распространенное решение для рендера вашего приложения на сервере. Сложность интеграции в существующее приложение во многом зависит от разработчика.Все еще есть нерешенные проблемы, из-за которых лично я не могу назвать Angular Universal решением, готовым к продакшену. Он подойдет для лендингов и статичных страниц, но на сложных приложениях можно собрать ворох проблем, решение которых разобьется о моргание страницы из-за отсутствия регидрации.
===========
Источник:
habr.com
===========
Похожие новости:
- [JavaScript] Квест для прогеров на Java script
- [C#, Serverless] Создание превью картинок в объектном хранилище с помощью Yandex Cloud Functions
- [JavaScript, Программирование] Как написать интерфейс пользователя (UI) PlayStation 5 на JavaScript (перевод)
- [Информационная безопасность, Системное администрирование, Софт, IT-компании] Microsoft выпустила инструмент для устранения уязвимости в серверах Exchange
- [Информационная безопасность, JavaScript] JavaScript prototype pollution: практика поиска и эксплуатации
- [Хакатоны, Карьера в IT-индустрии] Хакатон: как студенты Сколтеха обучали ассистента Олега финансовым играм
- [JavaScript] Контролируем JavaScript импорты с помощью Import maps
- [JavaScript, Программирование, VueJS] Сделаем худший Vue.js в мире (перевод)
- [JavaScript, Программирование] Основы JavaScript: почему вы должны знать, как работает JS-движок (перевод)
- [SQL, Microsoft SQL Server] Пожалуйста, прекратите использовать антипаттерн UPSERT (SQL Server) (перевод)
Теги для поиска: #_javascript, #_node.js, #_angular, #_typescript, #_angular, #_angular_universal, #_ssr, #_server, #_express.js, #_blog_kompanii_tinkoff (
Блог компании TINKOFF
), #_javascript, #_node.js, #_angular, #_typescript
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 12:24
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Angular Universal — это опенсорсный проект, который расширяет функциональность @angular/platform-server. Он делает возможным Server Side Rendering в Angular.Angular Universal поддерживает несколько бэкендов: Еще один пакет Socket Engine — это фреймворк-агностик, который теоретически позволяет подключать к SSR-серверу любой бэкенд. В этой статье мы разберем проблемы, с которыми мы столкнулись при разработке реального приложения с Angular Universal и Express, и их решения.Как работает Angular UniversalДля рендеринга на сервере Angular использует имплементацию DOM для node.js — domino. На каждый GET-запрос domino создает объект, аналогичный браузерному документу.В контексте этого объекта Angular инициализирует приложение. Приложение делает необходимые запросы на бэкенд, выполняет различные асинхронные задачи и мутирует DOM, находясь в серверном окружении. Затем движок рендера сериализует DOM в строку и отдает эту строку серверу. Сервер отправляет HTML в качестве ответа на GET-запрос. Angular-приложение на сервере после рендера разрушается.Проблемы SSR в Angular1. Бесконечная загрузка страницыСитуацияПользователь открывает страницу вашего сайта и видит белый экран. Другими словами, время до первого байта стремится к бесконечности. Браузер очень хочет получить ответ от сервера, но запрос завершается таймаутом.Почему так происходитСкорее всего, проблема кроется в специфичном для Angular механизме SSR. Прежде чем понять, в какой момент происходит рендеринг страницы, дадим определение Zone.js и ApplicationRef.Zone.js — это инструмент, который позволяет отслеживать асинхронные операции. С его помощью Angular создает свою зону и запускает в ней приложение. При завершении каждой асинхронной операции в зоне Angular запускается обнаружение изменений.ApplicationRef — это ссылка на запущенное приложение (docs). Из всего функционала этого класса нас интересует свойство ApplicationRef#isStable. Это Observable, который испускает boolean. isStable равен true тогда, когда в зоне Angular отсутствуют выполняющиеся асинхронные задачи, а false — когда таких задач нет.Из этого следует, что стабильность приложения — это состояние приложения, которое зависит от наличия асинхронных задач в зоне AngularИтак, в момент первого наступления стабильности Angular рендерит текущее состояние приложения и дестроит платформу. А платформа дестроит приложение.Теперь мы можем сделать предположение, что пользователь пытается открыть приложение, которое не может достичь стабильности. setInterval, rxjs.interval или любая другая рекурсивная асинхронная операция, запущенная в зоне Angular, сделают наступление стабильности невозможным. HTTP-запросы так же влияют на стабильность. Затянувшийся запрос на сервере оттягивает момент рендеринга страницы.Возможное решениеЧтобы избежать ситуации с долгими запросами, используйте оператор timeout: import { timeout, catchError } from 'rxjs/operators';
import { of } from 'rxjs/observable/of'; http.get('https://example.com') .pipe( timeout(2000), catchError(e => of(null)) ).subscribe()
import { NgModule } from '@angular/core';
import { ServerModule, } from '@angular/platform-server'; import { AppModule } from './app.module'; import { AppComponent } from './app.component'; import { NgxSsrTimeoutModule } from '@ngx-ssr/timeout'; @NgModule({ imports: [ AppModule, ServerModule, NgxSsrTimeoutModule.forRoot({ timeout: 500 }), ], bootstrap: [AppComponent], }) export class AppServerModule {} import { Injectable, NgZone } from "@angular/core";
@Injectable() export class SomeService { constructor(private ngZone: NgZone){ this.ngZone.runOutsideAngular(() => { interval(1).subscribe(() => { // somo code }) }); } } import { Injectable, NgZone } from "@angular/core";
import { tuiZonefree } from "@taiga-ui/cdk"; @Injectable() export class SomeService { constructor(private ngZone: NgZone){ interval(1).pipe(tuiZonefree(ngZone)).subscribe() } } Cache-Control: max-age=31536000
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; import { NgxSsrCacheModule } from '@ngx-ssr/cache'; import { environment } from '../environments/environment'; @NgModule({ declarations: [AppComponent], imports: [ BrowserModule, NgxSsrCacheModule.configLruCache({ maxAge: 10 * 60_000, maxSize: 50 }), ], bootstrap: [AppComponent], }) export class AppModule {} import { ngExpressEngine } from '@nguniversal/express-engine';
import { LRUCache } from '@ngx-ssr/cache'; import { withCache } from '@ngx-ssr/cache/express'; server.engine( 'html', withCache( new LRUCache({ maxAge: 10 * 60_000, maxSize: 100 }), ngExpressEngine({ bootstrap: AppServerModule, }) ) ); import {Component, Inject, NgModule} from '@angular/core';
import {LOCAL_STORAGE} from '@ng-web-apis/common'; @Component({...}) export class SomeComponent { constructor(@Inject(LOCAL_STORAGE) localStorage: Storage) { localStorage.getItem('key'); } } import { NgModule } from '@angular/core';
import { ServerModule, } from '@angular/platform-server'; import { AppModule } from './app.module'; import { AppComponent } from './app.component'; import { UNIVERSAL_LOCAL_STORAGE } from '@ngx-ssr/timeout'; @NgModule({ imports: [ AppModule, ServerModule, ], providers: [UNIVERSAL_LOCAL_STORAGE], bootstrap: [AppComponent], }) export class AppServerModule {} @Component({
selector: 'ram-root', template: '<some-сomp *ngIf="isServer"></some-сomp>', styleUrls: ['./app.component.less'], }) export class AppComponent { isServer = isPlatformServer(this.platformId); constructor(@Inject(PLATFORM_ID) private platformId: Object){} } export const IS_SERVER_PLATFORM = new InjectionToken<boolean>('Is server?', {
factory() { return isPlatformServer(inject(PLATFORM_ID)); }, }); @Directive({
selector: '[ifIsServer]', }) export class IfIsServerDirective { constructor( @Inject(IS_SERVER_PLATFORM) isServer: boolean, templateRef: TemplateRef<any>, viewContainer: ViewContainerRef ) { if (isServer) { viewContainer.createEmbeddedView(templateRef); } } } @Component({
selector: 'ram-root', template: '<some-сomp *ifIsServer"></some-сomp>', styleUrls: ['./app.component.less'], }) export class AppComponent {} import { Injectable, NgZone } from "@angular/core";
import { interval } from "rxjs"; @Injectable() export class LocationService { constructor(ngZone: NgZone) { ngZone.runOutsideAngular(() => interval(1000).subscribe(() => { ... })); } } import { Injectable, NgZone, OnDestroy } from "@angular/core";
import { interval, Subscription } from "rxjs"; @Injectable() export class LocationService implements OnDestroy { private subscription: Subscription; constructor(ngZone: NgZone) { this.subscription = ngZone.runOutsideAngular(() => interval(1000).subscribe(() => {}) ); } ngOnDestroy(): void { this.subscription.unsubscribe(); } } =========== Источник: habr.com =========== Похожие новости:
Блог компании TINKOFF ), #_javascript, #_node.js, #_angular, #_typescript |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 12:24
Часовой пояс: UTC + 5