Here are my criteria:
A clear and clean structure that allows a new teammate to get involved in our projects more quickly
Excellent extensibility allowing us to add new features without touching the skeleton
Good performance without a long loader making users impatient
Reasonable building time and a small bundle size
Including a mocking and testing mechanism
Completed building scripts for all environments
Let’s start with a global overview:
As you can see from the graphs above, there are two types of modules, the eager loaded ones and the lazy loaded ones. So, what are the differences between the two modules? By default, NgModules are all eager loaded, meaning that they are loaded as soon as the application is bootstrapped even if we don’t need them immediately. As a consequence, the loading time of the welcome page can be extremely long. In order to create better user experiences for the customers, the lazy loading technique is strongly recommended to developers. The technique can help developers to reduce the initial bundle size and the loading time. For the implementation, please refer to https://angular.io/guide/lazy-loading-ngmodules.
So, what are those modules? Let’s see one by one together :)
App Module: the entry point, the default root module for the application
App Routing Module: the root routing module
Core Module: the module where we put all singleton services and we will talk about the module in detail later.
Authorization Module: It’s in charge of the authorization. When the backend denies the users’ access, they will be redirected to the unauthorized page. As you may know, we put the famous “Guard” in this module. If you have never heard about the “Guard”, the following link can help you understand more about it https://angular.io/api/router/CanActivate
Error Handling Module: it contains all error pages except those already existed in the authorization module. For example, we can put our customized 500 error pages of the connection failure to the backend in the module.
Shared Module: where we can put all reusable elements. We will also talk about this module in detail later.
Feature Module & Feature Routing Module: as its name implies, a feature module implements a concrete feature that is composed of several pages, feature services, and a routing module.
The core module is designed for singleton services shared by the whole application. As for reusable services, it should be placed in the shared module. On the other hand, for services only used by one feature module, the best place will be in the feature module itself.
So, which services are singletons?
All interceptor services: for example the http-interceptor service for capturing error responses from the backend
Loader service: not everyone uses a specific service to control the loader. However, I think it is the best idea ever. A singleton loader service can avoid the display of multiple loaders simultaneously. We can simply pass the service wherever we want!
Toaster service: it is similar to the loader service
Configuration service: it reads configuration from the environment file and gives us the right URL of the backend
Analytics service: you may add this service if you are using tools like google analytics
Navigation service: for recording users’ navigation histories
Besides, the core module is a very special module because we should guarantee that it is imported only once in the app module. Therefore, we should create a special constructor with the decorators Optional and SkipSelf:
We know those decorators are for dependency injection. Optional means that it is okay if there’s nothing to inject. In other words, the injection will succeed even if the injector named parentModule does not exist. SkipSelf means skipping itself, the constructor should search for other CoreModule than the module itself.
When the first time the CoreModule is imported, the constructor is called with a null parentModule, so the import will succeed. However, if the CoreModule is imported the second time, since there is already a CoreModule, an error will be thrown.
That’s how we detect if the CoreModule is imported into another module. This is what we don’t want to see in the process of the import of CoreModule.
Unlike the core module, the shared module can be imported as many times as we want. In a large application, we have reusable pipes, components, directives, and services. We can reuse them by declaring them in the shared module and importing the shared module either in a feature module or in the root module sometimes.
However, we may think that an element will be used only by one feature module at the beginning of the implementation, so we directly declare it in the feature module. When we want to reuse the element in another feature module later, we will need to move it into the shared module.
What if we want to use the same component in different applications? The solution will be extracting this component to an angular library. As for the subtle widget, I will prefer to do it in React and import via its CDN link.
Before we say goodbye, I want to share three small points of best practices.
The first one is to choose the right dependencies. For example, if you can simply use a date format pipe or a small DIY method, there is no need to install moment.js. You can’t imagine how big moment.js is, it will be a disaster for the bundle size!
The second one is to avoid using native DOM or jQuery operations directly in angular codes. As Angular becomes stronger and stronger, there are always equivalent Angular solutions. While implementing a modal, instead of opening or closing it with a jQuery selector, I would rather integrate NgbActiveModal of ng-bootstrap, its API offers us all clean operations https://ng-bootstrap.github.io/#/components/modal/api. If you want to access DOM elements in a component, don’t forget the decorators @ViewChild and @ViewChildren.
Last but not least, good communication with the backend is necessary. I have noticed performance problems after I joined a project in the middle of its development. The loading time of the welcome page displaying the product list is extremely long. Therefore, I decided to analyze the logs in the navigator console. You know what, I was shocked by the size of the responses of the backend API. It fetched all the properties of the products that we don’t need them to be displayed on the welcome page but should be displayed on the product detail page. The solution is simple. In this case, we did two separate endpoints in the backend. The first one contains only principle information of the products on the welcome page; the second one, on the other hand, returns all the information of the product with a given id for the product detail page. Therefore, when the communication is bad, we should ask ourselves, do we fetch too much useless information from the backend?
Thank you for reading the article! I know this article is quite general. If you are interested in a particular part, don’t hesitate to contact me. Besides, in order to improve the architecture, please feel free to write your comments below!
The Tech Platform