Дружим Angular с Google (Angular Universal)
- вторник, 28 марта 2017 г. в 03:15:29
Когда мы говорим про современные интернет магазины, мы представляем себе тяжелые для понимания серверы, рендрящие тысячи статических страничек. Причем именно эти тысячи отрендеренных страниц одна из причин, почему Single Page Applications не прижились в электронной коммерции. Даже крупнейшие магазины электронной коммерции по-прежнему выглядят как куча статических страниц. Для пользователя это нескончаемый цикл кликов, ожиданий и перезагрузки страниц.
Одностраничные приложения приятно отличаются динамичностью взаимодействия с пользователем и более сложным UX. Но как не прискорбно обычно пользовательский комфорт приносится в жертву SEO оптимизации. Для сеошника сайт на angular – это своего рода проблема, поскольку поисковикам трудно индексировать страницы с динамическим контентом.
Другой недостаток SPA
— это превью сайта. Например, пользователь только-что купил новый телевизор в нашем интернет-магазине и хочет порекомендовать его своим друзьям в соцсетях. Превью ссылки в случае Angular
будет выглядеть так:
Мы любим JS и Angular. Мы верим, что классный и удобный UX может быть построен на этом стеке технологий, и мы можем решить все сопутствующие проблемы. В какой-то момент мы столкнулись с Angular Universal
. Это модуль Angular
для рендеринга на стороне сервера. Сначала нам показалось, вот оно – решение! Но радость была преждевременной — и отсутствие больших проектов с его применением тому доказательство.
В итоге, мы начали разрабатывать компоненты для интернет-магазина, используя обычный Angular 2
, и ждали, когда Universal
будет объединен с Angular Core
. На данный момент слияния проектов еще не произошло, и пока не ясно, когда произойдет (или как итоговый вариант будет совместим с текущей реализацией), однако сам Universal
уже перекочевал в github репозиторий Angular
.
Несмотря на эти трудности, наша цель осталась неизменной — создавать классные веб-приложения на Angular
с серверным рендерингом для индексации поисковиками. В результате наша команда разработала более 20 универсальных компонентов для быстрой сборки интернет-магазина. Как в итоге это было достигнуто?
Прежде всего, давайте обсудим, что такое 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
заботится о серверном рендеринге.
До этого момента все выглядит неплохо? Возможно, но дьявол скрыт в мелочах, как всегда.
Итак, мы хотим поделиться с вами подводными камнями, с которыми мы столкнулись на нашем пути.
Когда мы начали тестировать компоненты нашего магазина с помощью 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
приложений из-за большого размера самой библиотеки).