In the column “The Angular Adventures”, Manfred Steyer plunges into the midst of the dynamic world of web development. Tips and tricks for Angular developers are just as much a topic as current developments in the JavaScript ecosystem.
Products are usually sold together with services. This is no different with software products. Often, the customer wants to have a purchased standard software adapted to better suit their own business processes. But how does one develop the core of the software in such a way that it becomes adaptable for different purposes?
In this issue, I’ll show you how to do that with Angular. For this I use a simple example with a component to pay for a purchase. Figure 1 shows their default implementation, which allows payment by credit card.
Fig. 1: Standard implementation of the payment area
The component shown consists of two others whose boundaries are indicated by a dashed line. For customization, all of these components, or just part of them, should now be interchangeable. The same applies to services that handle payment processing. An example of such an adjustment is shown in Figure 2 .
Fig. 2: Customer-specific variant of the payment area
As always, the solution presented here is on GitHub .
Angular adjustment at runtime or during build?
There are basically two options for implementing customer-specific adjustments. Either you take care of it at runtime or already during the build, so that the compilation is already tailored to the customer.
If the first variant is preferred, several variants can be implemented in parallel with branches ( if , switch, etc.) and activated, for example, via a configuration. The disadvantage of this possibility is that it inflates the code and reduces its maintainability. The use of Dependency Injection provides a remedy: For example, the application could register a factory function for each service at Angular. This then decides which characteristic of the service is to be used.
If you go one step further, you load the customer-requested service and component variants dynamically at runtime. Dynamic reloading is particularly easy when Web Components are used.
But if you want to get the most out of performance, the customization already integrates in the build. This minimizes the effort at runtime and allows optimizations such as tree shaking. In addition, the customer receives only the licensed program parts. The reloading of individual parts can be done here by means of lazy loading. This is exactly what we will look at here. For this I use the currently very popular Monorepo approach.
Flexibility with mono pos
A monorepo is actually just a folder with related projects ( Figure 3 ).
Fig. 3: Monorepo
Each of these projects can be either an executable application or a reusable library. In the example considered, flight-app is the former, with the payment-lib , which houses the components shown at the beginning, the latter. The variant adapted for a specific customer can be found in the library payment-lib-customer-a .
For all projects, there is a global node_modules folder with common external packages, such as Angular or Bootstrap. Thus, the same versions of the external packages are used everywhere, which prevents version conflicts.
Normally, the individual projects do not directly access each other. Instead, each library provides a public_api.ts file. This file represents a facade in front of the library and publishes those parts that are intended for consumers, but shields implementation details. The facade of the payment-lib can be seen in Listing 1.
Listing 1
export * from './lib/payment-lib.module';
export * from './lib/payment.service';
export * from './lib/default-payment.service';
// Payment-Card
export * from './lib/container/container.component';
// Not customized component with buyer's name
export * from './lib/non-customized/non-customized.component';
// Customized component with payment details
// (e. g. credit card, bank info etc.)
export * from './lib/payment.component';
Since version 6, the Angular CLI can even help build such a project structure. After creating a new project with ng new , it allows you to add subprojects. For example, the ng generate application flight-app statement adds an executable application. Similarly, you can use ng generate library payment-lib to create a library.
So that the individual projects can refer to each other, their paths are mapped to logical names. This happens in tsconfig.json (Listing 2).
Listing 2
"paths": {
"@flight-workspace/flight-api": [
"projects/flight-api/src/public_api"
],
"@flight-workspace/payment-lib": [
"projects/payment-lib/src/public_api"
],
"@flight-workspace/payment-lib-customer-a": [
"projects/payment-lib-customer-a/src/public_api"
]
},
These logical names refer to the facade of the respective library. To refer to it, use the logical name in each import statement:
import { PaymentLibModule } from '@flight-workspace/payment-lib';
From the point of view of the application, there is no difference
between referencing a downloaded npm package and a separate library. In addition, the source code does not have to make any assumptions about the library’s folder. This can thus be easily replaced by modifying the mappings shown. This is exactly the key to the integration of custom extensions.
implementation
Of course, the custom version of the library has an angular module behind its façade. This registers corresponding counterparts for components and services that need to be replaced. Those components and services that the customer would like to take over one-to-one are to be re-registered here (Listing 3).
Listing 3
import { NgModule } from '@angular/core';
// Customized Service
import { PaymentCustomerAService } from './payment-customer-a.service';
// Customized Component
import { PaymentCustomerAComponent } from './payment-customer-a.component';
// Non customized stuff (relative path)
import { ContainerComponent } from '../../../payment-lib/src/lib/container/container.component';
// Non customized stuff (internal mapping)
import { NonCustomizedComponent } from '@internal/payment-lib/non-customized/non-customized.component';
import { PaymentService } from '@internal/payment-lib/payment.service';
@NgModule({
imports: [
],
providers: [
{provide: PaymentService, useClass: PaymentCustomerAService }
],
declarations: [
PaymentCustomerAComponent,
ContainerComponent,
NonCustomizedComponent
],
exports: [
ContainerComponent
]
})
export class PaymentLibModule { }
Of particular note is that building blocks taken over by payment-lib may not be imported via their facade or their logical name. In this case, Angular would also discover the module of payment-lib in the façade. Since this partly registers the same components as the payment-lib-customer-a , Angular reacts with an error message: A component must be deposited with just one module.
To work around this problem, the listing shown demonstrates two options. In the case of the Container Component a relative reference is used. In the case of the other two imports, the example uses an additional logical name. This points to the folder of the library and not to its facade:
"@internal/payment-lib/*": [
"projects/payment-lib/src/lib/*"
]
In order to activate the customization, you only have to adjust the mapping in the tsconfig.json :
"@flight-workspace/payment-lib": [
// "projects/payment-lib/src/public_api"
"projects/payment-lib-customer-a/src/public_api"
]
For this to work, the facade of payment-lib-customer-a must be compatible with that of payment-lib . The same applies to the provided Angular module. The former can easily be ensured by the TypeScript compiler, which requires integration tests for the latter.
Customize Angular Applications – Conclusion
Customizing a product by exchanging libraries in a Monorepo is straightforward and provides the best runtime performance.
One reason for this is that the sources for each customer are compiled
together, which makes optimizations such as tree shaking possible. Due to the additional use of lazy loading, certain parts can only be loaded from the server when needed. In addition, the customer only receives the program parts that he has licensed in this way.
A little annoying is the fact that integration tests have to ensure the
compatibility of the custom libraries with the original libraries.
However, the use of integration tests is a best practice anyway, which
also supports the quality of the development in other ways.
In addition, the development team is also forced to create many small interchangeable libraries in Monorepo. This too is a best practice, especially since it reduces complexity and improves maintainability. This is also supported by the use of one facade per library, which publishes only the public parts for other program parts and hides implementation details.
Source: https://entwickler.de/online/javascript/angular-anwendungen-anpassen-579872320.html
The post Customize Angular Applications: How to integrate custom modifications maintainably appeared first on FrontNet Blog.