Learn

Filter by Category
Filter by Category

Server Side Rendering Using Angular in Action

CUTTING WORK IN HALF

In over a decade, SPAs (Single Page Applications) have overtaken the market and vast majority of produced web applications would follow this approach. This is backed up by the amount of frameworks born, dedicated to construction of such applications: Angular(Js), React, Vue.js, Knockout, Ember.js, Meteor.js to name a few. SPAs differ from the classic way of constructing web applications in many ways, but most notable being better user experience thanks to incremental updates to the view in the browser utilizing JavaScript. This is also way more performing than full page refreshes and round trips to server to get the updated view in the classic HTML applications.

On the initial page request in SPAs, the browser would get from the server only the bare HTML skeleton of the application, and all subsequent interaction with such application would be handled by JavaScript in the browser. However, the smooth user experience comes at a certain set of costs. First and most significant is crippled SEO availability and scoring for such pages, as most of the bots indexing the pages lack the ability to execute the JavaScript, so they see only the HTML skeleton of the page being present, which was returned as a response from the server. Nowadays, the crawler from Google can index SPAs pretty well, but others, like Bing, still depend on the HTML content returned from the server.

Another drawback is the growing size of JavaScript that is needed to deliver the application, in both hand-coded and 3rd party dependencies. In some cases, it goes up to megabytes of extra data that needs to be downloaded by the user to use the web application. Compared to some other static content that is heavy by definition (like hi-res images) it does not seem too much, but is far from negligible. On top of the time and bandwidth needed to download the asset, computing in the browser is required, to parse the scripts, as well as do all the necessary bootstrapping.Multiple strategies and approaches were invented to mitigate those issues, one of them being server side rendering using Universal JavaScript.

SERVER SIDE RENDERING

Server side rendering can be accomplished in many ways. It can vary from using static HTML views, through a templating engine like Pug and ending with something more sophisticated like Freemarker used in Java world, and built on top of JSPs.

The main problem with those approaches is that having dynamic views comes at the cost of duplication of work. A developer would need to implement data-to-view mappings in Freemarker and then do the same in the front-end. Freemarker is an example where even templates reuse is limited since it depends on the Java execution environment. So, in an extreme case, one would need to even keep two sets of templates rendering the same view in sync, one set used in the server and another in the browser.

Things may go wrong easily in such scenarios as mismatches happen between how the view is being rendered in the browser and on the server side. However, the most notable disadvantage of such an approach is the development and maintenance cost which can double for the application view layer.

Figure 1:  Server side rendering (SSR) vs Client side rendering (CSR) flow.

Figure 1: Server side rendering (SSR) vs Client side rendering (CSR) flow.

UNIVERSAL JAVASCRIPT TO THE RESCUE

A concept was born addressing this issue a few years back. First popularized by Spike Brehm at Airbnb and at the time referred to as ‘isomorphic’ JavaScript. In his post, he described the need to design web applications so that they provide the SEO capabilities of server rendered pages and the performance of incrementally updated views in the browser. All of that - with little-to-no development overhead.

This is basically what universal JavaScript is – a piece of JavaScript code agnostic of whether it is run inside the browser or on the server. An in-depth discussion on the topic is available here.

There does exist a limitation in this approach; the universal JavaScript snippet cannot use APIs that are available only in one of the platforms (browser or server). Alternatively, there needs to be extra plumbing implemented to prevent the code from failing, when missing APIs are accessed.
All those concerns are addressed by Angular Universal, which is the solution we will be presenting.

 

Figure 2: Universal JavaSript concept depicted
Figure 2: Universal JavaSript concept depicted

ANGULAR UNIVERSAL

The Angular Universal package started off as a community initiative and then got included into the main repository of all modules developed and maintained by Google. It has been battle-tested in many applications in production.

Apart from the ability to render the views dynamically on the server side, it gives the possibility to ‘pre-render’ the views statically as part of the build process. This approach can be sufficient in cases where we don’t care about up-to-date data being rendered on the server or just want to present the application shell to the user initially when the web application gets requested. After that, JavaScript does all the necessary updates to the views.

We will not be going into details of how to setup Angular Universal in your app. But the official Angular docs include a great guide, available here, and it is regularly updated by the Angular development team.

DEFINING THE APPLICATION SHELL

Once the universal app is setup, the next step is to decide which parts of your views should be rendered on the server. By default, the Angular Universal engine will render the entire page for you, but that might not be optimal or desired, depending on your use-case.

Transferring huge amounts of HTML is not performant as it has a verbose syntax when comparing it for instance to JSON. Thus, it can be a better option from a performance perspective to skip some HTML heavy parts of your application when rendering the view on the server. This trimmed down version of your application is what is called the application shell.

