Angular architecture

From bibbleWiki
Jump to navigation Jump to search

Introduction To Architecture

  • Planning
  • Organizing Features
  • Structuring Components
  • Component Communication & Concurrent Communication
  • State management
  • Additional Considerations

Planning

Key Things To Think About

  • App Overview, what is for, what are the benefits
  • App Features,
  • Domain Security, are we using roles, tokens etc
  • Domain Rules, client side, server side or both
  • Logging, what will this look like, app insights etc
  • Service/Communication, generally HTTP but could be Web Sockets, Service bus
  • Data Model, what will this look like, do we need view models
  • Feature Components, who will components communicate
  • Shared Functionality, what are we going to be using

Example Data

App Overview

  • Overall Goals
    • Support viewing and editing customers
    • Support viewing orders
    • Secure customer edit form
  • Key Requirements
    • Display customers with card/grid option
    • Support filtering, sorting, and paging customers
    • Map customers (Google maps)
    • Customer editing support (CRUD)
    • Display orders with paging
    • Support login/logout with email/password

App Features

  • Customers Feature
    • Display customers
    • Support card/grid modes
    • Display customers on map
    • Support filtering/paging/sorting
  • Customer Feature
    • Create/Update/Delete customer entity
    • Display customer details/orders/map
    • Form provides validation
  • Orders Feature
    • Display orders
    • Support paging
  • Login/Logout Feature

Domain Security

  • Email/Password for initial release
  • Consider tokens for future release?
    • HttpInterceptor could be used to set auth header
    • What option will be used for token issuer?

Domain Rules

  • Each order must be associated with a customer
  • Order totals should be shown for a given customer
  • Customer edit form should validate data entry
  • User must login to access customer edit form (route guard)
  • Validate login credentials

Logging

  • Create an Angular service for logging
  • Consider using Azure AppInsights
    • Wrap existing AppInsights client SDK
    • Handle logging to different areas based on dev/stage/prod:
    • Console logger
    • AppInsights logger

Service/Communication

  • RESTful Service will be used (Node.js)
  • Angular Services
    • Data Services: Use HttpClient
    • Customers/Orders
    • Login/logout (auth)
    • Sorting service
    • Filtering service
    • Logger service
    • Mediator/EventBus if needed
  • HttpInterceptor will set token for HTTP requests if tokens used (GET/PUT/POST/DELETE/PATCH)

Data Model

  • Application models/interfaces:
    • Customer model/interface
    • Auth model/interface
    • No order editing so include with Customer?
  • Create a class or just use an interface on the client-side?

Feature Components

  • Customers
    • Display/filter/sort/page customers (need data service)
  • Customer
    • Create/read/update/delete customer (need data service and modal needed for deletes)
  • Orders
    • Display/page orders
  • Login
    • Login form and logout functionality (need auth service)

Shared Functionality

  • Toast/Growl (success/information)
  • Modal dialog
  • Google maps
  • Pager
  • Menu
  • Grid/Card (used more than once?)
  • 3rd Party, Ng Bootstrap, Angular Material etc

Angular Style Guide

This can be found here https://angular.io/guide/styleguide This provides,

  • Coding conventions
  • Naming Rules
  • Application Structure
  • Organizing Modules
  • Creating and using components
  • Creating and using services
  • Lifecycle hooks

Other Considerations

  • Accessibility
  • i18n (internationalization)
  • Testing
  • CI/CD, versioning, pipelines
  • APIs

Organizing Features

LIFE

Follow the LIFT appoach

  • Locate code quickly, good naming of folders, modules
  • Identify the code at a glance, good naming of files
  • Keep Flattest structure you can
  • Try to be DRY (Do not repeat yourself)

Organize Code

Reasons to not used Conventions Base

  • Follows strict name conventions
  • Related code may be separated
  • Can result in a lot of files in a folder

Reasons For Feature based

  • Features organized into there own folder
  • Features are self contained
  • Easy to find everything related to the feature

Core and Shared Modules

The examples provide said Core (or Common) module should have

  • Singleton services such as
    • Login, Error, logging

Shared module should have

  • Reusable components, pipes (formatters), directives

Making Core Imported Once

Seemed like a biggy on the course so here is how to do it. It also breaks up an very text driven section :)

@NgModule({
  imports:      [ CommonModule ],
  declarations: [ SomeComponent ],
  exports:      [ SomeComponent ],
})
export class CoreModule {

