Angular Platforms in depth. Part 1. What are Angular Platforms?

In this series of articles, I’m going to reveal to you how does it even possible — execute Angular applications across different environments. Also, we’ll learn how to build custom Angular platform which renders applications inside the system’s terminal using ASCII graphics.

Angular Platforms in depth. Part 1. What are Angular Platforms?

The Angular framework was designed to be a platform independent. That approach allows Angular applications to be executed across different environments — browser, server, web-worker, and even mobile devices.

Articles:


Table of contents

  • Angular is a cross-platform framework
  • What are Angular platforms?
  • How do Angular platforms allow cross-platform execution?

Angular is a cross-platform framework

As I stated previously, Angular was designed with flexibility in mind. That’s why Angular is a cross-platform framework, which is not limited by the browser. The only thing which is required by Angular to be executed is a JavaScript engine. Let’s take a look at the most popular Angular environments.

Browser

When we’re creating a new Angular application using Angular CLI ng new MyNewApplication it sets the browser environment as a default environment for our application.

Server

Angular applications can be compiled and executed at the server side. In that case, we can compile Angular application to static HTML files, then send that files to the clients.

That approach allows us to speed up applications loading, also as make sure that application will be properly indexed by all search engines.

Web worker

Also, we can move part of Angular application to the separate thread. To the web worker thread. In that case, we’re leaving only a small part of the application at the main thread, that part will allow the web worker part to communicate with the document API’s.

That approach allows you to produce a smoother UI, free of “janks”, as most of your application’s computations will occur separately from the UI.

Web worker environment was experimental from the beginning and since Angular 8 it’s deprecated.

NativeScript

Also, there is plenty of third-party libraries that allow us to execute Angular applications across different environments. For instance NativeScript. NativeScript enables Angular to be executed on mobile devices utilizing all the functionality of native platforms.

But how does it even possible to execute Angular applications across different environments? ?

The answer is platforms!

What are Angular platforms?

To figure out what are Angular platforms we need to take a look at the entry point of each Angular application — main.tsfile:

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

platformBrowserDynamic().bootstrapModule(AppModule);

Here we could notice two important parts:

  • platformBrowserDynamic() function called and returned some object.
  • That object is used to bootstrap our application.

If we slightly rewrite it we’ll find one interesting detail here:

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { PlatformRef } from '@angular/core';
 
 
// Create Browser Platform
const platformRef: PlatformRef = platformBrowserDynamic();
 
// Bootstrap Application
platformRef.bootstrapModule(AppModule);

platformBrowserDynamic is a platform factory — a function which creates new instances of platforms. The result of the platformBrowserDynamic function call is a PlatformRef. PlatformRef is a plain Angular service which knows how to bootstrap our applications. For a better understanding of how that instance of PlatformRef created, let’s have a deeper look at the platformBrowserDynamic implementation:

export const platformBrowserDynamic = createPlatformFactory(
  
  // Parent platform factory
  platformCoreDynamic,
  
  // New factory name
  'browserDynamic',
  
  // Additional services
  INTERNAL_BROWSER_DYNAMIC_PLATFORM_PROVIDERS,
);

Here we can see that platformBrowserDynamic function is just a result of the createPlatformFactory function, which accepts the following params:

  • Parent platform factory — platformCoreDynamic
  • Name for a new platform — ‘browserDynamic’
  • Additional providers — INTERNAL_BROWSER_DYNAMIC_PLAFORM_PROVIDERS,

Well, platformCoreDynamic here is a parent platform factory function. We can treat the relation between platformCoreDynamic and platformBrowserDynamic as inheritance. So, createPlatformFactory function just helps us to inherit one platform factory from another. Easy as that.

The more interesting part takes place slightly deeper in the inheritance hierarchy. In fact, platformCoreDynamic inherits platformCore which in its turn haven’t got a parent.

So, here is the full hierarchy for platformBrowserDynamic:

But during inheritance, Angular platform factories don’t change the behavior of parent platform factories. Instead, they’re providing additional tokens and services for our applications.

Ok, kind of complicated. Let’s dive deeper into createPlatformFactory function to figure out how exactly Angular platform factories created.

Here is the super simplified code for the createPlatformFactory:

type PlatformFactory = (extraProviders?: StaticProvider[]) => PlatformRef;

export function createPlatformFactory(
  parentPlatformFactory: PlatformFactory,
  name: string,
  providers: StaticProvider[] = [],
): PlatformFactory {

  return (extraProviders: StaticProvider[] = []) => {
    const injectedProviders: StaticProvider[] = providers.concat(extraProviders);

    if (parentPlatformFactory) {
      return parentPlatformFactory(injectedProviders);
    } else {
      return createPlatform(Injector.create({ providers: injectedProviders }));
    }
  };
}

