Developing an Angular Universal (aka isomorphic) application means to write code that can be rendered everywhere, regardless of the client: a web browser, a mobile app, a server-side Node.JS or ASP.NET service that does the prerendering, and so on. This has big advantages in terms of performances, scalability and SEO, but it also comes with some downsides that we need to understand.
The most important of them - and the one causing more issues - is probably the fact that some "global" Javascript objects known as browser types, such as window , document , navigator and so on are not so "global" anymore: when writing JS code using an universal/isomorphic approach we can't take them for granted because they simply do not exist on the server, which is one of the contexts we need to take care of.
To better explain this, it means that we can't do this:
1 |
window.localStorage.setItem("someKey", "someValue"); |
Because it will definitely generate an error during the server-side prerendering phase.
If we need to use these objects, we need to wrap them into a conditional code block that will ensure they'll be executed only in the appropriate context: in other words, we need to execute these kind of instructions situationally within our JS code, thus adopting an isomorphic approach.
Luckily enough, Angular provides us a way to easily do so by making available two very useful objects:
- The PLATFORM_ID token (part of the @angular/core package), which can identify the executing platform's type.
- The isPlatformBrowser and isPlatformServer methods (from @angular/common), which can be used - in conjunction with the aforementioned PLATFORM_ID - to check whether the current platform is browser or server.
Thanks to these two objects we can easily pull off something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import { PLATFORM_ID } from '@angular/core'; import { isPlatformBrowser, isPlatformServer } from '@angular/common'; constructor(@Inject(PLATFORM_ID) private platformId: Object) { // constructor code } ngOnInit() { if (isPlatformBrowser(this.platformId)) { // Client only code. } if (isPlatformServer(this.platformId)) { // Server only code. } } |
These conditional blocks can be put anywhere within our Angular component's code.
IMPORTANT: Writing isomorphic code and writing decent Angular code are two different things: no matter how conditional and situational our code might be, if we use a global object within an Angular component it means that we're dealing with this stuff in a bad way. To better understand this concept, I strongly suggest to read this post.
For more info about Angular Universal and isomorphic JavaScript check out the following resources:
- https://universal.angular.io
- http://isomorphic.net/javascript
- https://github.com/angular/universal#angular-universal
Hi. I’m trying to figure out the Universal aspect of Angular for a component I’m working on.
I’m finding that there are some things that the Angular system just can’t deal with – without reverting to native Web API’s. For example the content we recieve is markdown, which is then injected into the innerHtml of an Angular component after being processed by marked.js. Angular has no way of accessing the generated HTML, apart from using ElementRef.nativeElement of the parent component, and then doing a querySelectorAll for the elements we need to retrieve. But, using nativeElement is not recomened by Angular. Can we use Renderer2 to solve this – not from my experience, as it also needs ElementRef.nativeElement, which will be null on the server.
What are your thoughts on this?