  constructor (@Optional() @SkipSelf() parentModule: CoreModule) {
    if (parentModule) {
      throw new Error(
        'CoreModule is already loaded. Import it in the AppModule only');
    }
  }

  static forRoot(config: UserServiceConfig): ModuleWithProviders {
    return {
      ngModule: CoreModule,
      providers: [SomeService]
    };
  }
}

And the author was so keen they provided their approach too

export class EnsureModuleLoadedOnceGuard {
   constructor(targetModule :any) {
      if (targetModule) {   
         throw new Error(`${targetModule.constructor.name} has already been loaded.
             Import this module in the AppModule only.`);
      }
   }
}

Create a Library in Angular

This can be done using the angular cli.

# Create library
ng new my-project
cd new my-project
ng g library my-lib
ng build my-lib
# Build and Publish
ng build my-lib
cd dist/my-lib
npm publish

Structuring Components

Container and Presentation Pattern

Like React or probably any web app it is important to separate the container from the presentation.
Arc container presentation.png
The one take away was around the routing which could be used to send email to colleague if available. E.g. https://www.acme.co.nz/person/1. Sometimes with containers this is not always available.
The Angular course defined the following characteristics

  • Presentation
    • Concerned with look
    • HTML Markup and CSS
    • No dependencies on the rest of the app
    • Does specify how data is loaded or changed but emits events using @outputs
    • Receives data via @inputs
    • May contain other components
  • Container
    • Concerned with how things work
    • Has little, not HTML or CSS
    • Has Injected dependencies
    • Are stateful on how data is loaded or changed
    • Top level routes
    • May contain other components

Change Detection

With react this is built in almost using state and the propagation of data to child components. With Angular, we use the change detection strategy. From reading the net it seemed to me that OnPush is the way to go for Container/Presentation unless there is a specific reason not to.

  • Default Change Detection Strategy

By default, Angular uses the ChangeDetectionStrategy.Default change detection strategy. This default strategy checks every component in the component tree from top to bottom every time an event triggers change detection (like user event, timer, XHR, promise and so on). This conservative way of checking without making any assumption on the component’s dependencies is called dirty checking. It can negatively influence your application’s performance in large applications which consists of many components

  • OnPush Change Detection Strategy

We can switch to the ChangeDetectionStrategy.OnPush change detection strategy by adding the changeDetection property to the component decorator metadata:

@Component({
    selector: 'hero-card',
    changeDetection: ChangeDetectionStrategy.OnPush,
    template: ...
})
export class HeroCard {
    ...
}

This change detection strategy provides the possibility to skip unnecessary checks for this component and all it’s child components.

The GIF demonstrates skipping parts of the component tree by using the OnPush change detection strategy: Cd-on-push-cycle.gif
Using this strategy, Angular knows that the component only needs to be updated if:

  • the input reference has changed
  • the component or one of its children triggers an event handler
  • change detection is triggered manually
  • an observable linked to the template via the async pipe emits a new value

Component Inheritance

If we have common inputs and outputs, in Angular, you can use the extend keyword to reuse from a base component. Given the move to functional components I am sure this is not as popular as the author suggested

export class Widget2Compoent extends BaseComponent implements OnInit {
   constructor() {
      super()
   }

   ngOnInit() {
   }
}

Component Communication

Event Bus vs Observable Service

Event Bus

  • Mediator pattern
  • Angular service acts as middleperson between components
  • Components don't know where the data is coming form by default
  • Loosely coupled
  • Relies on subject/observable

The essence of the Mediator Pattern is to "define an object that encapsulates how a set of objects interact". It promotes loose coupling by keeping objects from referring to each other explicitly, and it allows their interaction to be varied independently.Client classes can use the mediator to send messages to other clients, and can receive messages from other clients via an event on the mediator class. Mediator pattern.png
Observable Service

  • Observable pattern
  • Angular service exposes observable directly to components
  • Components know where data coming from
  • Not as loosely coupled as event bus
  • Release on subject/observable

The Observer design pattern is one of the twenty-three well-known "Gang of Four" design patterns that describe how to solve recurring design problems to design flexible and reusable object-oriented software, that is, objects that are easier to implement, change, test, and reuse. Observer pattern.png

ReactiveX Subjects

