While Angular is a powerful way to build web apps, developers have long known its limitations with SEO and accessibility. Sure, Google's crawler can execute JavaScript, but it's not the only crawler in the game. For example, after posting a link to Slack, its crawler will pull down a preview, but it doesn't execute JavaScript, so the raw Angular HTML templates show up in the preview. To eliminate the trouble this causes, Jeff Whelpley and Patrick Stapleton have worked on Angular Universal that allows the rendering to happen on the server.
The universal JavaScript technique (sometimes incorrectly referred to as "isomorphic") is not unique to Angular. Angular Universal works by doing an initial render of the app on the server, sending that to the browser for the user to see right away, and follows up with the client JavaScript. This is a slightly different order than typical Angular apps where the client JavaScript is sent and then the initial UI is rendered on the client.
Welpley and Stapleton have been working on Angular Unversal for over a year and have discovered six patterns that have popped-up over and over. They focused on three of those in their 2016 ng-conf session:
- Gap Events
- Async
- Dependencies
Gap Events are a side effect of the fact that rendering occurs on the server before the JavaScript client code is sent down to the browser. Depending on how fast that JavaScript code is sent down and processed, the user may interact with the UI before the code is ready to handle it. This gap can result in the loss of the user's interaction.
The solution to this problem is to record the user's events and replay them once the client JS has loaded. Here's a sample of what that code looks like:
var myEvents = [];
var myInputValue;
// record all keyup events on client view myInput
function recordEvents() {
var $myInput = document.querySelector('.myInput')
$myInput.addEventListener('keyup', function (event) {
myEvents.push(event);
myInputValue = event.target.value;
});
}
// play back all keyup events on server view myInput
function replayEvents() {
var $myInput = document.querySelector('.myInput');
myEvents.forEach(function (event) {
$myInput.dispatchEvent(event);
});
$myInput.value = myInputValue;
$myInput.focus();
}
// starting recording as soon as the window has loaded
window.addEventListener('load', recordEvents);
Rather making developers do this manually, Angular Universal uses a process called preboot to handle this work and it's enabled with a flag:
preboot: true
JavaScript is naturally asynchronous, but this can cause problems when trying to render Angular code on the server. The solution is often to use chaining or callbacks, but requiring developers to rewrite their code to solve the problem is not an option. "We can't do that. We have to figure out a way to handle all these disparate async events and coordinate when to send back the response," said Whelpley.
Angular Universal has another flag that solves the problem in one line:
async: true
Turning this flag on will use the new Zones feature of Angular to "track all async calls and know when they're resolved."
The third major problem with rendering Angular code on the server is the use of platform specific dependencies. For example, localStorage
is a browser feature and doesn't exist on the server. Whelpley and Stapleton point to the use of Dependency Injection (DI) as the solution. Rather than using the platform features specifically, they recommend using DI to swap out the implementation depending on the context of code.
To developers used to testing their, this is no surprise. Whelpley pointed to testing and the other platforms that Angular runs on as proof that the technique will become prevalent in Angular 2 code. "We're pushing out platform specific dependencies to the edge. This pattern is the most powerful we've talked about today," said Whelpley.
The full video of the their ng-conf session is available now.