Angular Universal

Angular provides a front – to – back solution for server-side rendering. It is Angular Universal, a technology for running Angular applications on the server.

A standard Angular application executes in a browser, rendering pages in the DOM in response to user actions.

Angular Universal generates static application pages on the server side through a process called server-side rendering.

It can generate these pages and use them directly in response to a browser request. It can also prerepresent pages as HTML files and make them available to the server as static files.

The working principle of

To make a Universal application, install the platform-server package. The platform-Server package provides server-side DOM implementation, XMLHttpRequest, and other low-level features, but no longer relies on the browser.

You compile the client application using the platform-server module instead of the platform-Browser module, and run the Universal application on a Web server.

The server (the Node Express server used in the following example) passes client requests to the application page to the renderModuleFactory function.

The renderModuleFactory function takes as input a template HTML page (usually index.html), an Angular module containing components, and a route to determine which components to display.

The route is sent to the server from the client’s request. Each request gives an appropriate view of the requested route.

The renderModuleFactory renders the view in the

tag in the template and creates a finished HTML page for the client.

Finally, the server returns the rendered page to the client.

Why server side rendering

Three main reasons:

  1. Help web crawlers (SEO)

  2. Improve performance on mobile phones and low-power devices

  3. The first home page is quickly displayed

Help web crawlers (SEO)

Google, Bing, Baidu, Facebook, Twitter and other search engines or social media sites all rely on web crawlers to index your app’s content and make it searchable on the Web.

These web crawlers may not navigate and index your highly interactive Angular app the way humans do.

Angular Universal generates a static version of your app that is searchable, linkable, and browsable without JavaScript. It also allows the site to be previewed, as each URL returns a fully rendered page.

Enabling web crawlers is often referred to as search engine optimization (SEO).

Improve performance on mobile phones and low-power devices

Some devices do not support JavaScript or perform JavaScript poorly, resulting in an unacceptable user experience. For these cases, you’ll probably need a server-side rendered, javasjavascript free version of the app. There are some limitations, but this version may be the only option for those who have no access to the app at all.

Quick Display of home page

Displaying the front page quickly is critical to attracting users.

If pages take longer than three seconds to load, 53% of mobile sites are abandoned. Your app needs to launch faster to grab users’ attention before they decide to do something else.

With Angular Universal, you can generate “landing pages” for your app that look just like the full app. These landing pages are pure HTML and display even if JavaScript is disabled. These pages do not handle browser events, but they can be navigated through the site using routerLink.

In practice, you might want to use a static version of the landing page to keep the user focused. You also load the full Angular app behind the scenes. The user thinks the landing page appears almost immediately, and when the full application is loaded, there is a full interactive experience.

The sample analysis

This is based on my example project angular-universal-Starter on GitHub.

This project, like the sample project in the first article, is built on the Angular CLI, so the only difference is the configuration required for server-side rendering.

Installation tools

Before you can start, the following packages must be installed (the sample projects are all configured, just NPM install) :

  • @angular/platform-server– Server component of Universal.
  • @nguniversal/module-map-ngfactory-loader– Used to handle lazy loading in server rendering environments.
  • @nguniversal/express-engine-Universal appExpress engine.
  • ts-loader– Used to translate server applications.
  • express– Node Express server

Install them using the following commands:

npm install --save @angular/platform-server @nguniversal/module-map-ngfactory-loader ts-loader @nguniversal/express-engine express
Copy the code

Project configuration

The configuration work includes:

  1. Create a server application module:src/app/app.server.module.ts
  2. Modify the client application module:src/app/app.module.ts
  3. Create server application bootstrap file:src/main.server.ts
  4. Modify the client application bootstrap file:src/main.ts
  5. Create a TypeScript server configuration:src/tsconfig.server.json
  6. Modify @angular/cli configuration file:.angular-cli.json
  7. Create a Node Express service:server.ts
  8. Create server-side prerender program:prerender.ts
  9. Create a server configuration for Webpack:webpack.server.config.js

1. Create server application module:src/app/app.server.module.ts

import { NgModule } from '@angular/core';
import { ServerModule, ServerTransferStateModule } from '@angular/platform-server';
import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader';

import { AppBrowserModule } from './app.module';
import { AppComponent } from './app.component';

