Оптимизируем Angular 2+ под SEO


8 шагов, чтобы сделать ваше SPA (одностраничное приложение) на Angular доступным для SEО.

Перевод статьи: Server Side Rendering SSR in Angular 5 the simplest and quickest SSR

Большинство веб - приложений выполняют рендер страницы на клиентской стороне (СSR - Client Side Render), что означает все необходимые коды (HTML, CSS, JavaScript) комплектуются вместе и загружаются в браузер клиента один раз. В зависимости от URl браузера, фреймворки такие как Angular, React, Vue etc. использующие этот код показывают представления по манипуляции DOM и ответах сетевых запросов. Это значительно улучшает пользовательский интерфейс, но при определенной стоимости.

С помощью такого подхода, пользователь должен ждать большое количество времени до загрузки всех необходимых файлов, которые состоят из кода фреймворка и кода приложения. До загрузки тех файлов, пользователь принудительно видит пустую страницу, Поскольку все значимые представления генерируются на стороне клиента, поисковые системы и социальные боты , которые не имеют возможности прочтения JavaScript, только получают пустую страницу index.html. Следовательно нет индексирования поисковыми системами и предварительного просмотра социальных фрагментов. Для разрешения этих проблем, нам нужно рендерить HTML код на сервере, когда пользователь или боты создают запрос на страницу.

В Angular в основном страница index.html служит из express сервера для всех URL путей и эта страница index.html передается через движок express представления, который внедряет HTML в <app-root></app-root>, Исходя из текущего маршрута и компонента для этого маршрута.

Итак, в этой статье, я буду объяснять шаг за шагом реализацию рендера на серверной стороне (SSR - Server Side Render) с некоторыми примерами. Убедитесь, что установлена версия Angular CLI 1.6+. Если вы уже создали новый Angular приложение, тогда лучше установить новую версию Angular CLI с помощью команды:

npm install -g @angular/cli@latest


Первое, нам нужно создать angular приложение с маршрутизацией. Маршрутизация не является обязательной, но просто показать вам как рендер серверной стороны работает на различных URL, мы начинаем импортировать модуль маршрутизации. Создать angular приложение с автоматически сгенерированным модулем маршрутизации и с поддержкой SCSS стилей, используйте команду ниже.

ng new angular-ssr-example --style scss --routing

Это создаст папку angular-ssr-example, где будет находиться наш angular код. Откройте этот проект в вашем редакторе кода.

Давайте поймем, как рендер на стороне клиента работает на практике. Традиционно, мы использовали всю папку dist (технически, только index.html файл из папки dist), который содержит скомпилированные файлы нашего Angular приложения. Но сейчас мы собираемся создать express-сервер который обслуживает index.html файл из папки dist-browser.

Мы собираемся создать два новых Angular модуля, один для браузера и один для сервера, которые импортируются в наш app module сгенерированный Angular CLI. Эти модули будут генерировать собственные папки распределения. Express-сервер будет использовать папку распространения серверного модуля dist-server для внедрения соответствующего HTML (на основе URL-адреса запроса) внутри app-root блока index.html из браузерной папки распределения dist-browser и возвращать этот файл. Позже браузер имеет полностью отрисованную HTML, но приложение будет переотрисовываться (при загрузке) один раз всех необходимых JavaScript и CSS файлов.

Шаг 1: Подготовка приложения

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

ng g c components/home --module app.module.ts
ng g c components/about --module app.module.ts

Эти команды будут генерировать home и about компоненты внутри папки src/app/components. Затем нам нужно импортировать эти компоненты внутрь модуля маршрутизации, который будет app-routing.module.ts внутри папки src/app.

// app-routing.module.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AboutComponent } from './components/about/about.component';
import { HomeComponent } from './components/home/home.component';
const routes: Routes = [
    { path: '', component: HomeComponent },
    { path: 'about', component: AboutComponent },
    { path: '**', redirectTo: '/' },
];
@NgModule({
    imports: [RouterModule.forRoot(routes)],
    exports: [RouterModule]
})
export class AppRoutingModule { }

Поскольку, мы реализовали различные компоненты для различных маршрутов, нам нужно добавить router-outlet внутри app.component.html. Давайте так же добавим некоторые CSS стили в app компонент.

<!-- app.component.html -->

<h1>Welcome to Angular SSR Example</h1>

<div class="nav">
    <a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">Home</a>
    <a routerLink="/about" routerLinkActive="active">About</a>
</div>

<!-- views -->
<router-outlet></router-outlet>