This application shell may contain only a basic skeleton of the page, like the menu and some form of loading indicator, but may go as far as a full-blown view, depending on your use-case.

RELATIVE URLS WHEN RETRIEVING DATA

A common setup for data retrieval is via Ajax requests using relative URLs to point to the data service endpoint. Unfortunately, this strategy turns out not to work well when the code is executed on the server, as there is no domain context for it. To fire off a valid request on the server one needs to specify the full address of the target endpoint.

Assuming that your code is organized in a way that you have dedicated services retrieving your data, or even maybe a base class for them, the solution becomes rather simple and localized. Namely, providing a URL root for your services when the code is executed on the server, and keeping an empty one when in the browser. This allows us to use relative URLs in the browser whilst providing the full address for the services on the server.

To achieve that one can utilize APP_BASE_HREF. This approach is described here. An alternative would be to define a custom injection token and utilize that. That token can be defined in the server.ts file, to utilize Angular’s DI mechanism and potentially read the URL root from a configuration file located on the box or via an environment variable in the system in which the server side app is deployed. This allows us to have different roots in different environments.

app.engine('html', ngExpressEngine({
  bootstrap: AppServerModuleNgFactory,
  providers: [
    provideModuleMap(LAZY_MODULE_MAP),
    { provide: ROOT_TOKEN, useValue: process.env.ROOT_TOKEN },
  ]
}));

Snippet 1: Injecting URL root token into server side executed Angular app

In the service itself, it is enough to mark that token as @Optional and provide the default empty value, which will be leveraged by the browser.

AUTH AND COOKIES

Most often, we have two forms of access in web applications: public and authenticated. The case when a user of our application is authenticated is usually handled leveraging security token in the form of a cookie or a header. The token’s presence in the request allows the back-end to perform authentication and session association where applicable. Next, it is also pivotal to perform a fine-grained authorization: e.g., the server can determine whether access to data should be granted or which specific portions of the data should be included in the response.

This case should be handled seamlessly by the server side application, as it is a common and cross cutting concern. At the time of this write up, there was no built-in solution into Angular Universal (v6.x) package to handle this scenario. In order to achieve that, i.e. to have the security token propagated from the request fired off by the browser to the one done internally by our app on the server, additional plastering needs to be added.

To achieve this we will be leveraging HTTP interceptor to add the required cookie headers when on the server.

We need to start with server.ts file, where we will inject the REQUEST and RESPONSE objects in the form of providers. Please refer the snippet below for the details.

app.get('*', (req, res) => {
  res.render(
    'index', {
      req: req,
      res: res,
      providers: [{
          provide: 'REQUEST', useValue: req
        }, {
          provide: 'RESPONSE', useValue: res
        }]
    }, (err, html) => {
      if (!!err) { throw err; }
      res.send(html);
    }
  );
});

Snippet 2: Injecting request and response objects from the level of Express server into our Angular app

As you can see, we are leveraging the request and response objects provided by the Express server. We will be passing them into our Angular app to read and pass over the cookies using an HTTP interceptor. Now, let’s have a look at how the implementation of the interceptor might look like.

@Injectable()
export class HttpHeadersInterceptor implements HttpInterceptor {
    constructor(private injector: Injector,
                 @Inject(PLATFORM_ID) private platformId: Object) { }

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<httpevent<any>> {
          // if we are server side, then import cookie header from express
         if (isPlatformServer(this.platformId)) {
              const req: any = this.injector.get('REQUEST');
              const rawCookies = !!req.headers['cookie'] ? req.headers['cookie'] : '';
              request = request.clone({ setHeaders: { cookie: rawCookies } });
          }
          // pass through any requests not handled above
         return next.handle(request);
     }
}

Snippet 3: HTTP requests interceptor passing over cookie headers from the original request made from the browser

As we can see, we are using the Injector service here directly in order to retrieve the REQUEST object which was injected from the level of the Express server. We are using the service directly, instead of DI, guarded by a platform check, since we do not want to provide a dummy request object to be available when executed in the browser.

With these two simple tweaks, we can now see our requests made on the server side behave as if there were fired off by the browser with cookies being propagated correctly.

STATE TRANSFER TO THE BROWSER

Finally, you have your app set up, the application shell beautifully defined, but when accessing your application in the browser you can see the XHR requests being fired AGAIN, data is being re-downloaded, what is happening?!
The issue here is that the browser has no idea that the data was already retrieved on the server and it proceeds with the regular bootstrapping of your front-end app. It will try to fetch the data again and render the view.

TransferState class comes handy in this case. It allows both applications (server-side and in browser) to communicate and transfer the data fetched when rendering the view on the server.

