maciej treder - files.speakerdeck.com€¦ · a medicine for the seo/cdn issues maciej treder...

Post on 23-Jul-2020

13 Views

Category:

Documents

0 Downloads

Preview:

Click to see full reader

TRANSCRIPT

Summer. Sea. JavaScript.

Angular Universal A medicine for the SEO/CDN issues

Maciej Treder

@maciejtreder

Outline

• SPA pitfall

• Server-side rendering

• Server vs. Browser

• API optimization

• Deployment

• Prerendering & Summary

SPA pitfall SSR Server vs. Browser APIs Deploy Summary

SPA pitfall

ng build

• Ahead of Time compilation

Server vs.

—prod flag

• Ahead of Time compilation

• Minified

• Tree-shaked

SPA pitfallServer vs.

ng build vs. —prod

SPA pitfallServer vs.

SPA problem<IfModule mod_rewrite.c> RewriteEngine On RewriteBase / RewriteRule ^index\.html$ - [L] RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . index.html [L]</IfModule>

.htaccess

SPA pitfallServer vs.

SPA Problem

GET /

GET /anotherPage

index.html

GET /s

ubpa

ge

GET /contact

GET /home

SPA pitfallServer vs.

SPA Problem

GET / GET /anotherPage

SPA pitfallServer vs.

SPA Problem

SPA pitfallServer vs.

Server Side Rendering

SSRServer vs.

Server Side Rendering

GET /GET /anotherPage

SSRServer vs.

Is it worth?curl localhost:8080 <!DOCTYPE html><html lang="en"><head> <meta charset="utf-8"> <title>SomeProject</title> <base href="/">

<meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" type="image/x-icon" href="favicon.ico"> <link rel="stylesheet" href="styles.3ff695c00d717f2d2a11.css"><style ng-transition="app-root"> /*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IiIsImZpbGUiOiJzcmMvYXBwL2FwcC5jb21wb25lbnQuY3NzIn0= */</style></head> <body>

<script type="text/javascript" src="runtime.26209474bfa8dc87a77c.js"></script><script type="text/javascript" src="es2015-polyfills.c5dd28b362270c767b34.js" nomodule=""></script><script type="text/javascript" src="polyfills.8bbb231b43165d65d357.js"></script><script type="text/javascript" src="main.8a9128130a3a38dd7ee5.js"></script>

<script id="app-root-state" type="application/json">{}</script></body></html>

<app-root _nghost-sc0="" ng-version="7.2.9"><div _ngcontent-sc0="" style="text-align:center"><h1 _ngcontent-sc0=""> Welcome to someProject! </h1><img _ngcontent-sc0="" alt="Angular Logo" src="data:image/

svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERDAwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJGIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogIDwvc3ZnPg==" width="300"></div><h2 _ngcontent-sc0="">Here are some links to help you start: </h2><ul _ngcontent-sc0=""><li

_ngcontent-sc0=""><h2 _ngcontent-sc0=""><a _ngcontent-sc0="" href="https://angular.io/tutorial" rel="noopener" target="_blank">Tour of Heroes</a></h2></li><li _ngcontent-sc0=""><h2 _ngcontent-sc0=""><a _ngcontent-sc0="" href="https://angular.io/cli" rel="noopener" target="_blank">CLI Documentation</a></h2></li><li _ngcontent-sc0=""><h2 _ngcontent-sc0=""><a _ngcontent-sc0="" href="https://blog.angular.io/" rel="noopener" target="_blank">Angular blog</a></h2></li></ul></app-root>

SSRServer vs.

SSRServer vs.

—prod vs. universal

SSRServer vs.

—prod vs. universal

Load HTML Bootstrap

Load HTML Bootstrap

SSR

NO SSR First meaningful paint

First meaningful paint

SSRServer vs.

How to start?

Official guide

https://angular.io/guide/universal

ng-toolkit

https://github.com/maciejtreder/ng-toolkit

SSRServer vs.

ng add @nguniversal/express-engine

CREATE src/main.server.ts (220 bytes) CREATE src/app/app.server.module.ts (318 bytes) CREATE src/tsconfig.server.json (219 bytes) CREATE webpack.server.config.js (1360 bytes) CREATE server.ts (1500 bytes) UPDATE package.json (1876 bytes) UPDATE angular.json (4411 bytes) UPDATE src/main.ts (432 bytes) UPDATE src/app/app.module.ts (359 bytes)

SSRServer vs.

Adjust your modulesapp.module.ts app.server.module.ts

@NgModule({ bootstrap: [AppComponent], imports: [ BrowserModule.withServerTransition({appId: 'my-app'}), //other imports ],

})export class AppModule {}

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: [ AppModule, ServerModule, ModuleMapLoaderModule ], bootstrap: [AppComponent],})export class AppServerModule {}

