Дружим Angular с Google


Google ненавидит SPA


Когда мы говорим про современные интернет магазины, мы представляем себе тяжелые для понимания серверы, рендрящие тысячи статических страничек. Причем именно эти тысячи отрендеренных страниц одна из причин, почему Single Page Applications не прижились в электронной коммерции. Даже крупнейшие магазины электронной коммерции по-прежнему выглядят как куча статических страниц. Для пользователя это нескончаемый цикл кликов, ожиданий и перезагрузки страниц.



Одностраничные приложения приятно отличаются динамичностью взаимодействия с пользователем и более сложным UX. Но как не прискорбно обычно пользовательский комфорт приносится в жертву SEO оптимизации. Для сеошника сайт на angular – это своего рода проблема, поскольку поисковикам трудно индексировать страницы с динамическим контентом.


Другой недостаток SPA — это превью сайта. Например, пользователь только-что купил новый телевизор в нашем интернет-магазине и хочет порекомендовать его своим друзьям в соцсетях. Превью ссылки в случае Angular будет выглядеть так:


site preview

Мы любим JS и Angular. Мы верим, что классный и удобный UX может быть построен на этом стеке технологий, и мы можем решить все сопутствующие проблемы. В какой-то момент мы столкнулись с Angular Universal. Это модуль Angular для рендеринга на стороне сервера. Сначала нам показалось, вот оно – решение! Но радость была преждевременной — и отсутствие больших проектов с его применением тому доказательство.


В итоге, мы начали разрабатывать компоненты для интернет-магазина, используя обычный Angular 2, и ждали, когда Universal будет объединен с Angular Core. На данный момент слияния проектов еще не произошло, и пока не ясно, когда произойдет (или как итоговый вариант будет совместим с текущей реализацией), однако сам Universal уже перекочевал в github репозиторий Angular.


Несмотря на эти трудности, наша цель осталась неизменной — создавать классные веб-приложения на Angular с серверным рендерингом для индексации поисковиками. В результате наша команда разработала более 20 универсальных компонентов для быстрой сборки интернет-магазина. Как в итоге это было достигнуто?


Что такое Angular Universal


Прежде всего, давайте обсудим, что такое Angular Universal. Когда мы запустим наше приложение на Angular 2 и откроем исходный код, увидим что-то вроде этого:


<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Angular 2 app</title>
  <!-- base url -->
  <base href="/">
<body>
  <app></app>
</body>
</html>

Фронтенд фреймворки, такие как Angular, динамически добавляют контент, стили и данные в тег .
Для приложения ориентированного в первую очередь на бизнес зависимый от индексации сайта поисковиками важно чтобы наш контент индексировался и был в топе выдачи Google.

Для решения проблем c индексацией Angular Universal дает нам возможность выполнять рендеринг на стороне сервера. Наша страница будет создаваться на «бэкэнд-сервере», написанном на Node.Js, .NET или другом языке, и браузер пользователя получит страницу со всеми привычными тегами в ней -заголовками, мета-тегами и контентом.

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

До этого момента все выглядит неплохо? Возможно, но дьявол скрыт в мелочах, как всегда.
Итак, мы хотим поделиться с вами подводными камнями, с которыми мы столкнулись на нашем пути.


Подводные камни Angular Universal


Не трогайте DOM


Когда мы начали тестировать компоненты нашего магазина с помощью Universal, нам пришлось потратить некоторое время, чтобы понять, почему наш сервер падает при запуске без вывода серверной страницы. Например, у нас есть компонент Session Flow component, который отслеживает активность пользователя во время сессии (перемещения пользователя, клики, рефферер, информация об устройстве пользователя и т.д.). После поиска информации в issues на GitHub мы поняли, что в Universal нет обертки над DOM.


DOM на сервере не существует.


Если вы склонируете этот Angular Universal стартер и откроете browser.module.ts вы увидите, что в массиве providers разработчики Universal предоставляют дваboolean значения:


providers: [
  { provide: 'isBrowser', useValue: isBrowser },
  { provide: 'isNode', useValue: isNode },
  ...
]

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


@Injectable()
export class SessionFlow{
  private reffererUrl : string;
  constructor(@Inject('isBrowser') private isBrowser){
    if(isBrowser){
      this.reffererUrl = document.referrer;
    }
  }
}

Universal автоматически добавляет false, если это сервер, и true, если браузер. Может быть, позже разработчики Universal пересмотрят эту реализацию, и нам не придется беспокоиться об этом.


Если вы хотите активно взаимодействовать с элементами DOM, используйте сервисы Angular API, такие какElementRef, Renderer или ViewContainer.