When we’re calling that function, it returns a platform factory function. Which accepts additional StaticProviders for our applications. And if we’re providing parent platform factory, createPlatformFactory function will call it and return its value, otherwise, it’ll just create and return a new platform. Let’s take a look at the platformBrowserDynamic creation process step by step for better understanding:

  1. platformBrowserDynamic created as a result of createPlatformFactory function call with platformCoreDynamic as a parent platform.

2. We’re calling platformBrowserDynamic function to create a new platform.

3. It checks that parentPlatformFactory exists and calls it with additional providers array and then just returns its value:

if (parentPlatformFactory) { 
  return parentPlatformFactory(injectedProviders); 
}

4. On that stage we could notice, that result of the platformBrowserDynamic function is actually a result of platformCoreDynamic function with all services provided by the platformBrowserDynamic .

5. platformCoreDynamic created the same way as platformBrowserDynamicWith two differences — it extends platformCore and provides its own providers.

export const platformCoreDynamic = createPlatformFactory(
  platformCore,
  'coreDynamic', 
  CORE_DYNAMIC_PROVIDERS,
);

Here we could notice that we have the same situation: because of the existence of parent platform we’ll just return parent platform factory result with additional providers:

platformCore([ ...CORE_DYNAMIC_PROVIDERS, ...BROWSER_DYNAMIC_PROVIDERS ]);

6. But inside platformCore we have a slightly different situation.

export const platformCore = createPlatformFactory(
  null,
  'core',
  CORE_PLATFORM_PROVIDERS,
);

Here, CORE_PLATFORM_PROVIDERS is an array which contains the crucial provider — PlatformRef service. As we’re providing null as a parent platform factory, createPlatformFactory function will just return the result of createPlatform function.

7. createPlatform function in its turn will just retrieve PlatformRef from the injector. And return it to the caller.

function createPlatform(injector: Injector): PlatformRef {
  return injector.get(PlatformRef);
}

8. Now we have PlatformRef created:

const ref: PlatformRef = platformBrowserDynamic();

But look, during inheritance platforms doesn’t change the behavior of the PlatformRef explicitly. Instead, they’re providing new sets of services that will be used by the PlatformRef during the bootstrap process.

Here we can notice, that platformCore is not like other platforms. platformCore is kind of special. Because it’s responsible for providingPlatformRef for the platform creation process, and that’s why it serves as a root platform for each platform in the Angular ecosystem.

After that we could say that each Angular platform consists of two important parts:

  • PlatformRef — service that will bootstrap Angular application.
  • Providers — Array of tokens and services provided to be used during bootstrap and execution phases.

How do Angular platforms allow cross-platform execution?

On that stage, we’ve learned what are Angular platforms and how do they created. So, let’s discuss how Angular platforms allow Angular to be the cross-platform framework.

It’s all about abstraction. As we know Angular highly relies on the dependency injection system. And that’s why a pretty big part of Angular itself is declared as abstract services:

  • Renderer2
  • Compiler
  • ElementSchemaRegistry
  • Sanitizer
  • etc.

Services mentioned above and many others declared as abstract classes inside Angular. And when we’re using different platforms, those platforms provide appropriate implementations for that abstract classes. Let me explain, for instance, here we a number of abstract services declared by angular. I do personally prefer to imagine them as blue circles:

But it’s just abstract classes that lack any implementation or functionality. And when we’re using Browser Platform, it provides its own implementations for those services:

However, if we’re using Server Platform, for instance, it provides its own implementations for those abstract core services.

Ok, Let’s have a concrete example here.

For instance, Angular uses DomAdapter abstraction to manipulate DOM in an environment-agnostic way. Here’s a simplified version of the DomAdapter abstract class.

export abstract class DomAdapter {
  abstract setProperty(el: Element, name: string, value: any): any;
  abstract getProperty(el: Element, name: string): any;
  abstract querySelector(el: any, selector: string): any;
  abstract querySelectorAll(el: any, selector: string): any[];
  abstract appendChild(el: any, node: any): any;
  abstract removeChild(el: any, node: any): any;
  
  //... and so on
}

And when we’re using Browser Platform it provides appropriate browser implementation for that abstract class:

export class BrowserDomAdapter extends DomAdapter { ... }

BrowserDomAdapter interacts with browser DOM directly and that’s why can’t be used anywhere outside the browser.

That’s why to be allowed to be executed on the server side for the server side rendering purposes we’re using Server Platform, which in its turn provides another implementation:

export class DominoAdapter extends DomAdapter { ... }

DominAdapter doesn’t interact with the DOM because we haven’t got DOM on the server side. Instead, it utilizes the domino library, which emulates DOM for the node.js.

As a result, we have the following structure:


Conclusion

Congratulations ? you’ve reached the end of the article. During the article, we’ve covered what are Angular platforms, how are they created, went step by step during the platformBrowserDynamic creation process. And finally, figured out how platforms concept allows Angular to be a cross-platform framework.

If you want to get deeper knowledge on Angular platforms, take a look at the rest articles of the series:

Also, follow me on twitter to be notified about new Angular articles as soon as possible!