There are four types of subjects in ReactiveX

  • PublishSubject (only values at subscription
  • BehaviorSubject (1 previous value)
  • ReplySubject (n Previous value)
  • AsyncSubject (last value)

PublishSubject

PublishSubject emits to an observer only those items that are emitted by the source Observable(s) subsequent to the time of the subscription. ReactiveX publish.png

BehaviorSubject (1 previous value)

When an observer subscribes to a BehaviorSubject, it begins by emitting the item most recently emitted by the source Observable (or a seed/default value if none has yet been emitted) and then continues to emit any other items emitted later by the source Observable(s). File:ReactiveX behavior.png

ReplySubject (n Previous value)

ReplaySubject emits to any observer all of the items that were emitted by the source Observable(s), regardless of when the observer subscribes. ReactiveX reply.png

AsyncSubject (last value)

An AsyncSubject emits the last value (and only the last value) emitted by the source Observable, and only after that source Observable completes. (If the source Observable does not emit any values, the AsyncSubject also completes without emitting any values.) ReactiveX async.png

Event Bus Service

Here is an example of using the an event bus service. Event bus.png

Example Event Bus Service

import { Injectable } from '@angular/core';
import { Subject, Subscription, Observable } from 'rxjs';
import { filter, map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class EventBusService {

    private subject$ = new Subject();

    on(event: Events, action: any): Subscription {
         return this.subject$
              .pipe(
                    filter((e: EmitEvent) => e.name === event),
                    map((e: EmitEvent) => e.value)
                  )
              .subscribe(action);
    }

    emit(event: EmitEvent) {
        this.subject$.next(event);
    }
}

export class EmitEvent {

  constructor(public name: any, public value?: any) { }

}

export enum Events {
  CustomerSelected
}

Example Usage of Event Bus

We raise an event using

  selectCustomer(cust: Customer) {
    // send to parent via output property
    // note: could use eventbus as well if desired but output property
    // would be the preferred method for passing data to am immediate parent
    this.customerSelected.emit(cust);
    // Send customer to any eventbus listeners listening for the CustomerSelected event
    this.eventbus.emit(new EmitEvent(Events.CustomerSelected, cust));
  }

We act on the event with

  ngOnInit() {
    //Example of using an event bus to provide loosely coupled communication (mediator pattern)
    this.eventbusSub = this.eventbus.on(Events.CustomerSelected, cust => (this.customer = cust));
  }

Observable Service

The difference this and the event bus is, not only is the service sending the data but it is the source of the changes to the data. Observable service.png

Using the Service

This is as simple as subscribing. I not sure I liked the tight coupling of the two but who knows.

    //Example of using BehaviorSubject to be notified when a service changes
    this.customersChangedSub = this.dataService.customersChanged$.subscribe(custs => (this.customers = custs));

State management

Things which have State

  • Application State, data for the app
  • Session State, user preferences
  • Entity State, data in app, orders, customers etc

Options

Suggestions made to do this was

  • Create a service to do it
  • Use NgRx
  • Use Observable Store (derived from rxJs)

NgRx

NgRx is built on Redux (react) and features

  • Redux + RxJs = NgRx
  • Single source of truth
  • Immutable Data (I like)
  • Provides consistency across team
  • Diagnostic tool

NgRx and Effects.png

Additional Considerations

Security

CORS

  • CORS allows the browser to call a different domain or port
  • Enable on the server as needed
  • Limit allowed domain, headers and methods

CSRF (Cross-Request Request Forgery)

  • Enable CSRF on the server if using cookie authentication
  • Angular will read a token from a cookie set by the server and add it to the request header
  • Change the cookie/header name as appropriate for your server
  • Server will validate the header value

Route Guards

  • Define route guards need by the application based on user or group/role
  • Keep in mind that route guards don't "secure" an application
  • Rely on the server to secure the data, APIs, etc

Sensitive Data

  • Anyone can access the browser developer tools to view variables, local/session storage , cookies, etc.
  • Do not store sensitive data (secrets, keys passwords) in the browser
  • If the API requires a secret to be passed consider using a middle man service
  • Use JWT tokens where possible for server authentication (set appropriate TTL expiration tokens)

HTTP Interceptors

  • HTTP Interceptors and CORS
    • HTTP Interceptors provide a centralized placer to hook into requests/responses
    • Add withCredentials when using cookie and call via CORS
  • HTTP Interceptors and Tokens
    • Interceptors can be used to pass tokens required by services