Правильный роутинг


Поскольку сервер отражает наше приложение, у нас была проблема с роутингом.


Ваш роутинг на клиенте, написанный на Angular, должен соответствовать роутингу на сервере.
Поэтому не забудьте добавить тот же роутинг на сервере что и на клиенте. Например, у нас есть такие роуты на клиенте:


[
  { path: '', redirectTo: '/home', pathMatch: 'full' },
  { path: 'products', component: ProductsComponent },
  { path: 'product/:id', component: ProductComponent}
]

Тогда нужно создать файл server.routes.ts с массивом роутов сервера. Корневой маршрут можно не добавлять:


export const routes: string[] = [
  'products',
  'product/:id'
];

Наконец, добавьте роуты на сервер:


import { routes } from './server.routes';
... other server configuration
app.get('/', ngApp);
routes.forEach(route => {
  app.get(`/${route}`, ngApp);
  app.get(`/${route}/*`, ngApp);
});

Пререндеринг стартовой страницы


Одной из наиболее важных особенностей Angular Universal является пререндеринг. Из исследования Kissmetrics следует, что 47% потребителей ожидают, что веб-страница загрузится за 2 секунды или даже менее. Для нас было очень важно отобразить страницу как можно быстрее. Таким образом, пререндеринг в Universal как раз про нашу задачу. Давайте подробнее рассмотрим, что это такое и как его использовать.


Когда пользователь открывает URL нашего магазина, Universal немедленно возвращает предварительно подготовленную HTML страничку с контентом, а уже затем затем начинает загружать все приложение в фоновом режиме. Как только приложение полностью загрузится, Universal подменяет изначальную страницу нашим приложением. Вы спросите, что будет, если пользователь начнет взаимодействовать со страницей до загрузки приложения? Не беспокойтесь, библиотека Preboot.js запишет все события, которые выполнит пользователь и после загрузки приложения выполнит их уже в приложении.


Чтобы включить пререндеринг, просто добавьте в конфигурацию сервера preboot: true:


res.render('index', {
  req,
  res,
  preboot: true,
  baseUrl: '/',
  requestUrl: req.originalUrl,
  originUrl: `http://localhost:${ app.get('port') }`
  }
);

Добавление мета-тегов


В случае SEO-ориентированного сайта добавление мета-тегов очень важно. Например, у нас есть Product component, который представляет страницу определенного продукта. Он должен асинхронно получать данные о продуктах из базы данных и на основе этих данных добавлять мета-теги и другую специальную информацию для поискового робота.


Команда Angular Universal создала сервис angular2-meta, чтобы легко манипулировать мета-тегами. Вставьте мета-сервис в ваш компонент и несколько строк кода добавлят мета-теги в вашу страницу:


import { Meta, MetaDefinition } from './../../angular2-meta';
@Component({
  selector: 'main-page',
  templateUrl: './main-page.component.html',
  styleUrls: ['./main-page.component.scss']
})
export class MainPageComponent {
  constructor(private metaService: Meta){
    const name: MetaDefinition = {
      name: 'application-name',
      content: 'application-content'
    };
    metaService.addTags(name);
  }
}

В следующей версии Angular этот сервис будет перемещен в@angular/platform-server


Кэширование данных


Angular Universal запускает ваш XHR запрос дважды: один на сервере, а другой при загрузке приложения магазина.


Но зачем нам нужно запрашивать данные на сервере дважды? PatricJs создал пример, как сделать Http-запрос на сервере один раз и закэшировать полученные данные для клиента. Посмотреть исходный код примера можно здесь. Чтобы использовать его заинжекте Model service и вызовите метод get для выполнения http-вызовов с кешированием:


public data;
constructor(public model: ModelService) {
  this.universalInit();
}
universalInit() {
  this.model.get('/data.json').subscribe(data => {
    this.data = data;
  });
}

Выводы


Рендеринг на стороне сервера с помощью Angular Universal позволяет нам создавать клиентоориентированные приложения электронной коммерции более не переживая об индексации вашего приложения. Кроме того, функция «prendering» позволяет сразу показать сайт для вашего клиента, улучшая время рендеринга (что довольно неприятный момент для Angular приложений из-за большого размера самой библиотеки).

Поделиться с друзьями
-->

