Angular 4 the new gig

Onefootball office

Onefootball’s mission is to tell the world’s football stories and we do that through apps on iOS, Android and the web.

At the beginning of this year we decided to revamp the whole website with a new content strategy alongside a new editorial plan and a new UI design. We also felt the need to improve the architecture running under the hood.

The previous setup had been running for almost three years, an AngularJS single page application we started coding in 2014. It served to the users an amalgamation of our content.

The main challenge for that project was to use our API in the best way possible to display content and create meaningful navigation paths in several languages. We serve 7 languages and 11 markets.

The other challenge for us was to make our website indexable by web crawlers, since you probably want your users to find you easily.

With AngularJS, we had a separate service in charge of pre-compiling the pages of the single-page app, so whenever a crawler visited us we could give them a more digestible plain no-js HTML webpage. This isn’t the best setup and probably you would rely on it only if really have to.

When the new Angular came out with the promise of server side rendering, we thought we could kill the two birds with one stone.

I want to talk to you about three aspects of moving from AngularJS to the new Angular: the project setup, the isomorphic rendering and localised routing.

Project Setup: Welcome Typescript / Webpack / NPM Script

In the past we mostly used Grunt to produce the distribution package and for some side jobs; SVG optimisation and updating translation to name just two.

Moving to the new Angular, the project’s setup might seem quite different, but is in general more structured. Now everything can be done with Webpack and NPM scripts, everything can be finally imported à la ES6 modules and if you feel brave enough can always write your own loader.

And NPM scripts reinforce the concept of single responsibility at tooling level. We usually maintain a tool directory with scripts of various types, some of them are for NodeJS, while others are external command line utilities.

Despite using different tooling, it is easy to feel comfortable with Angular Typescript projects.

Universal Code and isomorphic rendering

You’ll find it under the name of server side rendering (SSR), isomorphic rendering or universal code. In short, it is the ability to run JavaScript code server- and client-side producing the same result. The browser at the first request receives a full HTML page, then client-side JavaScript takes over and from that moment everything is managed client-side.

To kickoff our website, we used the Angular Universal project (https://github.com/angular/universal), a fully working isomorphic application with some specific modules to make SSR possible. There is great people behind it and it’s very active. Recently it has been merged into angular-core.

Being able to run the same code server and client-side makes it possible to pre-elaborate almost everything so the client has to do less in order to bootstrap the application. One example is having a server-to-client state, store all the ajax calls responses in a cache and transfer it from the server to the client. That way the client doesn’t have to retrieve that data the server already collected. The result is pretty cool, no additional ajax calls on the first page load.

However a mindset shift is needed to make sure your code works server-side. No more direct access to the DOM, API and other libraries; just rely blindly on the Angular framework and make sure the resources you need exist in the current platform; for instance there is no window global object server-side.

Writing such kind of application forces you avoid all those tricks, we web developers do all the time.

Routes

Localised routing is always an interesting topic. In the old AngularJS was pretty straightforward how to create multilingual apps, you would simply load the translation files needed, then bootstrap your app and configure the modules’ routing with different strings:

$stateProvider
.state('competition', {
url: '/:lang/'+ getLocalizedString('URL.COMPETITION'),
...
})

That would make possible only the localized addresses to be reachable, disabling all the rest.

With Angular 4 is quite different. There’s no official way to do localized routing, and trying to apply the same strategy we used with AngularJS, well… doesn’t work. The relationship between the routing definitions and the module creations is made before any information about the wanted language exists.

However, nobody prevents you from hardcoding the routes in all the language for each module. But this will take you to some strange behavior, like half translated URL. For example this could be a pretty valid URL /first/secondo/third, a mix of English and Italian.

Luckily, there some good projects like localize-router or ngx-i18n-router which will solve all the issues. In addition to that you have use the same routing rules to your server-side Angular app.

If the whole URL, from root to leaf, is reserved for you angular app then is possible to tell the the Express app to listen to everything:

import * as express from 'express';
import { ngExpressEngine } from '@nguniversal/express-engine';

// Set the engine
app.engine('html', ngExpressEngine({
bootstrap: ServerAppModule // Give it a module to bootstrap
}));
app.set('view engine', 'html');

app.get('/**/*', (req: Request, res: Response) => { // render every url with the ServerAppModule
res.render('index', {
req,
res
});
});

We needed instead to support legacy url from previous implementation. So we have a setup where Angular urls cohabit with other legacy urls under the same roof. That means our Express app respond with the Angular engine only when needed.

We have a configuration with the predefined markets and their URL prefix, so it makes sense to reuse them to build the Express routing and keep the old routes as well.

// Set the engine
app.engine('html', ngExpressEngine({
bootstrap: ServerAppModule // Give it a module to bootstrap
}));
app.set('view engine', 'html');

// Use the routing config
const ROUTES = JSON.parse(fs.readFileSync(`markets-routing.json`, 'utf8')); // assuming this ['en', 'de', 'fr', 'es', 'it'];
ROUTES.forEach((route) => {
app.get(`/${route}`, angularHandler);
app.get(`/${route}/*`, angularHandler);
});

app.get(/legacy-service, legacyHandler); app.get(/not-localized-legacy, legacyHandler);

It works though

After the initially steep learning curve where nothing made much sense, we can now say that it works.

It works pretty well actually. The sense of order, control and readability are now even easier to achieve; feels natural now to apply the component-first approach to every feature. So thumb-up for the new Angular 👍

Like what you read? Give Andrea Dessì a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.