Angular architecture patterns – Additional application features

angular2 javascript patterns web development / Read time 21.5min

Welcome back to series of blog posts about architectural patterns for frontent web applications. The code examples are written in Angular 4 but the logic is applicable in plain javascript or any modern Javascript framework.

If you are new with this topic, check previously published posts:

Demo application with full source code is published on Github.

In the previous chapter we described all main project features and gave the examples of where to put them in the code. They represent the backbone of our application. All the additional logic that we’ll implement will be based on the previously described architecture. However if we want to make the project complete we need to add a few more things. This is the list of features we will be talking about in this chapter:

At the end the application architecture will have the following structure:

Figure 4: Detailed project architecture

Configuration

Configuration is something that almost every application needs to have. We need a place to store widely used constants (strings, numbers, object literals…) and be able to have different configuration settings for development, production and maybe testing environment. Things that go in configuration are api base urls, paths to various folders, global notification settings etc.  

In order to setup the configuration in the project we’ll have three json files and one Angular service that will read these files and load the configuration settings based on the current environment variable. The first json file is called env.json and it contains only one property – env, that holds currently set environment. This variable will be set to one of the predefined values (development, production) because we’ll be using it throughout the project and in the CLI while building the app. So, we’ll make a script which will act as a hook and, using Node.js built in functions, set the environment variable to the desired value.

{
  "env": "development"
}

myApp/config/env.json

function setEnvironment(configPath, environment) {
  fs.writeJson(configPath, {env: environment},
    function (res) {
      console.log('Environment variable set to ' + environment)
    }
  );
}

// Set environment variable to "production"
setEnvironment('./src/config/env.json', 'production');

myApp/hooks/pre-build.js

setEnvironment function reads the env.json file, modifies the env variable and saves it. In our package.json file we’ll define custom script which will call setEnvironment function before starting and building the application.

"scripts": {
  "start": "npm run pre-start & ng serve",
  "pre-start": "node hooks/pre-start.js",
  "pre-build": "node hooks/pre-build.js",
  "my-custom-build": "npm run pre-build & ng build --prod --aot"
}

package.json

So env variable (in our example) can be set to development or production only (generally this could be any arbitrary value but we’ll get stick to these ones because they are the most descriptive). Based on this value we’ll load the corresponding configuration file – development.json or production.json both of which hold the configuration content.

{
  "api": {
    "baseUrl": "/api"
  },
  "debugging": true
}

myApp/config/development.json

In order to load the values from the json file into our application we need a service which will read the env.json file to see which environment is used and after that load the configuration file and transform it into javascript object literal to be readable in our code.

@Injectable()
export class ConfigService {

  private config: Object
  private env:    Object

  constructor(private http: Http) {}

  /**
   * Loads the environment config file first. Reads the environment
   * variable from the file and based on that loads the appropriate
   * configuration file - development or production
   */
  load() {
    return new Promise((resolve, reject) => {
      this.http.get('/config/env.json')
      .map(res => res.json())
      .subscribe((env_data) => {
        this.env = env_data;

        this.http.get('/config/' + env_data.env + '.json')
          .map(res => res.json())
          .catch((error: any) => {
            return Observable.throw(error.json().error || 'Server error');
          })
          .subscribe((data) => {
            this.config = data;
            resolve(true);
          });
      });
    });
  }

  /**
   * Returns environment variable based on given key
   *
   * @param key
   */
  getEnv(key: any) {
    return this.env[key];
  }

  /**
   * Returns configuration value based on given key
   *
   * @param key
   */
  get(key: any) {
    return this.config[key];
  }
}

myApp/src/app/app-config.service.ts

Now let see how to use the service in practice:

let baseUrl: string = this.configService.get(api).baseUrl;

We can nest our properties as much as we want and in very elegant way pull the configuration in any part of our app.

There’s one more thing we need to set. We want to load the configuration file before any component is initialized, including our main app.component.ts file. This is important because once the application is bootstrapped the configuration needs to be available and ready to use.