// app.component.scss

  :host {
    display: block;
    text-align: center;
    >.nav {    
      padding: 15px;
      >a {
        display: inline-block;
        margin: 0 5px;
        padding: 5px 10px;
        text-decoration: none;
        color: #333;
        font-weight: 500;
        border: 2px solid grey;
        &:hover {
          border-color: black;
        }
        &.active {
          border-color: blue;
        }
      }
    }
  }

Я думаю, мы также должны сделать наше приложение красивым, установив хороший встроенный шрифт внутри файла styles.scss.

html,
body {
  padding: 0;
  margin: 0;
  font-size: 14px;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}

Вы можете добавить некоторую функциональность к home и about компонентам.

<!--  components/home.component.html -->

<h2>Welcome to home section</h2>

<img src="../../../assets/angular-logo.png" alt="">


// components/home.component.ts

import { Component, OnInit } from '@angular/core';
import { Title, Meta } from '@angular/platform-browser';

@Component({
    selector: 'app-home',
    templateUrl: './home.component.html',
    styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit {
    constructor(
        private title: Title,
        private meta: Meta
    ) { }

    ngOnInit() {
        this.title.setTitle('Home / Angular SSR');
        this.meta.updateTag({
            'description': 'Welcome to home section'
        });
    }
}

В home компоненте мы имеем добавленное простое сообщение и Angular логотип. Из home.component.ts мы имеем измененное meta title и meta description страницы. Поскольку, это посадочное представление, мы ожидаем название страницы для маршрута / - упомянутое выше.

<!--  components/about.component.html -->

<h2>Welcome to about section</h2>
<ul>
    <li *ngFor="let user of users">
        {{ user.name }} ({{ user.email }})
    </li>
</ul>


//  components/about.component.ts

import { Component, OnInit } from '@angular/core';
import { Title, Meta } from '@angular/platform-browser';
import { HttpClient } from '@angular/common/http';

@Component({
    selector: 'app-about',
    templateUrl: './about.component.html',
    styleUrls: ['./about.component.scss']
})
export class AboutComponent implements OnInit {
    public users: any = [];

    constructor(
        private title: Title,
        private meta: Meta,
        private http: HttpClient
    ) { }

    ngOnInit() {
        this.title.setTitle('About / Angular SSR');
        this.meta.updateTag({
            'description': 'Welcome to about section'
        });
        this.http.get('https://jsonplaceholder.typicode.com/users')
        .subscribe((users) => {
            this.users = users;
        }, (err) => {
            console.log(err);
        });
    }
}

По аналогии с home в about компоненте мы имеем измененные мета теги, но мы так же импортируем данные пользователей из удаленного сервера. Эти данные будут динамически внедряться внутрь ul элементов на стороне клиента, но мы ожидаем, что он будет доступен (уже отображен) в ответе с сервера.

Поскольку, мы готовы с нашим приложением на данный момент, мы можем посмотреть это запустив команду ng serve. Это предварительный просмотр будет доступным через сервер localhost порт 4200 и будет выглядеть следующим образом:

Убедитесь, что модуль HttpClientModule импортирован в app.module.ts, иначе приложение не будет работать.

Нажмите на различные кнопки и посмотрите изменяются ли представления, что должно быть. Вы можете так же увидеть изменение мета тегов. Но вы видите источник, используя параметр ctrl + u или view page source в контекстном меню браузера Chrome, вы можете видеть только файл index.html с пустым корнем app-root.

<!doctype html><html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Home / Angular SSR</title>
    <meta name="description" content="Welcome to home section">
    <base href="/">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="icon" type="image/x-icon" href="favicon.ico">
  </head>
  <body>
    <app-root></app-root>
    <script type="text/javascript" src="inline.bundle.js"></script>
    <script type="text/javascript" src="polyfills.bundle.js"></script>
    <script type="text/javascript" src="styles.bundle.js"></script>
    <script type="text/javascript" src="vendor.bundle.js"></script>
    <script type="text/javascript" src="main.bundle.js"></script>
  </body>
</html>

На данный момент мы знаем, что это то, что видят поисковые системы и боты социальных сетей, когда они посещают наш веб-сайт. Поскольку, компонент получается внедренный внутрь HTML <app-root></app-root> на клиентской стороне динамически, рендеринг на стороне сервера просто означает, внедрение его на самом сервере.

Шаг 2: Создание браузерного и серверного модуля

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

Мы уже имеем app.module.ts, который был сгенерирован Angular CLI. Измените его так, как показано ниже:

// Native
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { CommonModule } from '@angular/common';

// Routing
import { AppRoutingModule } from './app-routing.module';

// Components
import { AppComponent } from './app.component';
import { AboutComponent } from './components/about/about.component';
import { HomeComponent } from './components/home/home.component';

@NgModule({
    declarations: [
        AppComponent,
        AboutComponent,
        HomeComponent
    ],
    imports: [
        CommonModule,
        AppRoutingModule,
        HttpClientModule
    ],
    providers: []
})
export class AppModule { }

Мы в основном удалили свойство bootstrap, следовательно модуль приложения не будет загружать наше приложение.

Сейчас, мы собираемся создать browser.app.module.ts файл в той же директории, что и app.module.ts.

// Native
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

// Modules
import { AppModule } from './app.module';

// Components
import { AppComponent } from './app.component';

@NgModule({
    imports: [
        AppModule,
        BrowserModule.withServerTransition({ appId: 'ssr-example' }),
    ],
    bootstrap: [AppComponent]
})
export class BrowserAppModule { }

Этот модуль будет использоваться браузером и будет загружать приложение на стороне клиента. BrowserModule.withServerTransition( {appId: 'ssr-example' } ) строка импортирует BrowserModule с некоторой конфигурацией, которая говорит Angular что приложение на стороне сервера. Свойство appId - имя твоего приложение которое должно быть уникальным, но вы можете изменить его на что угодно.

Аналогично, нам так же нужен один модуль для сервера, который будет server.app.module.ts в той же директории с кодом ниже:

// Native
import { BrowserModule } from '@angular/platform-browser';
import { ServerModule } from '@angular/platform-server';
import { NgModule } from '@angular/core';

// Modules
import { AppModule } from './app.module';

// Routing
import { AppRoutingModule } from './app-routing.module';

// Components
import { AppComponent } from './app.component';

@NgModule({
    imports: [
        AppModule,
        BrowserModule.withServerTransition({ appId: 'ssr-example' }),
        ServerModule,
    ],
    bootstrap: [AppComponent]
})
export class ServerAppModule { }

Модуль сервера выглядит точно так же, как и браузерный модуль, но нам так же нужно импортировать ServerModule из @angular/platform-server модуля. Итак, если у вас нет этого модуля, обязательно установите его, используя команду ниже:

npm install --save @angular/platform-server

Шаг 3: Создание отдельных точек входа

Поскольку, мы готовы с модулями, нам нужны различные точки входа для этих модулей. Как обычно, по умолчанию точки входа генерируются Angular CLI в app/main.ts но нам нужно разделить точки входа, одна из браузера и одна из сервера.

Следовательно, переименовать main.ts в browser.main.ts и это должно быть, как показано ниже:

// Native
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { environment } from './environments/environment';

// Modules
import { BrowserAppModule } from './app/browser.app.module';

if (environment.production) {
    enableProdMode();
}
platformBrowserDynamic().bootstrapModule(BrowserAppModule)
    .catch(err => console.log(err));

Аналогично, точка входа для серверного модуля будет расположена в server.main.ts в той же папке c содержимым ниже:

import { enableProdMode } from '@angular/core';
export { ServerAppModule } from './app/server.app.module';

enableProdMode();

Шаг 4: Создание файлов tsconfig.json

У нас уже есть tsconfig.app.json файл внутри папки src. Это typescript конфигурация генерирующая Angular CLI для модуля приложения. Так как, мы имеем два различных модуля для различных ролей, нам нужно иметь две различные конфигурации.

Переименуйте tsconfig.app.json в browser.config.app.json который расширит typescript конфигурацию для браузерного модуля.

{
    "extends": "../tsconfig.json",
    "angularCompilerOptions": {
        "entryModule": "./app/browser.app.module#BrowserAppModule"
    },
    "compilerOptions": {
        "outDir": "../out-tsc/browser",
        "baseUrl": "./",
        "module": "es2015",
        "types": []
    },
    "exclude": [
        "test.ts",
        "**/*.spec.ts"
    ]
}

Так же создайте server.config.app.json файл с содержимым ниже, который расширит typescript конфигурацию для серверного модуля.

{
    "extends": "../tsconfig.json",
    "angularCompilerOptions": {
        "entryModule": "./app/server.app.module#ServerAppModule"
    },
    "compilerOptions": {
        "outDir": "../out-tsc/server",
        "baseUrl": "./",
        "module": "commonjs",
        "types": []
    },
    "exclude": [
        "test.ts",
        "**/*.spec.ts"
    ]
}

Шаг 5: Модификация файла angular-cli.json

Мы модифицировали конфиг typescript файлов для использования различных модулей при создании разных файлов распределения. Теперь нам нужно изменить файл angular-cli.json для добавления дополнительной записи приложения для серверного модуля. Нам так же нужно изменить существующую запись приложения для переименования и новых файлов.

Мы имеем два приложения потому что там два модуля загружающих приложение.

{
    "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
    "project": {
        "name": "angular-ssr-example"
    },
    "apps": [
        {
            "platform": "browser",
            "root": "src",
            "outDir": "dist-browser",
            "assets": [
                "assets",
                "favicon.ico"
            ],
            "index": "index.html",
            "main": "browser.main.ts",
            "polyfills": "polyfills.ts",
            "test": "test.ts",
            "tsconfig": "browser.tsconfig.app.json",
            "testTsconfig": "tsconfig.spec.json",
            "prefix": "app",
            "styles": [
                "styles.scss"
            ],
            "scripts": [],
            "environmentSource": "environments/environment.ts",
            "environments": {
                "dev": "environments/environment.ts",
                "prod": "environments/environment.prod.ts"
            }
        },
        {
            "platform": "server",
            "root": "src",
            "outDir": "dist-server",
            "main": "server.main.ts",
            "tsconfig": "server.tsconfig.app.json",
            "testTsconfig": "tsconfig.spec.json",
            "environmentSource": "environments/environment.ts",
            "environments": {
                "dev": "environments/environment.ts",
                "prod": "environments/environment.prod.ts"
            }
        }
    ],
    "e2e": {
        "protractor": {
            "config": "./protractor.conf.js"
        }
    },
    "lint": [
        {
            "project": "src/tsconfig.app.json",
            "exclude": "**/node_modules/**"
        },
        {
            "project": "src/tsconfig.spec.json",
            "exclude": "**/node_modules/**"
        },
        {
            "project": "e2e/tsconfig.e2e.json",
            "exclude": "**/node_modules/**"
        }
    ],
    "test": {
        "karma": {
            "config": "./karma.conf.js"
        }
    },
    "defaults": {
        "styleExt": "scss",
        "component": {}
    }
}

Если вы видите запись приложения для конкретного сервера, у нас нет polyfills, styles, scripts и других записей. Это связано с тем, что наше серверное приложение ссылается на приложение-браузер, которое уже содержит эти ресурсы.

Шаг 6: Создание распределений

До сих пор, что мы сделали - это сказали Angular CLI о том, как создавать файлы распределения для браузерного и серверного приложения. Пришло время создавать сборки для этих приложений. Чтобы упростить измените ваш package.json файл, чтобы включить эти команды в сценарии.

"scripts": {
    "build:browser": "ng build --prod --app 0",
    "build:server": "ng build --prod --app 1 --output-hashing none",
    "build": "npm run build:browser && npm run build:server",
    "serve": "node server.js"
},

Чтобы собрать браузерное приложение, вам нужно использовать npm run build:browser и, чтобы создать серверное приложение, вам нужно использовать npm run build:server. Но как только вы закончите с некоторыми обновлениями в приложение, вы должны использовать только npm run build.

После запуска npm run build, вы должны увидеть папки dist-browser и dist-server в корне проекта.

Шаг 7: Создание express-сервера

Пришло время создать express-сервер, который будет рендерить HTML приложения на сервере. Создайте server.js файл внутри коря проекта с содержимым ниже:

// Angular requires Zone.js
require('zone.js/dist/zone-node');

const express = require('express');
const { ngExpressEngine } = require('@nguniversal/express-engine');

// create express app
const app = express();

// import server module bundle
var { ServerAppModuleNgFactory } = require('./dist-server/main.bundle');

// set up engine for .html file
app.engine('html', ngExpressEngine({
    bootstrap: ServerAppModuleNgFactory
}));

app.set('view engine', 'html');
app.set('views', 'dist-browser');

// server static files
app.use(express.static(__dirname + '/dist-browser', {
 index: false
}));

// return rendered index.html on every request
app.get('*', (req, res) => {
    res.render('index', { req, res });
    console.log(`new GET request at : ${req.originalUrl}`);
});

// start server and listen
app.listen(3000, () => {
    console.log('Angular server started on port 3000');
});

Обязательно установите @nguniversal/express-engine с помощью команды:

npm install --save @nguniversal/express-engine

Этот модуль необходим для внедрения подходящего HTML внутри app-root на основе маршрутов (URL-адреса запроса).

Помимо конкретного Angular кода в server.js, другие вещи должны быть довольно простыми, если вы являетесь разработчиком Node.js. Чтобы запустить сервер, используйте команду:

npm run serve

Это запустит HTTP сервер на порту 3000. Откройте ваш браузер на http://localhost:3000, который должен показывать HTML, отображаемый на сервере.

Там нет разницы между angular приложением без серверного рендера и с сервверным рендером, когда смотрим с клиентской стороны. Но если вы посмотрите исходники для маршрутов home и about, то вы можете увидеть изменения мета тегов и приложения в полной мере.

Теперь вы можете использовать server.js с pm2 или forever.js для работы в фоновом режиме навсегда.

Шаг 8: State Transfer

Если вы посетите http://localhost:3000/about страницу, тогда express-сервер будет отправлять HTTP запрос получения данных пользователей и возвращать от рисованное представление. На браузере так же запрос будет сделал повторно. Это не хорошо для пользовательского интерфейса и создаст множество проблем.

Именно в этом месте state transfer приходит на спасение. Мы может установить состояние приложения до того, как загрузится это приложение. Это выполняемо BrowserTransferStateModule и ServerTransferStateModule модулями.

Внутри browser.app.module.ts, импортируется BrowserTransferStateModule, который доступен из @angular/platform-browser. Внутри server.app.module.ts, импортируется ServerTransferStateModule модуль, который доступен из @angular/platform-server.

Эти модули помогают Angular переносить состояние загрузочного приложения на сервер, которые будут переданы в приложение, которое выполняется в браузере.

Теперь, следующий шаг не допустить HTTP запрос, если состояние приложения уже имеет ключ, который содержит некоторые данные. Если эти данные присутствуют, нам не нужно делать HTTP запрос. TransferState обеспечивает интерфейс сервиса для взаимодействия с состоянием приложения. Следовательно нам нужно внедрить это внутрь about.component.ts. Это доступно внутри platform-browser модуля.

import { Component, OnInit } from '@angular/core';
import { Title, Meta, TransferState, makeStateKey } from '@angular/platform-browser';
import { HttpClient } from '@angular/common/http';

// make state key in state to store usersconst STATE_KEY_USERS = makeStateKey('users');
@Component({
    selector: 'app-about',
    templateUrl: './about.component.html',
    styleUrls: ['./about.component.scss']
})
export class AboutComponent implements OnInit {
    public users: any = [];

    constructor(
        private title: Title,
        private meta: Meta,
        private http: HttpClient,
        private state: TransferState
    ) { }

    ngOnInit() {
        this.title.setTitle('About / Angular SSR');
        this.meta.updateTag({
            'description': 'Welcome to about section'
        });
        this.users = this.state.get(STATE_KEY_USERS, <any>[]);

        if (this.users.length == 0) {
            this.http.get('https://jsonplaceholder.typicode.com/users')
            .subscribe((users) => {
                this.users = users;
                this.state.set(STATE_KEY_USERS, <any>users);
            }, (err) => {
                console.log(err);
            });
        }
    }
}

TransferState обеспечивает get метод получения значения состояния. Первый параметр для метода get должен быть ключом в состоянии типа StateKey. Следовательно нужно создать ключ используя функцию makeStateKey, которая выполняется в строке номер 6. Второй параметр функции get является значением по умолчанию, если ключ не существует в состоянии, которое мы устанавливаем для пустого массива.

Позже мы проверяем, является ли this.users пустым массивом или нет, проверка выполняется в строке номер 31. Если массив пустой, тогда создаем HTTP запрос для получения данных пользователей и устанавливаем значение состояние, которое выполняется в строке номером 35. Позже, когда браузер загрузил приложение, данные пользователей будут уже в состояние и ему не придется делать другой запрос HTTP.

Когда серверное приложение загружается, это проверяет существование пользователей в состоянии, если это не происходит, тогда это создаст HTTP запрос. Ответ этого запроса сохраняется в состоянии, которое является ничем иным, как строкой JSON, встроенной в index.html внутри тега скрипта типа type = "application / json" и id = "{appId} -state", где appId - это идентификатор приложения установленное с помощью withServerTransition. Следовательно, когда браузер ищет состояние, он будет анализировать JSON в этом скрипте и генерировать состояние из него.

Давайте создадим приложение сервера и браузера и перезапустите express-сервер, используя команду ниже:

npm run build && npm run server

После перезагрузки сервера перейдите на страницу about и проверьте, Выполняет ли браузер сетевой запрос. Вы будете удивлены, что это так, но если вы видите источник, сгенерированный на сервере (ctrl + u), он будет выглядеть как показано ниже:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>About / Angular SSR</title>
    <meta name="description" content="Welcome to home section">
    <base href="/">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link rel="icon" type="image/x-icon" href="favicon.ico">
    <link href="styles.7d9457d710877291a1ca.bundle.css" rel="stylesheet">
    <style ng-transition="ssr-example">
      [_nghost-c0] {
        display: block;
        text-align: center
      }
      [_nghost-c0]>.nav[_ngcontent-c0] {
        padding: 15px
      }
      [_nghost-c0]>.nav[_ngcontent-c0]>a[_ngcontent-c0] {
        display: inline-block;
        margin: 0 5px;
        padding: 5px 10px;
        text-decoration: none;
        color: #333;
        font-weight: 500;
        border: 2px solid grey
      }
      [_nghost-c0]>.nav[_ngcontent-c0]>a[_ngcontent-c0]:hover {
        border-color: #000
      }
      [_nghost-c0]>.nav[_ngcontent-c0]>a.active[_ngcontent-c0] {
        border-color: #00f
      }
    </style>
    <style ng-transition="ssr-example"></style>
    <meta description="Welcome to about section">
  </head>
  <body>
    <app-root _nghost-c0="" ng-version="5.2.10">
      <h1 _ngcontent-c0="">Welcome to Angular SSR Example</h1>
      <div _ngcontent-c0="" class="nav">
        <a _ngcontent-c0="" routerlink="/" routerlinkactive="active" href="/">Home</a>
        <a _ngcontent-c0="" routerlink="/about" routerlinkactive="active" href="/about" class="active">About</a>
      </div>
      <router-outlet _ngcontent-c0=""></router-outlet>
      <app-about _nghost-c1="">
        <h2 _ngcontent-c1="">Welcome to about section</h2>
        <ul _ngcontent-c1="">
          <!---->
          <li _ngcontent-c1="">
            Leanne Graham (Sincere@april.biz)
          </li>
          <li _ngcontent-c1="">
            Ervin Howell (Shanna@melissa.tv)
          </li>
          <li _ngcontent-c1="">
            Clementine Bauch (Nathan@yesenia.net)
          </li>
          <li _ngcontent-c1="">
            Patricia Lebsack (Julianne.OConner@kory.org)
          </li>
          <li _ngcontent-c1="">
            Chelsey Dietrich (Lucio_Hettinger@annie.ca)
          </li>
          <li _ngcontent-c1="">
            Mrs. Dennis Schulist (Karley_Dach@jasper.info)
          </li>
          <li _ngcontent-c1="">
            Kurtis Weissnat (Telly.Hoeger@billy.biz)
          </li>
          <li _ngcontent-c1="">
            Nicholas Runolfsdottir V (Sherwood@rosamond.me)
          </li>
          <li _ngcontent-c1="">
            Glenna Reichert (Chaim_McDermott@dana.io)
          </li>
          <li _ngcontent-c1="">
            Clementina DuBuque (Rey.Padberg@karina.biz)
          </li>
        </ul>
      </app-about>
    </app-root>
    <script type="text/javascript" src="inline.318b50c57b4eba3d437b.bundle.js"></script>
    <script type="text/javascript" src="polyfills.b6b2cd0d4c472ac3ac12.bundle.js"></script>
    <script type="text/javascript" src="main.416e8f0633829dbe717e.bundle.js"></script>
        <script id="ssr-example-state" type="application/json">
      {&q;users&q;:[{&q;id&q;:1,&q;name&q;:&q;Leanne Graham&q;,&q;username&q;:&q;Bret&q;,&q;email&q;:&q;Sincere@april.biz&q;,&q;address&q;:{&q;street&q;:&q;Kulas Light&q;,&q;suite&q;:&q;Apt. 556&q;,&q;city&q;:&q;Gwenborough&q;,&q;zipcode&q;:&q;92998-3874&q;,&q;geo&q;:{&q;lat&q;:&q;-37.3159&q;,&q;lng&q;:&q;81.1496&q;}},&q;phone&q;:&q;1-770-736-8031 x56442&q;,&q;website&q;:&q;hildegard.org&q;,&q;company&q;:{&q;name&q;:&q;Romaguera-Crona&q;,&q;catchPhrase&q;:&q;Multi-layered client-server neural-net&q;,&q;bs&q;:&q;harness real-time e-markets&q;}},{&q;id&q;:2,&q;name&q;:&q;Ervin Howell&q;,&q;username&q;:&q;Antonette&q;,&q;email&q;:&q;Shanna@melissa.tv&q;,&q;address&q;:{&q;street&q;:&q;Victor Plains&q;,&q;suite&q;:&q;Suite 879&q;,&q;city&q;:&q;Wisokyburgh&q;,&q;zipcode&q;:&q;90566-7771&q;,&q;geo&q;:{&q;lat&q;:&q;-43.9509&q;,&q;lng&q;:&q;-34.4618&q;}},&q;phone&q;:&q;010-692-6593 x09125&q;,&q;website&q;:&q;anastasia.net&q;,&q;company&q;:{&q;name&q;:&q;Deckow-Crist&q;,&q;catchPhrase&q;:&q;Proactive didactic contingency&q;,&q;bs&q;:&q;synergize scalable supply-chains&q;}},{&q;id&q;:3,&q;name&q;:&q;Clementine Bauch&q;,&q;username&q;:&q;Samantha&q;,&q;email&q;:&q;Nathan@yesenia.net&q;,&q;address&q;:{&q;street&q;:&q;Douglas Extension&q;,&q;suite&q;:&q;Suite 847&q;,&q;city&q;:&q;McKenziehaven&q;,&q;zipcode&q;:&q;59590-4157&q;,&q;geo&q;:{&q;lat&q;:&q;-68.6102&q;,&q;lng&q;:&q;-47.0653&q;}},&q;phone&q;:&q;1-463-123-4447&q;,&q;website&q;:&q;ramiro.info&q;,&q;company&q;:{&q;name&q;:&q;Romaguera-Jacobson&q;,&q;catchPhrase&q;:&q;Face to face bifurcated interface&q;,&q;bs&q;:&q;e-enable strategic applications&q;}},{&q;id&q;:4,&q;name&q;:&q;Patricia Lebsack&q;,&q;username&q;:&q;Karianne&q;,&q;email&q;:&q;Julianne.OConner@kory.org&q;,&q;address&q;:{&q;street&q;:&q;Hoeger Mall&q;,&q;suite&q;:&q;Apt. 692&q;,&q;city&q;:&q;South Elvis&q;,&q;zipcode&q;:&q;53919-4257&q;,&q;geo&q;:{&q;lat&q;:&q;29.4572&q;,&q;lng&q;:&q;-164.2990&q;}},&q;phone&q;:&q;493-170-9623 x156&q;,&q;website&q;:&q;kale.biz&q;,&q;company&q;:{&q;name&q;:&q;Robel-Corkery&q;,&q;catchPhrase&q;:&q;Multi-tiered zero tolerance productivity&q;,&q;bs&q;:&q;transition cutting-edge web services&q;}},{&q;id&q;:5,&q;name&q;:&q;Chelsey Dietrich&q;,&q;username&q;:&q;Kamren&q;,&q;email&q;:&q;Lucio_Hettinger@annie.ca&q;,&q;address&q;:{&q;street&q;:&q;Skiles Walks&q;,&q;suite&q;:&q;Suite 351&q;,&q;city&q;:&q;Roscoeview&q;,&q;zipcode&q;:&q;33263&q;,&q;geo&q;:{&q;lat&q;:&q;-31.8129&q;,&q;lng&q;:&q;62.5342&q;}},&q;phone&q;:&q;(254)954-1289&q;,&q;website&q;:&q;demarco.info&q;,&q;company&q;:{&q;name&q;:&q;Keebler LLC&q;,&q;catchPhrase&q;:&q;User-centric fault-tolerant solution&q;,&q;bs&q;:&q;revolutionize end-to-end systems&q;}},{&q;id&q;:6,&q;name&q;:&q;Mrs. Dennis Schulist&q;,&q;username&q;:&q;Leopoldo_Corkery&q;,&q;email&q;:&q;Karley_Dach@jasper.info&q;,&q;address&q;:{&q;street&q;:&q;Norberto Crossing&q;,&q;suite&q;:&q;Apt. 950&q;,&q;city&q;:&q;South Christy&q;,&q;zipcode&q;:&q;23505-1337&q;,&q;geo&q;:{&q;lat&q;:&q;-71.4197&q;,&q;lng&q;:&q;71.7478&q;}},&q;phone&q;:&q;1-477-935-8478 x6430&q;,&q;website&q;:&q;ola.org&q;,&q;company&q;:{&q;name&q;:&q;Considine-Lockman&q;,&q;catchPhrase&q;:&q;Synchronised bottom-line interface&q;,&q;bs&q;:&q;e-enable innovative applications&q;}},{&q;id&q;:7,&q;name&q;:&q;Kurtis Weissnat&q;,&q;username&q;:&q;Elwyn.Skiles&q;,&q;email&q;:&q;Telly.Hoeger@billy.biz&q;,&q;address&q;:{&q;street&q;:&q;Rex Trail&q;,&q;suite&q;:&q;Suite 280&q;,&q;city&q;:&q;Howemouth&q;,&q;zipcode&q;:&q;58804-1099&q;,&q;geo&q;:{&q;lat&q;:&q;24.8918&q;,&q;lng&q;:&q;21.8984&q;}},&q;phone&q;:&q;210.067.6132&q;,&q;website&q;:&q;elvis.io&q;,&q;company&q;:{&q;name&q;:&q;Johns Group&q;,&q;catchPhrase&q;:&q;Configurable multimedia task-force&q;,&q;bs&q;:&q;generate enterprise e-tailers&q;}},{&q;id&q;:8,&q;name&q;:&q;Nicholas Runolfsdottir V&q;,&q;username&q;:&q;Maxime_Nienow&q;,&q;email&q;:&q;Sherwood@rosamond.me&q;,&q;address&q;:{&q;street&q;:&q;Ellsworth Summit&q;,&q;suite&q;:&q;Suite 729&q;,&q;city&q;:&q;Aliyaview&q;,&q;zipcode&q;:&q;45169&q;,&q;geo&q;:{&q;lat&q;:&q;-14.3990&q;,&q;lng&q;:&q;-120.7677&q;}},&q;phone&q;:&q;586.493.6943 x140&q;,&q;website&q;:&q;jacynthe.com&q;,&q;company&q;:{&q;name&q;:&q;Abernathy Group&q;,&q;catchPhrase&q;:&q;Implemented secondary concept&q;,&q;bs&q;:&q;e-enable extensible e-tailers&q;}},{&q;id&q;:9,&q;name&q;:&q;Glenna Reichert&q;,&q;username&q;:&q;Delphine&q;,&q;email&q;:&q;Chaim_McDermott@dana.io&q;,&q;address&q;:{&q;street&q;:&q;Dayna Park&q;,&q;suite&q;:&q;Suite 449&q;,&q;city&q;:&q;Bartholomebury&q;,&q;zipcode&q;:&q;76495-3109&q;,&q;geo&q;:{&q;lat&q;:&q;24.6463&q;,&q;lng&q;:&q;-168.8889&q;}},&q;phone&q;:&q;(775)976-6794 x41206&q;,&q;website&q;:&q;conrad.com&q;,&q;company&q;:{&q;name&q;:&q;Yost and Sons&q;,&q;catchPhrase&q;:&q;Switchable contextually-based project&q;,&q;bs&q;:&q;aggregate real-time technologies&q;}},{&q;id&q;:10,&q;name&q;:&q;Clementina DuBuque&q;,&q;username&q;:&q;Moriah.Stanton&q;,&q;email&q;:&q;Rey.Padberg@karina.biz&q;,&q;address&q;:{&q;street&q;:&q;Kattie Turnpike&q;,&q;suite&q;:&q;Suite 198&q;,&q;city&q;:&q;Lebsackbury&q;,&q;zipcode&q;:&q;31428-2261&q;,&q;geo&q;:{&q;lat&q;:&q;-38.2386&q;,&q;lng&q;:&q;57.2232&q;}},&q;phone&q;:&q;024-648-3804&q;,&q;website&q;:&q;ambrose.net&q;,&q;company&q;:{&q;name&q;:&q;Hoeger LLC&q;,&q;catchPhrase&q;:&q;Centralized empowering task-force&q;,&q;bs&q;:&q;target end-to-end models&q;}}]}
    </script>
  </body>
</html>

Вы можете ясно видеть из источника выше, что у нас есть состояние, сгенерированное на сервере. Но почему наш браузер не может его найти? Причиной этого является расположение этого блока сценария - это код HTML. Скрипты выполняются сверху вниз, поэтому, если какой-либо код кода JavaScript, который ищет этот блок перед деревом DOM, то он не сможет его найти. Следовательно, нам нужно отложить загрузку модуля браузера, пока DOM не будет полностью отображен. Это делается путем прослушивания события DOMContentLoaded в документе.

// browser.main.ts
...
document.addEventListener('DOMContentLoaded', () => {
  platformBrowserDynamic().bootstrapModule(BrowserAppModule)
  .catch(err => console.log(err));
});

После этого изменения нужно пересобрать приложение и перезапустить express-сервер. Теперь браузер не должен делать другой HTTP-запрос, поскольку объект состояния будет доступен, когда Angular приложение загружается на сервере.

Документация State Transfer API по-прежнему строится или не полностью объясняется на официальном веб-сайте Angular документации, поэтому я не буду подробно объяснять здесь, но вы должны иметь некоторые идеи.


Существует несколько проблем с рендерингом на стороне сервера. Поскольку приложение сперва загружается на Node.js, любой код, зависящий от контекста браузера и API браузера, не будет работать. Таким образом, любой код доступа к window и объекту document будет вызывать ошибку. Кроме того, поскольку DOM недоступен в узле, любая операция DOM также возвращается к ошибке. Любые API-интерфейсы браузера, такие как localStorage, IndexedDB, также будут вызывать ошибку при загрузке приложения на сервере.

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

Если вам нужно использовать API-интерфейсы браузера, сначала проверьте, является ли платформа, которая пытается загрузить приложение, является браузером сервера. Это можно сделать, проверив, существует ли объект window в глобальной области видимости или с помощью isPlatformBrowser.

import { Injectable, Inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
export class FeatureComponent implements OnInit{
    constructor(@Inject(PLATFORM_ID) private platform: any) { }
    ngOnInit() {
        if (isPlatformBrowser(this.platform)) {
            // use localStorage API
        }
    }
}


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