Ranjeet Sharma
Head, Digital Solutions Delivery
There has been a lot of talk about Angular in the last few months and how to use it to build client apps, but one of its most important innovations is actually happening on the server.
This is a technology that could help enable a whole new type of web applications: Angular Universal. Let's find out more about it! Let's go over the following topics:
Single page apps have been around for a while, and frameworks like Angular, React or Ember are probably the Javascript libraries that get the most attention in the Javascript world.
The advantages of SPAs are really just one thing
The advantages of single page apps are potentially many:
We can boil this down to only one thing:
Single Page Apps can provide a much better User Experience!
And user experience is critical in the public Internet world. There are numerous studies that prove beyond any doubt that page drop off and user abandonment increase very quickly with page delay.
If a page takes 200ms more to load, this will have a potential high impact on business and sales (see this research by Google). This is even more so on mobile devices where apps tend to be slower.
Given these stats and the fact the SPAs give a much-improved user experience, why isn't everybody using single page apps for everything?
They have been around for years, and any website with a navigation system could benefit from being built that way.
There is one major reason why single page apps are not used everywhere so far (with two separate causes):
Single page apps don't perform well on search engines
The two reasons for that are:
When a single page is retrieved, a search engine will see only very little HTML. Only when the MVC framework kicks in will the page actually be rendered fully using the data obtained from the server.
The problem is that the search engine needs to guess when the Javascript framework finishes rendering the page, so there is a risk of indexing incomplete content.
The second reason for why SPAs don't perform well with search engines is:
Due to the lack of HTML5 History support in browsers, single page apps based their navigation URLs in HTML bookmark anchors (URLs with #, like /home#section1). These are not easily indexed as separate pages by search engines, there are ways to do it but it's a pain and there will always be difficulties getting this indexed right as opposed to using just plain HTML.
The conclusion could be that there is no point in having the most easily navigable site if the way it's built prevents from having good SEO.
The good news is that none of these two reasons are 100% accurate anymore! Google has started to index better single page apps.
And the recent deprecation of IE9 means that HTML5 History is available almost everywhere, making the use of anchor URLs not needed anymore for SPAs, we can just use plain URLs (like /home/section1).
Also, there is still the issue of performance: a single page app will be slower due to the large Javascript amounts that it needs and large startup time, and will therefore perform worse than an HTML-based solution.
And this means page drop offs, this issue will not go away anytime soon especially on mobile. Is there any way to have the best of all worlds, and get both instant navigation, SEO friendliness and high performance on mobile?
The answer is Yes, with Angular Universal.
Simply put, Angular Universal enables us to build apps that have both the performance and user engagement advantages of single page apps combined with the SEO friendliness of static pages.
Angular Universal has several other features other than giving a solution for rendering HTML on the server. Based on the term "server-side rendering", we could think that what Angular Universal does is similar to, for example, a server-side template language like Jade. But there is a lot more functionality to it.
With Angular Universal, you get that initial HTML payload rendered on the server, but you also boot a trimmed down version of Angular on the client and from there Angular takes over the page as a single page app, generating from there all the HTML on the client instead of the server.
So the end result that you get is the same, it's a running single page application, but now because you got the initial HTML payload from the server you get a much better startup time and also a fully SEO indexable app.
Another possibility that Angular Universal opens is pre-rendering of content that does not frequently change at build time. This will be possible using either Grunt, Gulp or Webpack plugins. This is what a Gulp configuration will look like, that pre-renders the content of our application to a file:
import * as gulp from 'gulp'; |
---|
import {gulpPrerender} from '@angular/universal'; |
import {App} from './app'; |
gulp.task("prerender", () => { |
return gulp.src(['index.html']) |
.pipe(gulpPrerender({ |
directives: [App] |
})); |
}); |
And then simply upload this to an Amazon S3 bucket using the Amazon CLI:
aws s3 cp prerendered/*.html s3://your-amazon-s3-bucket/your-site --recursive |
---|
--expires 2100-01-01T00:00:00Z --acl public-read |
--cache-control max-age=2592000,public --content-encoding gzip |
There is an initial lag between the moment that the plain HTML is rendered and presented to the user and the moment that Angular kicks in on the client side and takes over as a SPA.
In that timeframe, the user might click on something or even start typing in a search box. What Angular Universal allows via its preboot functionality is to record the events that the user is triggering, and play them back once Angular kicks in.
This way Angular will be able to respond to those events, like for example by showing the search results on a Typeahead list.
But what does this look like on the server in terms of code?
The way that the content is rendered on the server is by using the express Angular Universal rendering engine expressEngine :
import {createEngine} from 'angular2-express-engine'; |
---|
import { UniversalModule } from 'angular2-universal'; |
app.engine('.html', createEngine({})); |
app.set('views', __dirname); |
app.set('view engine', 'html'); |
@NgModule({ |
bootstrap: [ App ], |
declarations: [ App, ... ], |
imports: [ |
UniversalModule, |
... |
], |
providers: [...], |
}) |
export class MainModule { |
} |
function ngApp(req, res) { |
let baseUrl = '/'; |
let url = req.originalUrl || '/'; |
let config = { |
req, |
res, |
requestUrl: url, |
baseUrl, |
ngModule: MainModule, |
preboot: false, |
originUrl: ROOT_URL |
}; |
console.log("rendering ..."); |
res.render('index', config, (err, html) => { |
res.status(200).send(html); |
}); |
} |
There is also a Hapi engine available if you prefer to use Hapi instead of Express. There are also server rendering engines coming up for all sorts of platforms: C#, Java, PHP.
The best place to start is the official starter universal-starter, with the starter we get a running application which includes an express server with server side rendering working out of the box.
What is innovative about Angular Universal is its simplicity of use. One of the main design features of Angular Universal is the way that it uses dependency injection.
Most of the time we want our application to do the exact same thing on the server as on the client, by not depending directly on browser APIs.
The reality of server-side rendering development is that that is not always possible, and there are certain things that we want to do differently on the client and on the server.
Take for example the rendering of a chart: you probably want to call a third party library that uses SVG. We cannot call it on the server, so how do we render it?
Another example, how can we render authenticated pages? Because the content depends on who is currently logged in.
To handle authentication, this is one way to do it:
How to have the same page output while navigating to that page on the client via a router transition vs on the server via a browser refresh?
The first step is to define an interface that your service will provide, which is independent of the runtime:
export interface AuthenticationService { |
---|
signUp(activationLink:boolean); |
login(); |
logout(); |
isLoggedIn(); |
isLoggedInWithGitHub(); |
} |
Then we provide two implementations for this interface, one for the client and the other for the server. For example on the server there is no occasion for the login method being called, so we throw an error:
export class ServerAuthenticationService implements AuthenticationService { |
---|
constructor(private authenticationId, private isUserLoggedIn:boolean) { |
} |
signUp() { |
throw new Error("Signup event cannot be called while doing server side rendering"); |
} |
login() { |
throw new Error("Login event cannot be called while doing server side rendering"); |
} |
logout() { |
throw new Error("Logout event cannot be called while doing server side rendering"); |
} |
isLoggedIn() { |
return this.isUserLoggedIn; |
} |
isLoggedInWithGitHub() { |
... |
} |
} |
While on the client, we are going to trigger the Auth0 lock screen (a third-party browser only library) to provide a sign-in form:
@Injectable() |
---|
export class ClientAuthenticationService implements AuthenticationService { |
signUp(activationLink = false) { |
this.messagesService.clearAll(); |
const lock = new Auth0Lock(AUTH0_API_KEY, AUTH0_SUB_DOMAIN); |
lock.showSignup(this.loginConfig(activationLink), this.loginCallback()); |
} |
.... |
} |
We then inject different implementations of the interface on the server and on the client, for the same injection token:
// define an injection name |
---|
export const authenticationService = new OpaqueToken("authenticationService"); |
// inject on the server |
provide(authenticationService, { |
useFactory: () => new ServerAuthenticationService(req.authenticationId,isLoggedIn) |
}) |
// inject on the client |
provide(authenticationService, { |
useFactory: (messagesService: MessagesService, ngZone: NgZone, cookieService: CookieService, userService: UserService, router:Router, gitHubService : GitHubService) => { |
return new ClientAuthenticationService(messagesService, ngZone, cookieService, userService, router, gitHubService); |
}, |
deps: [MessagesService, NgZone, CookieService, UserService, Router, GitHubService] |
})]) |
And this is how we can do something different on the client and on the server in Angular Universal, by leveraging the Angular dependency injection container.
In fact, Angular Universal is built also using this notion: for example the way that HTML is rendered is by instead of injecting a DOM renderer while bootstrapping the framework, it injects a server renderer which generates HTML using the parse5 HTML serialization library.