Комментарии (30)


  1. Apokalepsis
    27.03.2017 14:03

    Спасибо за статью. Было интересно почитать почему вы делаете интернет магазины на Angular, а например не на Битриксе или Magento. Ну и конечно бы хотелось кейса разработанного интернет-магазина — как делали, где сложности, зачем и почему))))


    1. NosovK
      27.03.2017 14:45
      +3

      Мы делаем магазины на Angular чтобы избежать постраничности, постоянных переходов — таким образом пользователю приятнее пользоваться сайтом. Поэтому мы ушли с PrestaShop\Magento в сторону разработки переиспользуемых компонент на Angular (только сразу на втором). А дальше мы из них собираем фронт выбирая те модули что нам нужны в конкретном магазине. Все модули независимы друг от друга, но есть связующее звено DAL — data abstraction layer, через которое происходит работа с backend. На github есть коннектор к firebase как бэкэнду. Вообще скоро планируется статья отдельно про firebase, а потом ещк именно про сами компоненты для интернет магазинов с примерами (к этому моменту надеюсь напишем starter-project).


    1. XeL077
      28.03.2017 17:58

      Angular 2 хороший конструктор, каждый компонент можно создать легко и правильно в архитектурном смысле, прописав все интерфейсы.

      В Bitrix часто приходится делать костыли из модуля «новости» и засовывать все в инфоблоки, которые хранятся в базе, делая бутылочное горлышко еще уже.

      Magento очень тяжелая и по ней мало специалистов, не все хотят иметь опыт по «cms N».


  1. spatNeHochu
    27.03.2017 14:46
    -2

    Да да, самый главный вопрос зачем и почему… Скорее всего потому что лицо принимающее решение когда то изучило ангуляр, такой модный и хайпный)


    1. NosovK
      27.03.2017 14:57
      +2

      Это все про 4й Angular, он не такой хайпный, и совсем-совсем новый. Учли всю ту боль с которой мы жили используя 1й Angular. Новый Angular совсем-совсем модульный. Собственно из-за этого он намного более похож на конструктор, более чем плагины в той же magento.
      + разрабатывать интерфейс на нем намного комфортнее чем в шаблонах того же prestashop\magento, за счет большей кастомизируемости и гибкости.
      Скажем так — по всем параметрам, кроме SEO, удобство решения на Angular выше чем у класического написания шаблонов для magento\prestashop.
      А про то как решается вопрос с SEO как раз и расказанно в статье.


      1. spatNeHochu
        27.03.2017 15:03
        -3

        Я просто хочу сказать что именно для ваших задач тот же react на порядок больше подходит, пока я не понимаю что вы нашли в angular.


        1. NosovK
          27.03.2017 15:18
          +1

          Вечный холиварный вопрос — Angular with React да будет кровь!..
          Банально дешвеле стоимость владения и больше перееиспользуемость между проектами. Очень помогает RxJS в разработке. Ну и кончено удобство работы с Firebase из Angular. На данный момент иной нежели Firebase конеектор не предусмотрен. С появление Firebase Functions разработка других конекторов становится менее приоритетной.


          1. spatNeHochu
            27.03.2017 16:09
            -3

            Да никакой он не вечный, просто есть задачи где angular больше подходит из коробки, а есть где больше подходит не angular, без всяких кровей.

            Судя по вашим репозиториям и сайту опыта у вас довольно мало, поэтому пишу против монстров типа angular:

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

            Во вторых попробуйте найти человека, который действительно знает что такое angular, а не использует его как шаблонизатор. Хотя бы тот кто вам объяснить почему | это не битовая операция а вызов фильтра (первая версия)

            В третьих сам angular создает вокруг себя замкнутую малоподвижную и сильносвязную архитектуру, и вы не можете просто взять без оберток и использовать хорошую стороннюю либу, но это уже про поддержку вашего конструктора.


        1. psFitz
          27.03.2017 16:40
          +2

          Конечно же вам лучше знать, кому и что подходит)


          1. spatNeHochu
            27.03.2017 17:15

            Ну это как бы просто моё мнение если что и вроде как я имею право его выражать здесь, нет?


            1. NosovK
              27.03.2017 19:13

              Конечно, но вопросы по теме статьи приветствуются в первую очередь. Если у вас есть вопросы об Angular Universal и том как его применять в живых проектах — будем рады.
              В статье описанны те грабли с которыми мы столкунулись — возможно вы можете подсказать нам на какие мы еще не напоролись в Angular Universal?


            1. taujavarob
              30.03.2017 18:02
              -1

              spatNeHochu >

              Ну это как бы просто моё мнение если что и вроде как я имею право его выражать здесь, нет?

              Нет, не имеете, ибо вы не написали ни одной статьи на хабре.

              Хочешь понижать другому карму, оценивать коменты читателей и давать оценки статьям — напиши статью на Хабр. (С)

              Хабр не место для дискуссий


              1. spatNeHochu
                31.03.2017 10:48

                Как бы сказать, я вовсе не тот самый кармадрочер, дали возможность комментировать буду писать комментарии, где буду мнение своё писать на что захочу (интерфейс сайта позволяет же), а ежели отберут — не буду.


                1. taujavarob
                  31.03.2017 18:08
                  -1

                  spatNeHochu

                  дали возможность комментировать буду писать комментарии, где буду мнение своё писать на что захочу (интерфейс сайта позволяет же), а ежели отберут — не буду.

                  Ясно. Готовьтесь к коментам в зависимости от кармы:

                  От ?1 до ?10 Можно комментировать лишь 1 раз в 5 минут
                  От ?11 до ?30 1 комментарий в час
                  От ?31 до ?100 1 комментарий в день
                  *От ?100 и ниже 1 комментарий в неделю и значок «Тролль» *

                  Удачи! :-)

                  P.S. Или становитесь… писателем — им тут разрешено всё. Это их сайт. (С)


    1. Yeah
      27.03.2017 15:13
      +2

      Вангую: потому что потом заказчик никуда не денется от их поддержки. Это вам не магентщика найти.


      1. NosovK
        27.03.2017 15:26

        на самом деле нет — все компоненты в opensource. Второй Angular очень большой путь проделал по унификации и улучшению поддерживаемости, так что любой фронт разработчик знакомый с Angular сможет вносить правки в проект. А правки по оформлению может внести банально верстальщик за счет разделения кода шаблона и кода компонент.
        Наше столкновение с Magento как раз показало что получается дорого и малость отстало с точки зрения фронтэнда. Конечно Magento2 уже дружит с Symfony, что безусловно пойдет ей на пользу (особенно в плане удешевления разработки под нее и уменьшения количества плохо написанных дополнений).


        1. Yeah
          27.03.2017 15:38
          +1

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


        1. justboris
          27.03.2017 22:17

          на самом деле нет — все компоненты в opensource

          Однако это никак не опровергает тезис, что разработчика со знанием Magento намного проще, чем для модулей, которые в опенсорсе 11 дней как


          1. NosovK
            27.03.2017 23:09
            +1

            найти разработчика на Angular 2 в скором времени будет проще чем найти толкового разработчика Magento. Как раз потому что область применения Angular шире чем область применения Magento.
            И статья не об этом, а о новой технологии которая позволяет еще более расширить область применения Angular на сферу в которой он изначально не был применим из-за проблем с индексацией поисковиками.


        1. justboris
          27.03.2017 22:18
          -1

          Наше столкновение с Magento как раз показало что получается дорого и малость отстало с точки зрения фронтэнда.

          А можно поподробнее раскрыть тезис про "отстало"? В коворкинге смузи не нальют?


          1. NosovK
            27.03.2017 23:11

            Основная претензия раскрыта в первом абзаце

            Даже крупнейшие магазины электронной коммерции по-прежнему выглядят как куча статических страниц. Для пользователя это нескончаемый цикл кликов, ожиданий и перезагрузки страниц.


  1. Apokalepsis
    28.03.2017 07:31

    Кстати, интересно. Админка для ваших магазинов так же на Angular, или использовали что то другое? И есть ли вообще админка? Используете одну и туже для все проектов или пишите каждый новую под требования конкретного магазина? Есть ли проблемы с интеграции платежных систем и доставки, так же используете что то готовое или каждый раз пишите заново?


    1. NosovK
      28.03.2017 11:43

      Про сами компоненты чуть позже будет отдельная статья.

      Сейчас все наши магазины это витрины к разнообразным учетным системам. То есть в бэкэнд данные попадают через демон импортер из 1c\google sheets\yandex.yml или иной системы. Общая концепция состоит в том что ненужно создавать еще одну монстроадинку с складом\налогами\доставкой, а интегрироваться с существующими решениями для управления складом и учетом. Чаще всего у клиентов уже есть какие-то способы управления\организациию Для посмотреть\поиграться или просто маленького магазина проще всего google sheets. Как оказалось небольшие магазины и так ведут в нем склад и продажи, так что им удобно — просто теперь данные из sheets синхронизируются с магазином (+попадаются просто зубодробильные формулы по расчету цены реализации).
      А вот аналитика помимио стандартных google analytics собирается в бд, отдельно по всем действиям пользователя — устройства, сессии, клики, показы и т.д.

      По поводу платежных систем — пишем универсальные модули по мере запросов от клиентов (stripe, 2checkout, w1 etc). С ними проблем нет. По поводу систем доставки — пока все статичные, будут запросы на какие-то сервисы — сделаем и с ними интеграцию.
      Вообще вся корзина это просто метод композиции из модулей платежек и способов доставки, просто с учетом экспортируемых ими требований (к примеру PayPal не требует выбора адреса доставки, а наложенный платеж Новой Почтой не требует указания ничего кроме адреса)


      1. Apokalepsis
        29.03.2017 00:47

        Еще раз спасибо за прекрасную статью и развернуты ответ. Была бы возможность поставить плюсы, поставил бы под каждым постом)) Я бы сказал что у вас не обычный подход, так как принято считать что Angular и Node вообще больше подходит для «штучных» сервисов и в основном используется так. Редко кто использует его для клиентских проектом. Но вы показали как его можно использовать для клиентских проектов, появилось сразу 100500 идей, а руки зачесались сделать какой нибудь магазин на angular))))

        Единственное мне кажется все же какая то админка должна быть, потому что помимо склада есть еще и маркетинг и это достаточно большой пласт. Но делать еще одну монструозную админку, действительно не выход.
        Но учитывая тенденцию микросервисов, можно сделать решение «между». Какая то общая админка для всех клиентов в которую интегрируется по API большое количество уже существующих решений, в основном связанные с маркетингом ( рассылки, аналитика, crm). На мой взгляд это бы коммерчески выгодным.


        1. NosovK
          29.03.2017 14:39

          Собственно благодаря Universal надеюсь в скором времени произойдет перелом в сторону клиентских проектов, а не только админок. (Хотя и они никуда не уйдут — уже сейчас сделали пару админок на angular 2 + firebase и очень довольны).
          Для маркетинга и аналитики выбран чуть иной путь. Все данные из firebase переливать в bigquery. Заказы, клики, просмотры, все-все. А дале по датасету в bigquery можно делать анализ и для любителей красоты и дашбордов есть google data studio. В нем удобно именно маркетологам работать.
          Да, для работы с бд есть DataAbstractionLayer — который как раз и дает такое API и должен позволить нам в случае необходимости заменить firebase на couchdb к примеру. А так да — есть observer server сейчас, который загружает плагины, которые уже оформляют подписки на нужные события и отправляют их в интегрированные сервисы. Пример такого сервера будет в стартере в следующем месяце.

          Соберетесь делать интернет магазин — милости просим :) мы будем стараться помогать всем пользователям компонент, сейчас сделали документацию на базе compodoc, далее в планах стартер для желающих быстро запустить демо магазин (правда быстро будет включать в себя докер, что увы как оказалось многих может остановить)


          1. psFitz
            29.03.2017 15:59

            У вас firebase заменяет полностью бд?
            Просто первый раз слышу об этом подходе, оч интересно)


            1. NosovK
              29.03.2017 17:49

              Да. Сначла мы работали на MEAN стэке, потом пару раз столкнулись с Parse. У нас ранее был проект с закрытым кодом, в который мы через GTM инжектили скрипты для AB тестов, промоакций, трекинга событий и просто банальна фиксы дизайна. Соотвтетственно нам нужно было решение для храниния настроек и состояний без бд. Потом появился FireBase с возможностью подписки на изменения что позволило существенно расширить функционал. А далее мы выяснили что FireBase был куплен Google, который ни о ком не заботясь сделал релиз 3й версии, который заставил нас много всего переписать. Собственно далее мы стали его использовать как бэк для мобильных приложений, чтобы не заботится о масштабировании. Пототм для админ панелей. А сейчас вообщем-то используем для большинства новых проектов. Плюс недавний релиз функций может позволить нам избавится от oserver сервисов.
              Ну и у firebase есть бесплатный хостинг — что нравистя многим клиентам, да и нам — настроил и забыл.


              1. psFitz
                29.03.2017 22:58

                Спасибо, очень интересно, как-то даже не думал раньше об этом, сейчас понимаю, что отличная вещь)


  1. x07
    03.04.2017 12:34

    На данный момент слияния проектов еще не произошло, и пока не ясно, когда произойдет (или как итоговый вариант будет совместим с текущей реализацией)

    Уже произошло https://github.com/angular/universal


    1. NosovK
      03.04.2017 18:33

      Да, особенно радует роадмап:

      — Write documentation for core API (In Progress)

      на самом деле мы уже пытаемся запустится с новым, если получится то выложим к концу недели обновление с работающим новым Universal в компоненты уже.
      + скоро запустим эксперимент с индексацией (делаем мультистор сайта с magento на наших компонентах — сравним индексацию)