Инструменты веб-разработки

Рендеринг на стороне сервера (SSR) в Angular 5+ | Самый простой и быстрый подход к SSR

Щелкните здесь, чтобы опубликовать эту статью в LinkedIn »

Большинство существующих веб-приложений имеют рендеринг на стороне клиента, что означает, что все необходимые коды (HTML, CSS, JavaScript) объединяются вместе и сразу отправляются в клиентский браузер. В зависимости от URL-адреса браузера такие фреймворки, как Angular, React, Vue и т. Д., Используют этот код для отображения различных представлений, манипулируя DOM и выполняя сетевые запросы. Это значительно улучшает пользовательский опыт, но за некоторую плату.

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

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

В этой статье я объясню пошаговую реализацию рендеринга на стороне сервера на нескольких примерах. Убедитесь, что у вас установлена ​​версия Angular CLI 1.6+. Если вы создаете новое угловое приложение, лучше установите новую версию Angular CLI с помощью следующей команды

npm install -g @angular/cli@latest

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

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

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

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

Мы собираемся создать два новых модуля 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 в компонент приложения.

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

Вы можете добавить некоторые функции к домашнему компоненту и компоненту.

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

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

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

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

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

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

2. Создайте модули браузера и сервера.

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

У нас уже есть app.module.ts, который был сгенерирован angular CLI, измените его таким образом, чтобы он выглядел как показано ниже.

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

Теперь мы собираемся создать browser.app.module.ts файл с приведенным ниже содержимым в том же каталоге app.module.ts.

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

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

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

npm install --save @angular/platform-server

3. Создайте отдельные точки входа.

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

Следовательно, переименуйте main.ts в browser.main.ts, и он должен иметь содержимое ниже.

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

4. Создайте файлы tsconfig.json.

У нас уже есть файл tsconfig.app.json в папке src. Это конфигурация машинописного текста, сгенерированная интерфейсом командной строки для модуля приложения. Поскольку у нас есть два разных модуля для разных ролей, нам нужно иметь две разные конфигурации.

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

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

5. Измените файл .angular-cli.json.

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

У нас есть два приложения, потому что есть два модуля, загружающих приложение.

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

6. Создавайте раздачи

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

"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. Создайте экспресс-сервер.

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

Обязательно установите @nguniversal/express-engine из команды ниже. Этот модуль необходим для вставки соответствующего HTML-кода внутрь app-root на основе маршрута (URL-адрес запроса).

npm install --save @nguniversal/express-engine

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

npm run serve

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

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

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

8. Государственный перевод

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

Здесь на помощь приходит государственная передача. Мы можем установить состояние приложения до его начальной загрузки. Это делают модули BrowserTransferStateModule и ServerTransferStateModule.

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

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

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

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

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

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

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

npm run build && npm run server

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

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

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

После этого изменения перестройте приложение и перезапустите экспресс-сервер. Теперь браузер не должен делать еще один 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 на сервере. Но поскольку это была простая демонстрация, я рекомендую вам посетить несколько хороших статей о тех концепциях, которые доступны здесь, на Medium.

На GitHub есть проект браузера Chrome без заголовка, созданный командой Google под названием rendertron, который предоставляет среду браузера Chrome. Таким образом, наш экспресс-сервер может использовать API-интерфейсы rendertron для создания HTML из приложения angular, которое прямо или косвенно зависит от API-интерфейсов браузера. Вам обязательно стоит проверить этот проект и посмотреть, сможете ли вы интегрировать его с текущими настройками.

Я сделал пример репозитория GitHub со всеми кодами, реализованными в этой статье, проверьте ссылку ниже.