SSRServer vs.

Adjust your modules

Official guide

app.module.ts app.server.module.ts @NgModule({ declarations: [AppComponent], imports: [ //common imports ]})export class AppModule {}

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: [ AppModule, ServerModule, ModuleMapLoaderModule, //server specific imports ], bootstrap: [AppComponent],})export class AppServerModule {}

app.browser.module.ts @NgModule({ bootstrap: [AppComponent], imports: [ AppModule, BrowserModule.withServerTransition({appId: 'my-app'}), //browser specific imports ]})export class AppModule {}

//browser specific imports

//server specific imports

SSRServer vs.

ng add @ng-toolkit/universal

CREATE local.js (248 bytes) CREATE server.ts (1546 bytes) CREATE webpack.server.config.js (1214 bytes) CREATE src/main.server.ts (249 bytes) CREATE src/tsconfig.server.json (485 bytes) CREATE src/app/app.browser.module.ts (395 bytes) CREATE src/app/app.server.module.ts (788 bytes) CREATE ng-toolkit.json (95 bytes) UPDATE package.json (1840 bytes) UPDATE angular.json (4022 bytes) UPDATE src/app/app.module.ts (417 bytes) UPDATE src/main.ts (447 bytes)

SSRServer vs.

And let’s go!

• npm run build:prod

• npm run server

Date: 2018-11-21T13:04:33.302Z Hash: 1a82cb687d2e22b5d12b Time: 10752ms chunk {0} runtime.ec2944dd8b20ec099bf3.js (runtime) 1.41 kB [entry] [rendered] chunk {1} main.09093ffa4ad7f66bc6ff.js (main) 169 kB [initial] [rendered] chunk {2} polyfills.c6871e56cb80756a5498.js (polyfills) 37.5 kB [initial] [rendered] chunk {3} styles.3bb2a9d4949b7dc120a9.css (styles) 0 bytes [initial] [rendered]

> my-app@0.0.0 server /Users/mtreder/myApp > node local.js

Listening on: http://localhost:8080

SSRServer vs.

Under the hood

export const app = express();

app.use(compression()); app.use(cors()); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true }));

const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = require('./dist/server/main');

app.engine('html', ngExpressEngine({ bootstrap: AppServerModuleNgFactory, providers: [ provideModuleMap(LAZY_MODULE_MAP) ] }));

server.ts

SSRServer vs.

Under the hood

app.get('/*', (req, res) => { res.render('index', {req, res}, (err, html) => { if (html) { res.send(html); } else { console.error(err); res.send(err); } }); });

server.ts

SSRServer vs.

Under the hood

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

app.get('*.*', express.static('./dist/browser', { maxAge: '1y' }));

server.ts

SSRServer vs.

Server Side Rendering

GET /GET /anotherPage

SSRServer vs.

ng-toolkit

SSRServer vs.

Browser vs. Server

• document

• window

• navigator

• file system

• request

Server vs. Browser

Browser vs. Serverpublic ngOnInit(): void { console.log(window.navigator.language); }

Listening on: http://localhost:8080 ERROR ReferenceError: window is not defined at AppComponent.module.exports../src/app/app.component.ts.AppComponent.ngOnInit (/Users/mtreder/myApp/dist/server.js:118857:21) at checkAndUpdateDirectiveInline (/Users/mtreder/myApp/dist/server.js:19504:19) at checkAndUpdateNodeInline (/Users/mtreder/myApp/dist/server.js:20768:20) at checkAndUpdateNode (/Users/mtreder/myApp/dist/server.js:20730:16) at prodCheckAndUpdateNode (/Users/mtreder/myApp/dist/server.js:21271:5) at Object.updateDirectives (/Users/mtreder/myApp/dist/server.js:118833:264) at Object.updateDirectives (/Users/mtreder/myApp/dist/server.js:21059:72) at Object.checkAndUpdateView (/Users/mtreder/myApp/dist/server.js:20712:14) at ViewRef_.module.exports.ViewRef_.detectChanges (/Users/mtreder/myApp/dist/server.js:19093:22) at /Users/mtreder/myApp/dist/server.js:15755:63

Server vs. Browser

server? browser?import { Component, Inject, PLATFORM_ID, OnInit } from '@angular/core'; import { isPlatformBrowser, isPlatformServer } from '@angular/common';

@Component({ selector: 'home-view', templateUrl: './home.component.html' }) export class HomeComponent implements OnInit {

constructor( private platformId) {} public ngOnInit(): void { if ( ) { console.log('I am executed in the browser!’); // window.url can be reached here }

if (isPlatformServer(this.platformId)) { console.log('I am executed in the server!’); // window.url CAN’T be reached here } } }

Server vs. Browser

Wrapper Service

• Determine if we are in the browser or server

• Retrieve window or request object

• Create ‘mock’ window based on request object if necessary

Server vs. Browser

REQUESTimport { Component, OnInit, Inject, PLATFORM_ID, Optional } from ‘@angular/core’; import { REQUEST } from '@nguniversal/express-engine/tokens'; import { isPlatformServer } from '@angular/common';

@Component({ selector: 'app-root', templateUrl: './app.component.html', }) export class AppComponent implements OnInit {

constructor( @Inject(REQUEST) private request: any, @Inject(PLATFORM_ID) private platformId: any) {} public ngOnInit(): void { if (isPlatformServer(this.platformId)) { console.log(this.request.headers); } } }

import { REQUEST } from '@nguniversal/express-engine/tokens';

@Optional @Inject(REQUEST) private request: any,

console.log(this.request.headers);

Server vs. Browser

REQUEST

Listening on: http://localhost:8080 { host: 'localhost:8080', connection: 'keep-alive', 'cache-control': 'max-age=0', 'upgrade-insecure-requests': '1', 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36', accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'en-US,en;q=0.9,ru;q=0.8', 'if-none-match': 'W/"40e-JviTST4QyiABJz2Lg+QxzZtiXv8"' } 'accept-language': 'en-US,en;q=0.9,ru;q=0.8',

Server vs. Browser

Wrapper Service@Injectable() export class WindowService { private _window: Window; constructor(@Inject(PLATFORM_ID) platformId: any, @Optional @Inject(REQUEST) private request: any ) { if (isPlatformServer(platformId)) { this._window = { navigator: { language: this.request.headers['accept-language'] }, URL: this.request.headers.host + '' + this.request.url };

} else { this._window = window; } }

get window(): any { return this._window; } }

Server vs. Browser

Wrapper Service

import { Component , OnInit, Inject} from '@angular/core'; import { WINDOW } from '@ng-toolkit/universal';

export class AppComponent implements OnInit {

constructor(@Inject(WINDOW) private window: Window) {}

public ngOnInit(): void { console.log(window.navigator.language); } }

app.component.ts

console.log(this.window.navigator.language);

Server vs. Browser

@ng-toolkit/universal

import { NgtUniversalModule } from '@ng-toolkit/universal'; import { NgModule } from '@angular/core';

@NgModule({ imports:[ NgtUniversalModule ] }) export class AppModule { }

app.module.ts

Server vs. Browser

Server/Browser modules

@ngx-translate

• i18n module

• multiple ways of usage

{{‘Welcome to' | translate}}

<div [innerHTML]="'HELLO' | translate"></div>

{

"Welcome to": "Ласкаво просимо в"

}

uk.json

Server vs. Browser

Server/Browser modules

import { TranslateHttpLoader } from '@ngx-translate/http-loader'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core';

export function HttpLoaderFactory(http: HttpClient) { return new TranslateHttpLoader(http); }

@NgModule({ imports:[ TranslateModule.forRoot({ loader: {provide: TranslateLoader, useFactory: HttpLoaderFactory, deps: [httpClient]} }) ] }) export class AppBrowserModule {}

export function httpLoaderFactory(http: HttpClient): TranslateLoader { return new TranslateHttpLoader(http); }

HttpLoaderFactory,

app.browser.module.ts

Server vs. Browser

Server/Browser modulesimport { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { Observable, Observer } from 'rxjs'; import * as fs from 'fs';

export function universalLoader(): TranslateLoader { return { getTranslation: (lang: string) => { return Observable.create((observer: Observer<any>) => { observer.next(JSON.parse(fs.readFileSync(`./dist/assets/i18n/${lang}.json`, 'utf8'))); observer.complete(); }); } } as TranslateLoader; }

@NgModule({ imports:[ TranslateModule.forRoot({ loader: {provide: TranslateLoader, useFactory: universalLoader} }) ] }) export class AppServerModule {}

export function universalLoader(): TranslateLoader {

app.server.module.ts

universalLoader }

Server vs. Browser

Server/Browser modulesimport { Component, OnInit, Inject } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { WINDOW } from '@ng-toolkit/universal';

@Component({ selector: 'app-root', templateUrl: './app.component.html', }) export class AppComponent implements OnInit {

constructor( @Inject(WINDOW) private window: Window, private translateService: TranslateService ) {} public ngOnInit(): void { this.translateService.use(this.window.navigator.language); } }

app.component.ts

this.translateService.use(this.window.navigator.language);

Server vs. Browser

i18n with Universalhttps://www.twilio.com/blog/create-search-engine-friendly-internationalized-web-apps-angular-universal-ngx-translate

Server vs. Browser

Server vs. Browser

API optimization

Server vs. APIs

DRY(c)Don’t repeat your calls

export class AppComponent implements OnInit {

public post: Observable<any>;

constructor(private httpClient: HttpClient) {}

public ngOnInit(): void { this.post = this.httpClient.get('https://jsonplaceholder.typicode.com/posts/1'); } }

Server vs. APIs

2

1

3

45 6

external.api.com

Server vs. APIs

external.api.com

my-website.com

12

3

Server vs. APIs

HttpCacheModulenpm install @nguniversal/common

import { NgtUniversalModule } from '@ng-toolkit/universal'; import { CommonModule } from '@angular/common'; import { HttpClientModule } from '@angular/common/http'; import { TransferHttpCacheModule } from '@nguniversal/common'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component';

@NgModule({ declarations: [ AppComponent ], imports:[ CommonModule, NgtUniversalModule, TransferHttpCacheModule, HttpClientModule ] }) export class AppModule { }

Server vs. APIs

TransferState

• ServerTransferStateModule (@angular/platform-server)

• BrowserTransferStateModule (@angular/platform-browser)

• get(key, fallbackValue)

• set(key, value)

• has(key)

• remove(key)

Server vs. APIs

HTTP_INTERCEPTOR

• Provided in the AppModule

• Every http request made with HttpClient goes threw it

• Used to transform request or response ie:

• Adding authentication headers

Server vs. APIs

HTTP_INTERCEPTOR

@Injectable() export class ServerStateInterceptor implements HttpInterceptor {     constructor(private _transferState: TransferState) {}     intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {         return next.handle(req).pipe(tap(event => {             if (event instanceof HttpResponse) {                 this._transferState.set(makeStateKey(req.url), event.body);             }         }));     } }

Server vs. APIs

HTTP_INTERCEPTOR@Injectable() export class BrowserStateInterceptor implements HttpInterceptor {     constructor(private _transferState: TransferState) { }     intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {         if (req.method !== 'GET') {             return next.handle(req);         }         const storedResponse: string = this._transferState.get(makeStateKey(req.url), null);         if (storedResponse) {             const response = new HttpResponse({ body: storedResponse, status: 200 }); this._transferState.remove(makeStateKey(req.url));             return of(response);         }         return next.handle(req);     } }

Server vs. APIs

HTTP_INTERCEPTORimport {HTTP_INTERCEPTORS } from '@angular/common/http';

providers: [ { provide: HTTP_INTERCEPTORS, useClass: BrowserStateInterceptor, multi: true, } ]

import {HTTP_INTERCEPTORS } from '@angular/common/http';

providers: [ { provide: HTTP_INTERCEPTORS, useClass: ServerStateInterceptor, multi: true, } ]

Server vs. APIs

Performanceexport class RouteResolverService implements Resolve<any> {

constructor( private httpClient: HttpClient, @Inject(PLATFORM_ID) private platformId: any ) {}

public resolve(): Observable<any> {

} }

const watchdog: Observable<number> = timer(500);

if (isPlatformBrowser(this.platformId)) { return this.httpClient.get<any>('https://jsonplaceholder.typicode.com/posts/1'); }

return Observable.create(subject => { this.httpClient.get<any>('https://jsonplaceholder.typicode.com/posts/1') .subscribe(response => { subject.next(response); subject.complete(); });

})

.pipe(takeUntil(watchdog))

watchdog.subscribe(() => { subject.next('timeout'); subject.complete() })

Server vs. APIs

0.5

sec

Server vs. APIs

0.5

sec

Server vs. APIs

https://www.twilio.com/blog/faster-javascript-web-apps-angular-universal-transferstate-api-watchdog

DRY(c) & Performance

Server vs. APIs

Server vs. APIs

Deployment

Server vs. Deploy

Let’s go Serverless!

• Function as a Service

• Event-driven

• Scalable

• Pay for the up-time

Server vs. Deploy

Let’s go Serverless!https://www.twilio.com/blog/angular-universal-javascript-node-js-aws-lambda

Server vs. Deploy

Server vs. Deploy

Prerender

• Generating HTML files at a build time

• Can be hosted from traditional hosting (ie. AWS S3)

• Doesn’t perform dynamic request

• https://github.com/maciejtreder/angular-ssr-prerender

• @ng-toolkit/universal + npm run build:prerender

Server vs. Deploy

Summaryserver-side renderingprerenderng build —prod

SEO

Performance

Difficulty

SEO + external calls

Additional back-end logic

Server vs. Summary

@maciejtreder

top related