In our app.module.ts file we’ll define a special type of provider – APP_INITIALIZER to execute config.load() method before application startup, that is the load() method we defined in myApp/src/app/app-config.service.ts.

import { APP_INITIALIZER } from '@angular/core';

export function configServiceFactory (config: ConfigService) {
  return () => config.load()
}
...
@NgModule({
  ...
  providers: [
    ...
    ConfigService,
    {
      provide: APP_INITIALIZER,
      useFactory: configServiceFactory,
      deps: [ConfigService], 
      multi: true
    }
  ],
  ...
});

myApp/src/app/app-module.ts

configServiceFactory function is used as an exported function because calling functions or calling new is not supported in metadata (providers definition) when using AoT. The reason for this limitation is that the AoT compiler needs to generate the code that calls the factory and there is no way to import a lambda from a module, you can only import an exported symbol. The ‘multi: true‘ is being used because an application can have more APP_INITIALIZER providers.

And that’s it, the configuration setup is done and ready to use.

Internationalization

Internationalization is important not only for multi language purposes but also for defining label values in one place as well. It means that we can define translated values in one place and use them in multiplate places inside the project. It makes the maintenance and making the changes throughout the templates more easier.

For this purpose we used ng2-translate npm module. The basic installation is very simple. We just need to import TranslateModule in our main app module.

import { TranslateModule } from 'ng2-translate';

@NgModule({
  imports: [
    TranslateModule.forRoot()
  ],
});

For more options about installation you can refer to module documentation. By default the module looks for json files in myApp/i18n folder. We need to have a json file for every language we want to support e.g. en.json, hr.json etc. Once we create these files we can use the TranslatePipe and the TranslateService to translate values in the app. The TranslatePipe is an Angular pipe that can be used to translate static values, like labels in the templates.

<label>{{ 'HeaderTitle' | translate }}</label>

The TranslateService can be used both to translate JavaScript values, which includes observables as well, and to change the current language used in the app.

translateService.get('HeaderTitle').subscribe(
  value => {
    let alertTitle = value;
  }
)

let title = translateService.instant('HeaderTitle');

Before we start using the translated values we need to set a default language. This is the task for application root component since it’s used for bootstrapping the app. Actually the root component will delegate this task to it’s sandbox because the sandbox knows of the configuration and other smart application core stuff. Available languages are another thing we can store in the configuration. The code below shows how to setup a default language and store available languages in the store.

private setupLanguage(): void {
  // Load localization object from the confguration
  let localization: any = this.configService.get('localization');

  // Save language codes as an array
  let languages: Array<string> = localization.languages.map(lang => lang.code);

  // Get browser’s default language
  let browserLang: string = this.translate.getBrowserLang();

  // Tell the translate service for the available languages and set a
  // default one
  this.translate.addLangs(languages);
  this.translate.setDefaultLang(localization.defaultLanguage);

  // The default language will be a default language from the configuration
  // or selected browser language if it matches one of our predefined values
  let browserMatch = browserLang.match(/en|hr/);

  let selectedLang = browserMatch ? browserLang : localization.defaultLanguage;
  let selectedCulture = localization.languages.filter(lang => {
    return lang.code === selectedLang;
  })[0].culture;

  // Tell the translate service to use selected language
  this.translate.use(selectedLang);

  // Save selected language and culture in the store
  this.appState$.dispatch(new settingsActions.SetLanguageAction(selectedLang));
  this.appState$.dispatch(new settingsActions.SetCultureAction(selectedCulture));
  }

In this example we are using the browser’s default language as application’s default language. We can also hardcode it in the configuration file if we have such requirements.

Our final consideration is that we might need to separate json files into smaller pieces because, as our application continues to grow, our translation files will be getting bigger. Because of this we’ll separate them by features, the same way we separated the presentational modules. In the same time we can have the general json files for things we use commonly throughout the whole application, e.g. server response messages, notification titles…

