How to split monolith app with Angular 6
Let`s imagine we have a application where squirrels jump in the forest, seek nuts and gather them into one big Bucket. One day customer come and say this is great app and we have another app with hedgehogs and we want that Big Bucket for hedgehogs too. One way to implement is — copy and paste. But bucket can be BIG and changes to bucket in one application will not be applied to bucket in another one, so the solution is — split part of the application 1 to separate bundle and attach it to both applications, even more — with strongly typed abstractions you can share your bucket with the World and everyone can place the Bucket in its own zoo.
So here is small example.
The goal: Create a BucketWidget, that is reusable between animal apps and can display A FOOD.
ng new squirrelApp --prefix sqrlng generate library bucketLib --prefix bckt
ng generate application hedgehogApp --prefix hdg
Angular CLI 6 will create a project, then generate a library and add another application in workspace.
We can specify ports for apps:
Now both apps can be started with
ng serve hedgehogApp
ng serve squirrelApp
Lets create our Library API
Library has 3 packages:
- model — Interface for data that will be rendered in bucket component
- service — Interface of service that will provide model
- view — Components that will render model
export interface Food {
name: string;
image: string;
}
service — let the app provide real implementations
import {Observable} from 'rxjs';
import {Food} from '../model/models';
import {InjectionToken} from '@angular/core';
export const FOOD_PROVIDER: InjectionToken<FoodService> = new InjectionToken<FoodService>('food.service');
export interface FoodService {
getFood$(): Observable<Food[]>;
}
component — food provider is injected, works only with abstractions, real data will be injected by the Apps
import {Component, Inject, OnInit} from '@angular/core';
import {FOOD_PROVIDER, FoodService} from '../service/food.service';
import {Observable} from 'rxjs';
import {Food} from '../model/models';
@Component({
selector: 'bckt-bucketLib',
template: `
<div>A Bucket</div>
<div *ngFor="let foodItem of food | async">
<div>{{foodItem?.name}}</div>
<img [src]="foodItem?.image">
</div>
`,
styles: []
})
export class BucketLibComponent implements OnInit {
food$: Observable<Food[]>;
constructor(@Inject(FOOD_PROVIDER) private foodService: FoodService) {
}
ngOnInit() {
this.food$ = this.foodService.getFood$();
}
}
Now we need a way to let application provide concrete implementations of FoodService. Let`s define module with providers.
@NgModule({
imports: [
CommonModule
],
declarations: [BucketLibComponent],
exports: [BucketLibComponent]
})
export class BucketLibModule {
static forRoot(providers: Provider[]): ModuleWithProviders {
return {
ngModule: BucketLibModule,
providers
};
}
}
— public API: view+model abstraction+service abstraction
export * from './lib/service/food.service';
export * from './lib/model/models';
export * from './lib/bucket-lib.module';
The last part. Use our library.
ng build bucketLib --prod
After build our library, we can test it creating symlink.
cd dist/bucket-lib
npm link
then back to project root and
npm link bucket-lib
our library was added as symlink and is ready to use.
SquirrelApp
If lib was published to repo then install it (in case of symlink — its already in node_modules)
npm install bucket-lib
In Squirrel app we created NutsProvider which implements FoodProvider and provide… nuts :)
import {Injectable} from '@angular/core';
import {Food, FoodService} from 'bucketLib';
import {Observable, of} from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class NutsProviderService implements FoodService {
constructor() {
}
getFood$(): Observable<Food[]> {
return of([{
name: 'Squirrel found a Nut!',
image: 'https://vignette.wikia.nocookie.net/mystiqueisland/images/6/68/Acorn_Body.png/revision/latest?cb=20130623190324'
}]);
}
}
and wire Bucket with NutsProviderService in the app.module
import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import {AppComponent} from './app.component';
import {BucketLibModule, FOOD_PROVIDER} from 'bucketLib';
import {NutsProviderService} from './service/nuts-provider.service';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
BucketLibModule.forRoot([
{
provide: FOOD_PROVIDER,
useClass: NutsProviderService
}
])
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {
}
Hedgehog App
The same steps:
Create ApplesProvider as a FoodService and provide apples to the Bucket
import {Injectable} from '@angular/core';
import {Observable, of} from 'rxjs';
import {Food, FoodService} from 'bucketLib';
@Injectable({
providedIn: 'root'
})
export class ApplesProviderService implements FoodService {
constructor() {
}
getFood$(): Observable<Food[]> {
return of([{
name: 'Hedgehog found an apple',
image: 'https://images.pexels.com/photos/102104/pexels-photo-102104.jpeg?auto=compress&cs=tinysrgb&h=350'
}]);
}
}@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
BucketLibModule.forRoot([
{
provide: FOOD_PROVIDER,
useClass: ApplesProviderService
}
])
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {
}
Thats it! The last part is just add bucket to the app:
And as result:
So, conclusions: with Angular 6 we can easily move parts of monolith application logic to separate shared packages which can be used across multiple applications.
PS: during writing, no animals were harmed
PPS: GitHub
PPPS: highly recommended as high level architecture view http://atomicdesign.bradfrost.com/chapter-2/