The state is being transferred in the form of a script tag at the bottom of the page, which is automatically added by Angular when the state transfer is wired up. This is done by using BrowserStateTransferModule and ServerStateTransferModule available from the platform-browser and platform-server packages respectively.
Once the mentioned modules are registered, you can leverage transferred state inside your application.

import { TransferState, makeStateKey } from '@angular/platform-browser';

const STATE_KEY_DATA = makeStateKey('someData');

@Component({
    selector: 'app-about',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
    public data: any = [];

    constructor(
        private http: HttpClient,
        private state: TransferState
    ) { }

    ngOnInit() {
        this.data = this.state.get(STATE_KEY_DATA, <any>[]);

        if (!this.data.length) {
            this.http.get('https://mydomain.com/data')
                .subscribe((data) => {
                    this.state.set(STATE_KEY_DATA, data);
                    this.data = data;
                }, (err) => {
                    this.state.remove(STATE_KEY_DATA);
                });
        }
    }
}

Snippet 4: An example smart component utilizing the state transfer mechanism.

The thing to remember is that this piece of code is executed twice – first when the request reaches the server and again in the browser. On the server side, the data will be absent from the TransferState service. Thus, the XHR request will be done to fetch the data. However, once invoked in the browser, assuming all the setup was done correctly and the data transferred in a form of a script tag, no XHR request will be made since the data will be successfully retrieved from the said service.

There is one more trick required to get the full solution to work. Since the data is embedded within the HTML of the page, inside a script tag, bootstrapping of our application needs to be deferred until after this block is processed. To do that, in our main.browser.ts file instead of bootstrapping the app inline, we subscribe an event handler to the DOMContentLoaded event.

document.addEventListener('DOMContentLoaded', () => {
    platformBrowserDynamic()
        .bootstrapModule(AppModule)
        .catch(err => console.error(err));
});
Snippet 5: Amended Angular bootstrapping code, to cater for transferred state being loaded from a script tag.

That’s really it. Now, no redundant calls to fetch the data will be made when the application bootstraps.

PREBOOT

Preboot is a library targeted at universal applications and can be used with or without Angular. Please refer to the docs for more details.

When our server returns HTML, based on what we decide to include in our application shell, the user may see some elements he deems (correctly) to be interactive. However, unless we employ some sort of loading indicator to prevent the interaction, when clicking that very element nothing would happen until the JavaScript is downloaded and bootstrapped. Only once that is done will the elements start correctly responding to user’s interaction.

This scenario and many more are covered by this utility library. It is all about managing the user experience from the time when a server view is visible until the client view takes over control of the page.

CONCLUSIONS

Based on the techniques and technologies outlined in this article, we should be able to deliver a great user experience, having a fine-grained control over which parts of state are transferred to front-end and what would be our application shell.

Our server side application will be using the security tokens passed in the form of a cookie from the page load request so that our data can be loaded accordingly to the authenticated user. Apart from saving the roundtrips from the client to fetch the data, said data could be retrieved even within the same box, depending on how back-end components are deployed internally.

On top of that, the user will get busy straight away, browsing what was rendered on the server side, while the preboot module will record all the interactions and replay them as per the configuration supplied when the Angular app gets bootstrapped.

There are many factors to take into account when deciding whether to include the Angular Universal setup within your application. It comes at a certain additional complexity cost. This cost is mostly paid when setting-up the project, but in some cases, it can also add to the implementation cost of specific features. For example, to transfer some state from the back-end or define the application shell at a finer level.

There are obvious benefits from having it implemented. As outlined in the previous section, the user experience is much better when having the shell rendered on the server. The user is presented with something useful straight away when they visit our page. Moreover, the presence of our app will be boosted in all the search engines that still cannot properly index JavaScript based SPAs.

5 Marketing Tips for Independent Performers
Partner Spotlight: AVSecure

About Author

Artur Banas
Artur Banas

Currently holding the position of Front-End Architect, Artur Banas has been designing and implementing user interface solutions for nearly 10 years. He constantly researches new frameworks and tools that can give the company a competitive edge. Believes that good code quality, applying best practices and design patterns are the foundation of any piece of software. Well versed with Java, SQL, NodeJS, JavaScript and a wide range of front-end technologies. Particularly interested in the entire EcmaScript ecosystem.

Related Posts
GRPC VS REST
GRPC VS REST
Ecommerce Buzz: Try Subscription Retail
Ecommerce Buzz: Try Subscription Retail
PrestaShop 1.7.3 is out and we’re compatible!
PrestaShop 1.7.3 is out and we’re compatible!

Subscribe To Blog

Subscribe to Email Updates