Angular Universal: Реализация серверной генерации страниц для приложения на Angular 4

| Суббота, 14 октября, 2017

Метки Angular, TypeScript, SEO


Одностраничный сайт разработан. Открыт в общем доступе. Но как его продвигать? SEO (Search Engine Optimization) продвижение становится очень затруднительным из-за того, что другие сайты, социальные сети и поисковые системы не видят содержимое вашего сайта, так как оно генерируется в браузерах пользователей, а данные для них подгружаются ajax-запросами. Решаем эту проблему и делаем наш одностраничник информативным для других сайтов с помощью Angular Universal.

Библиотеки для разработки одностраничных приложений все больше и больше завоевывают внимание JavaScript-программистов по всему миру. Такие приложения работают непосредственно в браузере, генерируют содержимое страницы прямо на клиенте, выполняют навигацию и переключение страниц затрагивая сервер только для получения данных, и при этом нагружают сервер и сеть по минимуму. Кроме того, такие приложения обеспечивают прекрасную скорость работы, а также быстрые и удобные интерфейсы для взаимодействия с пользователем. Но есть один недостаток, приложение должно быть проиндексировано поисковыми машинами.

Многие поисковые сайты и социальные сети такие как Facebook и Twitter ожидают чистый HTML для вычитывания из него мета тегов и текстов страниц. Они не могут определить, когда Javascript закончит генерацию страницы и поэтому не получают ценной информации, а всего лишь малый набор тегов. Хотя такие монстры как Google хорошо генерируют страницы динамических сайтов, читают их и индексируют, но пользователи все равно впадают с ступор при попытке поделится ссылкой в социальной сети.

И для этого нужен сервер, который будет выдавать готовый HTML-код. Нам нужно, чтобы поисковые машины, социальные сети и просто пользователи приложения видели страницы, сгенерированные уже на сервере, так как это 100% гарантированный способ доставки содержимого любому клиенту. В этом нам поможет Angular Universal.

Angular Universal

Официальный сайт (universal.angular.io) заявляет, что Angular Universal — это рендеринг приложений Angular 2+ на серверной стороне. Фактически это промежуточное приложение (middleware) между node.js и Angular, которое связывает все лучшие качества одностраничных приложений (взаимодействие с пользователем, быстродействие) и статичных сайтов, которые прекрасно дружат с поисковой оптимизацией.

Сборка классического проекта Angular 4

Внимание, для повтора всех шагов, описанных в статье, убедитесь, что у вас глобально установлены версии не ниже (тестировалось, именно с этими с версиями): node.js (v6.9.2), npm (3.10.5), Angular CLI 1.4.4. Проверить версии можно командами соответственно: node -v, npm -v, ng -v. Данная утилита Angular CLI 1.4.4 соберет вам проект с Angular 4.2.x. Если Angular CLI установлен ниже, то переустановите его во избежание нестыковок и возможных проблем.

Angular CLl на момент написания статьи распространяется с именем @angular/cli вместо anguar-cli. Переустановка пакета состоит из трех шагов:

  1. Если у вас установлен angular-cli, удалите его.
    npm uninstall -g angular-cli
    Или, если установлена более новая версия
    npm uninstall -g @angular/cli
    И очистите кеш на всякий случай: npm cache clean
  2. Обновите, если необходимо node/npm. Angular CLI сейчас требует версии Node 6.9.0 и выше вместе с NPM 3 и выше.
  3. Установите глобально новую версию Angular CLI
    npm install -g @angular/cli@latest

Теперь у нас все готово для сборки обычного Angular-проекта. Создаем обычный проект с помощью команды ng new angular-universal-demo. Более подробно с созданием и запуском проекта на Angular 4 можно тут. По завершении создания запускаем этот проект. В браузере открываем просмотр кода страницы и видим в теле документа body пустой тег app-root, в котором Angular генерирует содержимое страницы.

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

