Server-side Rendering (SSR): An intro to Angular Universal
A normal Angular application executes in the browser, rendering pages in the DOM in response to user actions. Angular Universal executes on the server, generating static application pages that later get bootstrapped on the client. This means that the application generally renders more quickly, giving users a chance to view the application layout before it becomes fully interactive.
Why use server-side rendering?
There are three main reasons to create a Universal version of your app.
- Facilitate web crawlers through search engine optimization (SEO)
- Improve performance on mobile and low-powered devices
- Show the first page quickly with a first-contentful paint (FCP)
Facilitate web crawlers (SEO)
Google, Bing, Facebook, Twitter, and other social media sites rely on web crawlers to index your application content and make that content searchable on the web. These web crawlers may be unable to navigate and index your highly interactive Angular application as a human user could do.
Angular Universal can generate a static version of your app that is easily searchable, linkable, and navigable without JavaScript. Universal also makes a site preview available since each URL returns a fully rendered page.
Improve performance on mobile and low-powered devices
Some devices don't support JavaScript or execute JavaScript so poorly that the user experience is unacceptable. For these cases, you may require a server-rendered, no-JavaScript version of the app. This version, however limited, may be the only practical alternative for people who otherwise couldn't use the app at all.
Show the first page quickly
Displaying the first page quickly can be critical for user engagement. 53 percent of mobile site visits are abandoned if pages take longer than 3 seconds to load. Your app may have to launch faster to engage these users before they decide to do something else.
With Angular Universal, you can generate landing pages for the app that look like the complete app. The pages are pure HTML, and can display even if JavaScript is disabled. The pages don't handle browser events, but they do support navigation through the site using router Link.
In practice, you'll serve a static version of the landing page to hold the user's attention. At the same time, you'll load the full Angular app behind it. The user perceives near-instant performance from the landing page and gets the full interactive experience after the full app loads.
Universal web servers
A Universal web server responds to application page requests with static HTML rendered by the Universal template engine. The server receives and responds to HTTP requests from clients (usually browsers), and serves static assets such as scripts, CSS, and images. It may respond to data requests, either directly or as a proxy to a separate data server.
The sample web server for this guide is based on the popular Express framework.
Note: Any web server technology can serve a Universal app as long as it can call Universal's renderModuleFactory() function. The principles and decision points discussed here apply to any web server technology.
Universal applications use the Angular platform-server package (as opposed to platform-browser), which provides server implementations of the DOM, XMLHttpRequest, and other low-level features that don't rely on a browser.
The server (Node Express) passes client requests for application pages to the NgUniversal ngExpressEngine. Under the hood, this calls Universal's renderModuleFactory() function, while providing caching and other helpful utilities.
The renderModuleFactory() function takes as inputs a template HTML page (usually index.html), an Angular module containing components, and a route that determines which components to display. The route comes from the client's request to the server.
Each request results in the appropriate view for the requested route. The renderModuleFactory() function renders the view within the
Finally, the server returns the rendered page to the client.
Working around the browser APIs
Because a Universal app doesn't execute in the browser, some of the browser APIs and capabilities may be missing on the server.
For example, server-side applications can't reference browser-only global objects such as window, document, navigator, or location.
Angular provides some injectable abstractions over these objects, such as Location or DOCUMENT; it may substitute adequately for these APIs. If Angular doesn't provide it, it's possible to write new abstractions that delegate to the browser APIs while in the browser and to an alternative implementation while on the server (aka shimming).
Similarly, without mouse or keyboard events, a server-side app can't rely on a user clicking a button to show a component. The app must determine what to render based solely on the incoming client request. This is a good argument for making the app routable.
Using absolute URLs for server requests
In regular Angular applications, requests are sent to relative URLs such as api/heroes. In a Universal app, HTTP URLs must be absolute (for example, https://my-server.com/api/heroes). This means you need to change your services to make requests with absolute URLs when running on the server and with relative URLs when running in the browser.
One solution is to provide the full URL to your application on the server and write an interceptor that can retrieve this value and prepend it to the request URL. If you're using the ngExpressEngine, as shown in the example in this guide, half the work is already done. We'll assume this is the case, but it's trivial to provide the same functionality.
Start by creating an HttpInterceptor:
import {Injectable, Inject, Optional} from '@angular/core';
import {HttpInterceptor, HttpHandler, HttpRequest, HttpHeaders} from '@angular/common/http';
import {Request} from 'express';
import {REQUEST} from '@nguniversal/express-engine/tokens';
@Injectable()
export class UniversalInterceptor implements HttpInterceptor {
constructor(@Optional() @Inject(REQUEST) protected request: Request) {}
intercept(req: HttpRequest, next: HttpHandler) {
let serverReq: HttpRequest = req;
if (this.request) {
let newUrl = `${this.request.protocol}://${this.request.get('host')}`;
if (!req.url.startsWith('/')) {
newUrl += '/';
}
newUrl += req.url;
serverReq = req.clone({url: newUrl});
}
return next.handle(serverReq);
}
}
Live Demo:
It is just a code snippet explaining a particular concept and may not have any output
See the Pen HttpInterceptor by w3resource (@w3resource) on CodePen.
Next, provide the interceptor in the providers for the server AppModule (app.server.module.ts):
import {HTTP_INTERCEPTORS} from '@angular/common/http';
import {UniversalInterceptor} from './universal-interceptor';
@NgModule({
...
providers: [{
provide: HTTP_INTERCEPTORS,
useClass: UniversalInterceptor,
multi: true
}],
})
export class AppServerModule {}
Live Demo:
It is just a code snippet explaining a particular concept and may not have any output
See the Pen app.server.module.ts by w3resource (@w3resource) on CodePen.
Now, on every HTTP request made on the server, this interceptor will fire and replace the request URL with the absolute URL provided in the Express Request object.
Universal template engine
The important bit in the server.ts file is the ngExpressEngine() function.
app.engine('html', ngExpressEngine({
bootstrap: AppServerModuleNgFactory,
providers: [
provideModuleMap(LAZY_MODULE_MAP)
]
}));
Live Demo:
It is just a code snippet explaining a particular concept and may not have any output
See the Pen server.ts by w3resource (@w3resource) on CodePen.
The ngExpressEngine() function is a wrapper around Universal's renderModuleFactory() function which turns a client's requests into server-rendered HTML pages.
- The first parameter is AppServerModule. It's the bridge between the Universal server-side renderer and the Angular application.
- The second parameter, extraProviders, is optional. It lets you specify dependency providers that apply only when running on this server. You can do this when your app needs information that can only be determined by the currently running server instance. One example could be the running server's origin, which could be used to calculate absolute HTTP URLs if not using the Request token as shown above.
The ngExpressEngine() function returns a Promise callback that resolves to the rendered page. It's up to the engine to decide what to do with that page. This engine's Promise callback returns the rendered page to the web server, which then forwards it to the client in the HTTP response.
Note: These wrappers help hide the complexity of the renderModuleFactory() function. There are more wrappers for different backend technologies at the Universal repository.
Filtering request URLs
NOTE: the basic behavior described below is handled automatically when using the NgUniversal Express schematic, this is helpful when trying to understand the underlying behavior or replicate it without using the schematic.
The web server must distinguish app page requests from other kinds of requests.
It's not as simple as intercepting a request to the root address /. The browser could ask for one of the application routes such as /dashboard, /heroes, or /detail:12. In fact, if the app were only rendered by the server, every app link clicked would arrive at the server as a navigation URL intended for the router.
Fortunately, application routes have something in common: their URLs lack file extensions. (Data requests also lack extensions but they're easy to recognize because they always begin with /api.) All static asset requests have a file extension (such as main.js or /node_modules/zone.js/dist/zone.
Because we use routing, we can easily recognize the three types of requests and handle them differently.
- Data request: request URL that begins /api.
- App navigation: request URL with no file extension.
- Static asset: all other requests.
A Node Express server is a pipeline of middleware that filters and processes request one after the other. You configure the Node Express server pipeline with calls to app.get() like this one for data requests.
// TODO: implement data requests securely
app.get('/api/*', (req, res) => {
res.status(404).send('data requests are not supported');
});
Live Demo:
It is just a code snippet explaining a particular concept and may not have any output
See the Pen server.ts (data URL) by w3resource (@w3resource) on CodePen.
Note: This sample server doesn't handle data requests.
The tutorial's "in-memory web API" module, a demo and development tool, intercepts all HTTP calls and simulates the behavior of a remote data server. In practice, you would remove that module and register your web API middleware on the server here.
The following code filters for request URLs with no extensions and treats them as navigation requests.
// All regular routes use the Universal engine
app.get('*', (req, res) => {
res.render('index', { req });
});
Live Demo:
It is just a code snippet explaining a particular concept and may not have any output
See the Pen server.ts (navigation) by w3resource (@w3resource) on CodePen.
Serving static files safely
A single app.use() treats all other URLs as requests for static assets such as JavaScript, image, and style files.
To ensure that clients can only download the files that they are permitted to see, put all client-facing asset files in the /dist folder and only honor requests for files from the /dist folder.
The following Node Express code routes all remaining requests to /dist, and returns a 404 - NOT FOUND error if the file isn't found.
// Server static files from /browser
app.get('*.*', express.static(join(DIST_FOLDER, 'browser')));
Live Demo:
It is just a code snippet explaining a particular concept and may not have any output
See the Pen server.ts (static files) by w3resource (@w3resource) on CodePen.
Previous: Upgrading from AngularJS to Angular
Next:
Internationalization (i18n)
It will be nice if you may share this link in any developer community or anywhere else, from where other developers may find this content. Thanks.
https://www.w3resource.com/angular/server-side-rendering-an-intro-to-angular-universal.php
- Weekly Trends and Language Statistics
- Weekly Trends and Language Statistics