Since TranslateModule uses only one file per language (e.g. en.js) we need to merge them together. To achieve this we need to create another Node.js hook and attach it before start and before build process.

function mergeAndSaveJsonFiles(src, dest) {
 jsonConcat({ src: src, dest: dest },
   (res) => console.log('Localization files successfully merged!');
 );
}
// Merge all localization files into one
mergeAndSaveJsonFiles(localizationSourceFilesEN, "./i18n/en.json");
mergeAndSaveJsonFiles(localizationSourceFilesHR, "./i18n/hr.json");

 

Utility

Utility module partly solves DRY (don’t repeat yourself) problem. It’s used for keeping all commonly used logic and helper functions. We can divide it in two sections, functions which use other application’s dependencies (e.g. a service which requires a translation service as a dependency) and standalone functions (e.g. for capitalizing the words).

The first ones go in the service which will include other modules via dependency injection. For example, we can define a generic function for displaying different types of toast notifications (success, error, info…) with translated messages. For this to work we need an instance of notification and translation module.

/**
 * Translates given message code and title code and 
 * displays corresponding notification
 *
 * @param msgCode
 * @param type
 * @param titleCode
 */
public displayNotification(msgCode: string, type: string = 'info', titleCode?: string) {

  let message: string = this.translateService.instant(msgCode);
  let notificationOpts: any = this.configService.get('notifications').options;
  let title: string = titleCode ? this.translateService.instant(titleCode) : null;

  switch (type) {
    case "error":
      title = this.translateService.instant('ErrorTitle');
      break;
    case "success":
      title = this.translateService.instant('SuccessTitle');
      Break;
    default:
      title = this.translateService.instant('InfoNotificationTitle');
      break;
  }
  this.notificationService[type](title, message, notificationOpts);
}

The other type of utility functions, which don’t depend on other modules, are functions for general low level JavaScript tasks such as capitalizing the first letter in a sentence, flattening object literals, date to string transformations (and reverse) etc. They are standalone and use JavaScript browser API.

/**
 * Capitalizes the first character in given string
 * @param s
 */
export function capitalize(s: string) {
  if (!s || typeof s !== 'string') return s;
  return s && s[0].toUpperCase() + s.slice(1);
}

Shared components

Shared components are presentational elements stored in application core layer. Does that sound weird? Well, the application core can hold the presentational elements repeatedly used in the project and provide them to presentational modules to be included as snippets in the template. Each of these components belongs to a module that declares and exports it. This module will include other dependency modules required by the components to work properly. Let’s say we need to translate some text with TranslatePipe, we have to include the translate module.

We can also have smart components here. Now, it’s the architectural decision where to put them. We can create a separate module for containers so we can easily distinguish the difference between them. It would also be easier to find them in the code. Another approach is to place them in presentation layer but in that case we’ll break the rule of organizing presentational modules by feature. If you remember the folder structure from the second post in this series, the available options are myApp/src/app/shared/components or myApp/src/app/components folder.

Let’s say we are building a business app which contains of a dashboard and login page. Login page will have a login form with some simple background. Once the user gets signed in he/she will be redirected to dashboard. Dashboard page will contain a sidebar navigation menu, header with user’s image, link to sign out and a list with the most recent added products. The requirement is that every other page contains the same layout, with the sidebar and the header.

We can already see that we’ll need a place to handle all this commonly used logic. We can handle this in several ways and one of them is to create a smart layout component which will delegate it’s child events (from navbar and header) to it’s own sandbox. Another way is to let the root app component do this. We’ll go with the approach of creating a separate container module which will hold all reusable containers in the app. This way every presentational root component (except login) will be wrapped inside the layout container and automatically will inherit sidebar and header. This way the login component can be styled separately without the common layout.

Performance optimization

After we are done with code organization and project architecture we can think about boosting the performance to speed up the application’s initial start. This is not so important at the beginning while the project is still small but as it gets bigger the performance optimization becomes essential.