Подключаем серверный рендеринг

  1. Находим файл главного модуля нашего приложения app.module.ts. В нем в разделе imports вместо строки BrowserModule прописываем:
    BrowserModule.withServerTransition({appId: 'my-app'}) 
    
    AppId можем назначить любое.
  2. Рядом с файлом app.moodule.ts создадим файл app.server.module.ts. В нем помещаем следующий код:
    import {NgModule} from '@angular/core'; 
    import {ServerModule} from '@angular/platform-server'; 
    import {ModuleMapLoaderModule} from '@nguniversal/module-map-ngfactory-loader';
    
    import {AppModule} from './app.module'; 
    import {AppComponent} from './app.component';
    
    @NgModule({ 
      imports: [ 
        // В AppServerModule должен быть импортирован AppModule   
        // вместе с ServerModule из @angular/platform-server. 
        AppModule, 
        ServerModule, 
        ModuleMapLoaderModule, 
      ], 
      // Так как главный загрузочный компонент не наследуется из 
      // AppModule, то тут тут мы повторяем его подключение.  
      bootstrap: [AppComponent], 
    }) 
    export class AppServerModule {} 
    
    Здесь мы используем два новых модуля поэтому подключим их:
    npm install @angular/platform-server –save
    npm install @nguniversal/module-map-ngfactory-loader --save
  3. В папке src находим файл main.ts, создадим рядом файл main.server.ts со следующим кодом:
    import { environment } from './environments/environment'; 
    import { enableProdMode } from '@angular/core'; 
     
    if (environment.production) { 
      enableProdMode(); 
    } 
     
    export {AppServerModule} from './app/app.server.module';
    
  4. В этой же папке src рядом с файлом tsconfig.app.ts создаем файл tsconfig.server.ts с кодом (по сути это содержимое tsconfig.app.ts с двумя изменениями, помечены комментариями):
    { 
      "extends": "../tsconfig.json", 
      "compilerOptions": { 
        "outDir": "../out-tsc/app", 
        "baseUrl": "./", 
        // Изменяем формат модуля на "commonjs": 
        "module": "commonjs", 
        "types": [] 
      }, 
      "exclude": [ 
        "test.ts", 
        "**/*.spec.ts" 
      ], 
      // Добавляем "angularCompilerOptions" и указывает AppServerModule  
      // как входящий модуль "entryModule". 
      "angularCompilerOptions": { 
        "entryModule": "app/app.server.module#AppServerModule" 
      } 
    }
    
  5. Теперь найдем файл angular-cli.json и добавим ему серверное приложение для сборки в массив apps (тут тоже копирование данных клиентского приложения с указанием платформы и файлов созданных выше):
    { 
      "platform": "server", 
      "root": "src", 
      "outDir": "dist/dist-server", 
      "assets": [ 
        "assets", 
        "favicon.ico" 
      ], 
      "index": "index.html", 
      "main": "main.server.ts", 
      "test": "test.ts", 
      "tsconfig": "tsconfig.server.json", 
      "testTsconfig": "tsconfig.spec.json", 
      "prefix": "app", 
      "styles": [ 
        "styles.css" 
      ], 
      "scripts": [], 
      "environmentSource": "environments/environment.ts", 
      "environments": { 
        "dev": "environments/environment.ts", 
        "prod": "environments/environment.prod.ts" 
      } 
    } 
    
  6. Приложение почти готово, но нам нужно еще серверное приложение, которое знает все о клиентском и генерирует страницы так как это делает браузер на клиенте. Для этого в самой корневой папке приложения мы создадим файл server.js. Это приложение node.js которое запускается на localhost, выдает нам страницы нашего приложения в готовом виде:
    require('zone.js/dist/zone-node'); 
    require('reflect-metadata'); 
    const express = require('express'); 
     
    const { ngExpressEngine } = require('@nguniversal/express-engine'); 
    // Import module map for lazy loading 
    const { provideModuleMap } = require('@nguniversal/module-map-ngfactory-loader'); 
     
    // Import the AOT compiled factory for your AppServerModule. 
    // This import will change with the hash of your built server bundle. 
    const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require(`./dist-server/main.bundle`); 
     
    const app = express(); 
    const port = 8000; 
    const baseUrl = `http://localhost:${port}`; 
     
    // Set the engine 
    app.engine('html', ngExpressEngine({ 
      bootstrap: AppServerModuleNgFactory, 
      providers: [ 
        provideModuleMap(LAZY_MODULE_MAP) 
      ] 
    })); 
     
    app.set('view engine', 'html'); 
     
    app.set('views', './'); 
    app.use('/', express.static('./', {index: false})); 
     
    app.get('*', (req, res) => { 
      res.render('index', {req, res}); 
    }); 
     
    app.listen(port, () => { 
      console.log(`Listening at ${baseUrl}`); 
    }); 
    
    Две новые библиотеки нужно установить:
    Две новые библиотеки нужно установить:
    Npm install @nguniversal/express-engine –save
  7. Итак, у нас все готово за исключением одного момента в файле server.js мы вытаскиваем два модуля из несуществующей пока сборки require(`./dist/dist-server/main.bundle`).
    Для исправления этой ситуации откроем файл package.json и добавим в раздел scripts две строки:
    "build:dynamic": "ng build --prod && ng build --prod --app 1 --output-hashing=false && cpy ./server.js ./dist",
    "serve:dynamic": "npm run build:dynamic && cd dist && node server"
    
    Установим npm install cpy-cli --save-dev.
    Запуская первую команду npm run build:dynamic мы получим в папке dist полностью готовый для запуска проект. И второй командой мы можем собрать и запустить наше приложение в браузере.

Запуск приложения и проверка результата

Набираем команду npm run serve:dynamic, ждем выполнения. В логах мы увидим сборку двух приложений, так как в angular-cli.json мы прописали еще одно серверное. Открываем приложение в браузере. Мы увидим тоже самое, что мы видели запуская команду ng serve. Но посмотрим на исходный код страницы в браузере.

Мы увидим совсем другую картину. Сейчас мы видим внутри тега app-root разметку и тексты.

Это тот вид, которого мы добивались и который будут видеть поисковые системы и другие сайты. Серверный рендеринг открывает широкие перспективы для разработки сайтов программистами активно использующих Angular 4.

Резюмируем краткий план подключения:

Версии: @angular/cli": "1.4.4", "@angular/core": "^4.2.4",
  1. файл app.module.ts, добавляем withServerTransition
  2. создаем файл app.server.module.ts, устанавливаем @angular/platform-server, @nguniversal/module-map-ngfactory-loader
  3. создаем main.server.ts
  4. coздаем tsconfig.server.json
  5. в angular-cli.json добавим приложение
  6. создаем server.js (ES6), устанавливаем @nguniversal/express-engine
  7. добавляем команды в package.json (build:dynamic, serve:dynamic)

Пример проекта можно посмотреть тут https://github.com/ang4-examples/angular-universal-demo