You can register service providers that are specific to running applications in a Universal environment
@NgModule({
	imports: [
		AppBrowserModule, // AppModule of the client application
		ServerModule, // Angular module on the server side
		ModuleMapLoaderModule, // To implement lazy loading of routes on the server side
		ServerTransferStateModule, // Import at the server to transfer state from the server to the client].bootstrap: [AppComponent],
})
export class AppServerModule {}Copy the code

The server application module (conventionally called AppServerModule) is an Angular module that wraps the application root AppModule so that Universal can coordinate between your application and the server. The AppServerModule also tells Angular how to bootstrap your app when it runs in Universal mode.

2. Modify the client application module:src/app/app.module.ts

import { BrowserModule, BrowserTransferStateModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { APP_ID, Inject, NgModule, PLATFORM_ID } from '@angular/core';
import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';
import { TransferHttpCacheModule } from '@nguniversal/common';
import { isPlatformBrowser } from '@angular/common';
import { AppRoutingModule } from './app.routes';

@NgModule({
	imports: [
		AppRoutingModule,
		BrowserModule.withServerTransition({appId: 'my-app'}),
		TransferHttpCacheModule, // It is used to cache requests from the server to the client, preventing the client from repeating requests that the server has completed
		BrowserTransferStateModule, // Import at the client, which is used to transfer state from the server to the client
		HttpClientModule
	],
	declarations: [
		AppComponent,
		HomeComponent
	],
	providers: [].bootstrap: [AppComponent]
})
export class AppBrowserModule {
	constructor(@Inject(PLATFORM_ID) private platformId: Object,
				@Inject(APP_ID) private appId: string) {
		
		// Determine whether the operating environment is client or server
		const platform = isPlatformBrowser(platformId) ? 'in the browser' : 'on the server';
		console.log(`Running ${platform} with appId=${appId}`); }}Copy the code

Will be BrowserModule NgModule metadata import to BrowserModule. WithServerTransition ({appId: ‘my-app’}), Angular adds appId values (which can be any string) to the style name of the server rendered page so that they can be found and removed when the client application starts.

At this point, we can obtain runtime information about the current platform and appId by dependency injection (@inject (PLATFORM_ID) and @inject (APP_ID)) :

constructor(@Inject(PLATFORM_ID) private platformId: Object,
			@Inject(APP_ID) private appId: string) {
	
	// Determine whether the operating environment is client or server
	const platform = isPlatformBrowser(platformId) ? 'in the browser' : 'on the server';
	console.log(`Running ${platform} with appId=${appId}`);
}
Copy the code

Create server application bootstrap file:src/main.server.ts

The file is exported to the server module:

export { AppServerModule } from './app/app.server.module';
Copy the code

4, modify the client application boot program file:src/main.ts

Listen for the DOMContentLoaded event to run our code when the DOMContentLoaded event occurs for the TransferState to work properly

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppBrowserModule } from './app/app.module';
import { environment } from './environments/environment';

if (environment.production) {
	enableProdMode();
}

// Run our code with DOMContentLoaded to make TransferState work properly
document.addEventListener('DOMContentLoaded', () => {
	platformBrowserDynamic().bootstrapModule(AppBrowserModule);
});
Copy the code

Create a TypeScript server configuration:src/tsconfig.server.json

{
  "extends": ".. /tsconfig.json"."compilerOptions": {
    "outDir": ".. /out-tsc/app"."baseUrl": ". /"."module": "commonjs"."types": [
      "node"]},"exclude": [
    "test.ts"."**/*.spec.ts"]."angularCompilerOptions": {
    "entryModule": "app/app.server.module#AppServerModule"}}Copy the code

Json differs from tsconfig.app.json in that:

  • The Module property must be commonJS so that it can be imported into your server application by the require() method.

  • The angularCompilerOptions section has some options for AOT compilers:

    • EntryModule – the root module of the server application in the format of path/to/file#ClassName.

Modify @angular/cli configuration file:.angular-cli.json

Under apps add:

{
    "platform": "server"."root": "src"."outDir": "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": ""."styles": [
      "styles.scss"]."scripts": []."environmentSource": "environments/environment.ts"."environments": {
      "dev": "environments/environment.ts"."prod": "environments/environment.prod.ts"}}Copy the code

Create Node Express service:server.ts

import 'zone.js/dist/zone-node';
import 'reflect-metadata';
import { enableProdMode } from '@angular/core';

import * as express from 'express';
import { join } from 'path';
import { readFileSync } from 'fs';

// Faster server renders w/ Prod mode (dev mode never needed)
enableProdMode();

// Express server
const app = express();

const PORT = process.env.PORT || 4000;
const DIST_FOLDER = join(process.cwd(), 'dist');

// Our index.html we'll use as our template
const template = readFileSync(join(DIST_FOLDER, 'browser'.'index.html')).toString();

// * NOTE :: leave this as require() since this file is built Dynamically from webpack
const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = require('./dist/server/main.bundle');

// Express Engine
import { ngExpressEngine } from '@nguniversal/express-engine';
// Import module map for lazy loading
import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader';

// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
app.engine('html', ngExpressEngine({
	bootstrap: AppServerModuleNgFactory,
	providers: [
		provideModuleMap(LAZY_MODULE_MAP)
	]
}));

app.set('view engine'.'html');
app.set('views', join(DIST_FOLDER, 'browser'));

/* - Example Express Rest API endpoints - app.get('/api/**', (req, res) => { }); * /

// Server static files from /browser
app.get('*. *', express.static(join(DIST_FOLDER, 'browser'), {
	maxAge: '1y'
}));

// ALl regular routes use the Universal engine
app.get(The '*', (req, res) => {
	res.render('index', {req});
});

// Start up the Node server
app.listen(PORT, () => {
	console.log(`Node Express server listening on http://localhost:${PORT}`);
});
Copy the code
Universal template engine

The most important part of this file is the ngExpressEngine function:

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

NgExpressEngine is a wrapper around Universal’s renderModuleFactory function. It converts the client request into an HTML page rendered by the server. If you use a different server-side technology than Node, you will need to call this function in the server’s template engine.

  • The first parameter is the AppServerModule you wrote earlier. It is the bridge between the Universal server renderer and your application.

  • The second parameter is extraProviders. It is an optional Angular dependency injection provider that is required to run on the server. Provide the extraProviders parameter when your application needs information that is only needed when running in a server instance.

The ngExpressEngine function returns a Promise that is resolved into a rendered page.

Next your engine has to decide what to do with the page. Now the engine’s callback returns the rendered page to the Web server, which then forwards it to the client via an HTTP response.

8, create server-side prerender program:prerender.ts

// Load zone.js for the server.
import 'zone.js/dist/zone-node';
import 'reflect-metadata';
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
import { join } from 'path';

import { enableProdMode } from '@angular/core';
// Faster server renders w/ Prod mode (dev mode never needed)
enableProdMode();

// Import module map for lazy loading
import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader';
import { renderModuleFactory } from '@angular/platform-server';
import { ROUTES } from './static.paths';

// * NOTE :: leave this as require() since this file is built Dynamically from webpack
const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = require('./dist/server/main.bundle');

const BROWSER_FOLDER = join(process.cwd(), 'browser');

// Load the index.html file containing referances to your application bundle.
const index = readFileSync(join('browser'.'index.html'), 'utf8');

let previousRender = Promise.resolve();

// Iterate each route path
ROUTES.forEach(route= > {
	const fullPath = join(BROWSER_FOLDER, route);

	// Make sure the directory structure is there
	if(! existsSync(fullPath)) { mkdirSync(fullPath); }// Writes rendered HTML to index.html, replacing the file if it already exists.
	previousRender = previousRender.then(_= > renderModuleFactory(AppServerModuleNgFactory, {
		document: index,
		url: route,
		extraProviders: [
			provideModuleMap(LAZY_MODULE_MAP)
		]
	})).then(html= > writeFileSync(join(fullPath, 'index.html'), html));
});
Copy the code

Create server configuration for Webpack:webpack.server.config.js

The Universal app does not require any additional Webpack configuration; the Angular CLI handles them for us. However, since the Node Express service for this example is a TypeScript application (server.ts and prerender.ts), we use Webpack to translate it. Webpack configuration is not discussed here, you need to go to Webpack official website

// Work around for https://github.com/angular/angular-cli/issues/7200

const path = require('path');
const webpack = require('webpack');

module.exports = {
    entry: {
        server: './server.ts'.// This is our Express server for Dynamic universal
        prerender: './prerender.ts' // This is an example of Static prerendering (generative)
    },
    target: 'node'.resolve: {extensions: ['.ts'.'.js']},
    externals: [/(node_modules|main\.. *\.js)/,].// Make sure we include all node_modules etc
    output: {
        path: path.join(__dirname, 'dist'), // Puts the output at the root of the dist folder
        filename: '[name].js'
    },
    module: {
        rules: [{test: /\.ts$/.loader: 'ts-loader'}},plugins: [
        new webpack.ContextReplacementPlugin(
            1 / (+)? angular(\\|\/)core(.+)? /.// fixes WARNING Critical dependency: the request of a dependency is an expression
            path.join(__dirname, 'src'), // location of your src
            {} // a map of your routes
        ),
        new webpack.ContextReplacementPlugin(
            1 / (+)? express(\\|\/)(.+)? /.// fixes WARNING Critical dependency: the request of a dependency is an expression
            path.join(__dirname, 'src'), {})]};Copy the code

Test configuration

With the above configuration, we have created an Angular Universal application that can be rendered on the server side.

Configure build and serve commands in the scripts area of package.json:

{
    "scripts": {
    	"ng": "ng"."start": "ng serve -o"."ssr": "npm run build:ssr && npm run serve:ssr"."prerender": "npm run build:prerender && npm run serve:prerender"."build": "ng build"."build:client-and-server-bundles": "ng build --prod && ng build --prod --app 1 --output-hashing=false"."build:prerender": "npm run build:client-and-server-bundles && npm run webpack:server && npm run generate:prerender"."build:ssr": "npm run build:client-and-server-bundles && npm run webpack:server"."generate:prerender": "cd dist && node prerender"."webpack:server": "webpack --config webpack.server.config.js --progress --colors"."serve:prerender": "cd dist/browser && http-server"."serve:ssr": "node dist/server"}}Copy the code

Development just runsnpm run start

performnpm run ssrCompile the application and start a Node Express to service the applicationhttp://localhost:4000

Dist directory:

Run NPM run prerender – compile the application and pre-render the application files to start a demo HTTP server so that you can view ithttp://localhost:8080

Note: To deploy a static web site to a statically hosted platform, you must deploy the dist/ Browser folder, not the dist folder

Dist directory:

Based on the actual routing information of the project and configured in static.paths.ts of the root directory, it is provided for prerender.ts resolution.

export const ROUTES = [
	'/'.'/lazy'
];
Copy the code

Therefore, as you can see from the dist directory, server prerendering generates the corresponding static index.html in Browser based on the configured route. For example, / corresponds to /index.html, and /lazy corresponds to /lazy/index.html.

Lazy loading of modules on the server

In the previous introduction, we imported the ModuleMapLoaderModule in app.server.module.ts and in app.module.ts.

ModuleMapLoaderModule the ModuleMapLoaderModule allows lazy modules to be rendered on the server, and you only need to import them in app.server.module.ts.

State transfer from server to client

In front of the introduction, we in the app. The server module. Ts import ServerTransferStateModule, In the app. The module. Ts import BrowserTransferStateModule and TransferHttpCacheModule.

All three modules are concerned with state transfer from server to client:

  • ServerTransferStateModule: Imported on the server to transfer state from the server to the client
  • BrowserTransferStateModule: Imported on the client to transfer state from the server to the client
  • TransferHttpCacheModule: Cache the request transfer from the server to the client, preventing the client from repeatedly requesting the completed request from the server

Using these modules, it is possible to solve the problem that HTTP requests are requested at the server and client side respectively.

For example, home.component.ts has the following code:

import { Component, OnDestroy, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';

@Component({
	selector: 'app-home'.templateUrl: './home.component.html'.styleUrls: ['./home.component.scss']})export class HomeComponent implements OnInit.OnDestroy {
    constructor(public http: HttpClient) {
	}
	
    ngOnInit() {
    	this.poiSearch(this.keyword, 'Beijing').subscribe((data: any) = > {
    		console.log(data); }); } ngOnDestroy() { } poiSearch(text: string, city? : string): Observable<any> {return this.http.get(encodeURI(`http://restapi.amap.com/v3/place/text?keywords=${text}&city=${city}&offset=20&key=55f909211b9950837fba2c71d0488db9&extensions=all`)); }}Copy the code

After the code runs,

The server requests and prints:

The client requests again and prints:

Method 1: UseTransferHttpCacheModule

Using TransferHttpCacheModule is simple and does not require any code changes. After importing in app.module.ts, Angular automatically caches server requests to the client. In other words, the data from the server request is automatically transmitted to the client, and the client receives the data and doesn’t send any more requests.

Method 2: UseBrowserTransferStateModule

This method is slightly more complicated and requires some code changes.

Adjust the home.component.ts code as follows:

import { Component, OnDestroy, OnInit } from '@angular/core';
import { makeStateKey, TransferState } from '@angular/platform-browser';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';

const KFCLIST_KEY = makeStateKey('kfcList');

@Component({
	selector: 'app-home'.templateUrl: './home.component.html'.styleUrls: ['./home.component.scss']})export class HomeComponent implements OnInit.OnDestroy {
    constructor(public http: HttpClient,
				private state: TransferState) {
	}
	
    ngOnInit() {
    
    	// Use a flag to distinguish whether the server has received the data. If the server has not received the data, it requests the client. If the server has received the data, it does not send the request
		constKfcList: any [] =this.state.get(KFCLIST_KEY, null as any);

		if (!this.kfcList) {
			this.poiSearch(this.keyword, 'Beijing').subscribe((data: any) = > {
				console.log(data);
				this.state.set(KFCLIST_KEY, data as any); // Store data
			});
		}
    }
    
    ngOnDestroy() {
        if (typeof window= = ='object') {
			this.state.set(KFCLIST_KEY, null as any); // Delete data} } poiSearch(text: string, city? : string): Observable<any> {return this.http.get(encodeURI(`http://restapi.amap.com/v3/place/text?keywords=${text}&city=${city}&offset=20&key=55f909211b9950837fba2c71d0488db9&extensions=all`)); }}Copy the code
  • useconst KFCLIST_KEY = makeStateKey('kfcList')Create a StateKey to store the transferred data
  • inHomeComponentInto the constructor ofTransferState
  • inngOnInitAccording to thethis.state.get(KFCLIST_KEY, null as any)Determine whether the data exists (whether server or client). If it exists, no more requests are made. If it does not exist, the data is requested and passedthis.state.set(KFCLIST_KEY, data as any)Store and transfer data
  • inngOnDestroyTo determine whether to delete the stored data according to the current client

Client and server render comparisons

Finally, we make a comparison based on the three reasons:

  1. Help web crawlers (SEO)

  2. Improve performance on mobile phones and low-power devices

  3. The home page quickly displays

Help web crawlers (SEO)

Client rendering:

Server render:

As you can see above, the server renders the information to the returned page in advance so that the web crawler can get the information directly (web crawlers rarely parse javascript).

Improve performance on mobile phones and low-power devices

The reason for this is that, on some low-end devices, it is better to display the page directly than to parse javascript.

The home page quickly displays

The Fast 3G network is also tested

Client rendering:

Server render:

Keep a few things in mind

  • For server packages, you may need to include third-party modules in the nodeExternals whitelist

  • Window, Document, Navigator, and other browser types – which do not exist on the server – will not work on the server if you use them directly. Here are a few ways to make your code work:

    • Can be achieved byPLATFORM_IDToken injectedObjectTo check whether the current platform is a browser or a server, and then use the browser-side specific type
    import { PLATFORM_ID } from '@angular/core'; import { isPlatformBrowser, isPlatformServer } from '@angular/common'; constructor(@Inject(PLATFORM_ID) private platformId: Object) { ... } ngOnInit() {if (isPlatformBrowser(this.platformId)) {// Only browser code... } if (isPlatformServer(this.platformId)) {// The code that runs only on the server side... }}Copy the code
    • Try to limit or avoid using setTimeout. It slows down the server-side rendering process. Be sure to remove them in the component’s ngOnDestroy

    • For RxJs timeouts, be sure to unstream them on success, as they also slow down rendering.

  • Instead of working directly with nativeElement, use Renderer2 so that you can change the application view across platforms.

constructor(element: ElementRef, renderer: Renderer2) {
  this.renderer.setStyle(element.nativeElement, 'font-size'.'x-large');
}
Copy the code
  • Resolve an issue where the application runs XHR requests on the server and again on the client
    • Using caching from server to client (TransferState)
  • Have a clear understanding of DOM related properties and the differences between properties
  • Make instructions as stateless as possible. For stateful directives, you might need to provide an attribute that reflects the initial string value of the corresponding attribute, such as the URL in the IMG tag. For our native element, the SRC attribute is reflected as the SRC attribute of the element type HTMLImageElement

Please indicate the source of reprint, thank you!