Service Workers

The first thing we can do is introduce the service workers. Service worker is the middleware between our app and web server. It’s the JavaScript program that the browser runs in the background, in a separate process than our web app. We can use it to control the network requests and to handle the browser cache. In translation, we can tell the browser to cache the application assets after it downloads them for the first time. This way we can pull the assets from the cache and not make the network request. There are a lot of options for controlling the service worker and handling the background syncing. We can specify who has the priority, network or cache, and control what will happen if there’s no network connection. What we are the most interested in is requesting the resources from both the cache and the network in parallel, and responding with whichever returns first. Usually this will be the cached version, if there is one. On the one hand this strategy will always make a network request, even if the resource is cached. On the other hand, if/when the network request completes the cache is updated, so that future cache reads will be more up-to-date.

This decision of which service worker strategy to choose depends a lot on what kind of application we are building and it varies from case to case. If we consider that we have a business app this solution in which we request resources from both the cache and the network satisfies our needs the best.

Regarding the implementation there’s not much to do actually, thanks to Google’s sw-precache plugin responsible for generating the service worker for us, with everything configured. First we need to install the plugin and create sw-precache-config.js file inside our root project folder.

module.exports = {
  navigateFallback: '/index.html',
  stripPrefix: './dist',
  root: '../root/',
  staticFileGlobs: [
    './dist/index.html',
    './dist/**.js',
    './dist/**.css',
    './dist/**.ttf',
    './dist/assets/images/*',
    './dist/config/*',
    './dist/i18n/en.json',
    './dist/i18n/hr.json'
  ],
  runtimeCaching: [{
    urlPattern: /\/api\/lookup/,
    handler: 'fastest'
  }]
};

We defined all the static files we want to cache. We can also cache the http requests by specifying urlPattern and define which type of cache mechanism to use by specifying  handler property. By running the: sw-precache –root=./dist –config=sw-precache-config.js  command in the console sw-precache will generate the service worker for us which we need to include inside the index.html file in order to register the service worker in the browser.

<script>
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker
    .register('/service-worker.js')
    .then(function(registration) {
      console.log('Service Worker registered');
    }).catch(function(err) {
      console.log('Service Worker registration failed: ', err);
    });
  }
</script>

We can automate the process of building the service workers by adding the above command in package.json before building the app for production. This is pretty much it for service worker’s implementation. For more information you can refer to Cory Ryan’s blog post where you can learn about service workers in more details.

Another thing we can do, regarding the performance improvements, is to lazy load some presentational feature modules.

Lazy loading

Lazy loading is a feature which allows loading Angular modules on demand. This is extremely useful in bigger applications that consist of many presentational modules. Imagine again that we have an application with a login screen and dashboard. We can include both modules in the initial JavaScript bundle. Once the user signs in he/she will be automatically redirected to dashboard. If we add more presentational modules, e.g. a CRUD with products, we can load the products module after the user navigates from dashboard to products page or even via service worker in background thread, while the user is still on dashboard page. This way we will break the bundle file into multiple files, speed up the initial application bootstrap time and load the additional modules on demand. More info about lazy loading can be found in Angular docs.

Conclusion

In this final chapter we enriched the application with a couple of extra core features that allow us to build more robust and scalable apps. We also added performance optimizations to speed up the bootstrapping at the start. These small enhancements leave a big impact on the user when launching the application.

Let’s do an overall recap of what we have done through this series of articles:

We hope you got a good understanding of how to make a scalable application architecture from scratch. We have good foundations for adding new features and we should not be scared if our application starts to grow. Now when we have finished with the basics the additional development can begin.

Hello from NETMedia, EU based production team delivering high impact projects for selected direct clients, software/web development and digital agencies.

Like what you see? Let's have a chat about the cooperation.

contact us now
 
Cookies help us deliver our services and better user experience. By using our website, you agree to our use of cookies.
OK
More