Как реализовать универсальное (SSR) приложение с Angular Universal?

Angular Universal — это технология серверного рендеринга (SSR) в Angular, позволяющая рендерить приложение на сервере перед отправкой HTML-контента клиенту. Это ускоряет первую отрисовку, улучшает SEO и повышает производительность на слабых устройствах. SSR особенно полезен для приложений, которые должны быть индексируемыми поисковыми системами или быстро загружаться при медленном соединении.

1. Принцип работы Angular Universal

  • Пользователь делает запрос к серверу (например, GET /home).

  • Сервер запускает Node.js-приложение с Angular Universal.

  • Приложение рендерится в HTML на сервере.

  • Готовый HTML-контент отправляется пользователю.

  • После загрузки на клиенте Angular "оживляет" приложение (hydration), подключая обработчики событий и состояния.

2. Установка Angular Universal

Для преобразования существующего Angular-приложения в SSR-версию используется CLI:

ng add @nguniversal/express-engine

Команда:

  • добавит зависимости (@nguniversal/express-engine, ts-loader, express);

  • создаст необходимые файлы (server.ts, main.server.ts, app.server.module.ts);

  • обновит angular.json и package.json для поддержки SSR.

3. Основные файлы SSR-структуры

main.server.ts — entry point серверной сборки:

```python
export { AppServerModule } from './app/app.server.module';

**app.server.module.ts**  серверный модуль:  
<br/>```python
@NgModule({
imports: \[
AppModule,
ServerModule,
\],
bootstrap: \[AppComponent\],
})
export class AppServerModule {}

server.ts — Node.js сервер на Express:

import 'zone.js/node';
import express from 'express';
import { ngExpressEngine } from '@nguniversal/express-engine';
import { AppServerModule } from './src/main.server';
const app = express();
app.engine('html', ngExpressEngine({
bootstrap: AppServerModule,
}));
app.set('view engine', 'html');
app.set('views', 'dist/my-app/browser');
app.get('\*.\*', express.static('dist/my-app/browser'));
app.get('\*', (req, res) => {
res.render('index', { req });
});
app.listen(4000, () => console.log('Server running on http://localhost:4000'));

4. Сборка SSR-приложения

Создаются два бандла:

Browser (SPA, как обычно):

```python
ng build --prod

**Server** (Node.js):  
<br/>```python
ng run my-app:server:production

Можно использовать скрипт:

"scripts": {
"build:ssr": "ng build --prod && ng run my-app:server:production",
"serve:ssr": "node dist/my-app/server/main.js"
}

5. Использование API браузера

Поскольку SSR выполняется в среде Node.js, нельзя напрямую использовать API браузера (window, document, navigator). Необходимо оборачивать такие вызовы:

if (typeof window !== 'undefined') {
// безопасный доступ к window
}

Или через Angular PLATFORM_ID:

import { isPlatformBrowser } from '@angular/common';
constructor(@Inject(PLATFORM_ID) private platformId: Object) {
if (isPlatformBrowser(this.platformId)) {
// код выполняется только в браузере
}
}

6. Обработка асинхронных данных при SSR

Angular Universal дожидается выполнения всех асинхронных задач в шаблоне перед рендерингом. Например, Observable в *ngIf с async pipe будет автоматически разрешён на сервере.

Однако асинхронные операции вне шаблона (например, в ngOnInit) должны быть синхронизированы через APP_INITIALIZER, TransferState или resolve в маршрутах.

7. Кэширование HTML (предварительный рендеринг)

Angular Universal поддерживает предрендеринг (static prerender):

ng run my-app:prerender

Это генерирует статические HTML-файлы, которые можно отдавать как обычные страницы. Подходит для сайтов с фиксированным маршрутом.

8. TransferState API

Позволяет передать данные с сервера на клиент, чтобы не выполнять один и тот же HTTP-запрос дважды (на сервере и в браузере).

import { TransferState, makeStateKey } from '@angular/platform-browser';
const STATE_KEY = makeStateKey&lt;any&gt;('some-data');
constructor(private transferState: TransferState) {}
ngOnInit() {
if (this.transferState.hasKey(STATE_KEY)) {
this.data = this.transferState.get(STATE_KEY, null);
} else {
this.http.get('/api/data').subscribe(data => {
this.data = data;
this.transferState.set(STATE_KEY, data);
});
}
}

9. Angular CLI и Angular.json

После добавления Universal Angular CLI создаёт 2 цели сборки:

  • "browser" — обычная SPA;

  • "server" — серверная сборка;

  • "ssr" — сборка и запуск SSR;

  • "prerender" — статический рендер.

10. SSR и маршрутизация

SSR поддерживает RouterModule, включая resolve, lazy-loading, guards. Однако нужно учитывать:

  • Ленивая загрузка требует предварительной инициализации модулей.

  • CanActivate и CanLoad могут влиять на рендер.

11. SEO преимущества

Angular Universal позволяет:

  • Индексацию страниц поисковыми системами;

  • Быструю первую отрисовку (First Contentful Paint);

  • Создание мета-тегов на сервере (Title, Meta, Link).

constructor(private title: Title, private meta: Meta) {}
ngOnInit() {
this.title.setTitle('My Page Title');
this.meta.updateTag({ name: 'description', content: 'Page description' });
}

12. Производственные особенности

  • SSR требует собственного Node.js-сервера.

  • Можно использовать Express, NestJS, Firebase Functions, Vercel, Cloudflare Workers и другие среды.

  • Возможна интеграция с CDN, кэшами, Redis, и промежуточным рендерингом (islands architecture).

13. Ошибки SSR

Наиболее частые ошибки:

  • Использование window, document, localStorage без проверки окружения.

  • Отсутствие TransferState, приводящее к дублирующимся запросам.

  • Неправильные пути в Express или static middleware.

  • Неожиданные асинхронные операции, вызывающие пустой HTML.

14. Инструменты и отладка

  • Логируйте запросы и рендер в server.ts.

  • Используйте DevTools Lighthouse для замеров FCP, TTI, SEO.

  • Проверяйте результат рендеринга в HTML-ответе (view-source:).

  • Добавляйте логирование в main.server.ts, AppServerModule, ngExpressEngine.

Таким образом, Angular Universal предоставляет полную поддержку SSR в Angular-приложениях, включая рендеринг на сервере, передачу состояния, SEO-оптимизацию и динамическую маршрутизацию.