Micro frontends

As applications complexity increases, frontends are also getting more and more complex. Micro frontend architectures, like microservice architectures, try to reduce the complexity by defining smaller and loosely coupled components that can be deployed individually. Cumulocity IoT is built on top of a micro frontend architecture.

Introduction

There are several options for building a micro frontend architecture:

  1. Server-side - This is the classic approach of a page loaded on a different URL. You can serve unique frontends from different teams (even on separate web servers) on each URL. This already fulfills all the requirements of a micro frontend, as it is loosely coupled, can be developed independently, and deployed separately. However, there are challenges when using this approach, for example, with the communication between the different micro frontends and ensuring a similar look and feel across the other frontends.

Cumulocity IoT already allows using micro frontends via application hosting. You can deploy different frontends via the Application API) and switch between them via the app switcher. If you use the Web SDK, it ensures the same look and feel across all applications.

  1. Compile-time - Another option to allow a micro frontend architecture is to bundle and build your micro frontend as a library and provide it (for example, via npm). The packages can then be used to compose or build a new application. This has many benefits, as the developer has full control. The communication can be clearly defined and the look and feel can be aligned. However, it requires a new build each time one of the components changes. The coupling is much closer.

Cumulocity IoT provides different npm modules which let you use this approach. You can, for example, only use the API client, a default application with the styling, or import different features as Angular modules from the Cumulocity IoT components library. Take a look at the Cumulocity IoT npm options to see all provided libraries.

  1. Runtime - Providing different frontends dynamically while the application is running. You can load different parts of an application in an iframe or only load a script bundle from a different server. However, the communication and coupling with, for example, an iframe is nearly as hard as when using the server side integration. Therefore, the runtime integration is more complex. It allows you to plug deep into the application and shares, for example, the communication or the state layer. New technologies like Module Federation allow for sharing certain dependencies and defining their scope.

We introduced a new plugin concept into Cumulocity IoT which gives you the ability to extend any Web SDK based application at runtime.

Cumulocity IoT is already based on a micro frontend architecture. In fact, the server-side option is a concept of the platform since the beginning. The compile-time option was introduced in 2018. However, there is also a need for the runtime extension of applications, which is why it is introduced to Cumulocity IoT. See the sections below for more information.

Introducing plugins: Dynamically extending platform web applications

Plugins are a new concept to dynamically load features at runtime and allow an extension of any Web SDK based web application. To extend an application:

  1. In the Administration application, clone the application you want to extend, for example clone the Cockpit application.
  2. Open the Cockpit application details and click the Plugins tab.
  3. Click Install plugins and select a plugin.
  4. The application is now extended by the plugin you selected.

This is basically a script injection. The Cockpit application will now request a script called remoteEntry.js from the plugin. In terms of the micro frontend, the application that executes the call is called the “shell” which injects the “remote” into its scope.

Tip
If you have any issue with an application which includes a plugin, you can exclude all plugins via the ?noPlugin=true query parameter.

Those plugins can use any of the concepts that are integrated into the Web SDK. From just adding a certain button on a device to a full feature set with its own navigator node, route, and component. There are many options, but they are all within the borders of the Web SDK. Cumulocity IoT does not support, for example, React or Vue. If you want to use other frameworks, refer to Cumulocity IoT’s compile time integration. We decided to limit the options to give the same developer experience as we use to build current Angular applications. As a developer, you can start your first plugin by using the c8ycli almost the same way you build a new application.

Custom widget plugin

See Add a custom widget to a dashboard > Create the widget components on how to create a simple widget, what its structure looks like and how to add it to your application. The following tutorial focuses on how you can add this widget to an application using the micro frontend architecture and how this process differs from the previous one.

Info
The solution below is fully based on the Module Federation functionality introduced in Webpack 5. For more information on the functionality refer to Webpack: Module Federation.

1. Initialize the widget plugin example

Use the command shown below to start a multi-step process of creating a sample plugin:

c8ycli new

Select the plugin name, for example, “widget-plugin”:

? Enter the name of the project:  (my-application) widget-plugin

Select the version for which you want to create a sample application, for example, “1016.0.233”, as micro frontend are supported with version higher then 10.16.0.0:

? Which base version do you want to scaffold from?
  1015.0.372 (latest)
  1018.106.0 (next)
  1018.0.47
  1017.0.151
❯ 1016.0.233
  1014.0.359
  other

Select an application template as the basis for your plugin, for example, “widget-plugin”:

? Which base project do you want to scaffold from?
  administration
  application
  cockpit
  devicemanagement
  hybrid
  tutorial
❯ widget-plugin

After a few seconds, you should see the following message:

Application created. Go into the folder "widget-plugin" and run npm install

Navigate to your application folder and execute npm install.

The application folder should look like the example shown below. For this tutorial, the most important files are package.json and README.md.

app.module.spec.ts;
jest.config.js;
README.md;
tsconfig.spec.json;
app.module.ts;
package.json;
setup-jest.js;
widget/;
index.ts;
polyfills.ts;
tsconfig.json;

You have now created your first plugin that uses the micro frontend architecture.

2. Differences in approach to creating custom widgets

There are a couple of differences between a simple widget and one that is built according to the micro frontends architecture.

The biggest difference is the package.json file, where fields such as isPackage, package and exports are located. The following list shows the fields and what they are responsible for:

Info

When creating plugins, the custom modules are the backbone of this approach. The exported module is treated as the entry point that links the plugin with the application, which is referred to as the shell. You can create and export several modules, which have to contain ready-made functionality.

Furthermore, these modules behave like lazy loading modules. They are not loaded upfront as one big package, but instead like a collection of smaller packages loaded on demand. You can extend each module with additional features through the HOOK concept, see Extend an existing application and use hooks for more information. For example, a plugin can add another entry to the navigation menu using HOOK_NAVIGATOR_NODES, see Hooking a navigator node for more information.

There is also a difference in how to start the local development server, see the following step for more information on the server’s role.

3. Local server, debugging and deployment

Local server

To facilitate the process of creating a new plugin, the local server command was extended with a new flag to proxy all requests to the shell application “Cockpit”.

Run npm install, then start your local server:

npm start -- --shell cockpit

You should see the following output:

Shell application: cockpit
http://localhost:9000/apps/cockpit/index.html?remotes=%7B%22widget-plugin%22%3A%5B%22WidgetPluginModule%22%5D%7D

The link redirects you to the Cockpit login screen. Once logged in, add the widget-plugin to your dashboard in the Add widget dialogue window shown below:

Add widget

For the rest of the widget editing process follow the process for regular widgets. Refresh your browser to see your changes.

Debugging

Another difference in the package.json file between a regular widget and a widget modified for the micro frontend architecture is the field remote, see example below:

...
"remotes": {
  "widget-plugin": [     // contextPath
    "WidgetPluginModule" // module class name
  ]
}
...
Info
The remotes field is used to import modules. To properly import a module, specify the context path of the plugin (the contextPath field in package.json) followed by the name of the module class.

The plugin imports itself via a field called remotes. We recommend this as the first step in verifying the correctness of the exported module. It facilitates the application debugging. After importing your own modules, execute npm start to see if the local server starts.

To check the plugin at a later stage, we recommend you to control it locally with various shell applications, using npm start -- --shell cockpit.

Deployment

Uploading the widget is the same as for regular widgets. Execute the following commands sequentially:

npm run build

and

npm run deploy

Follow the console prompt to deploy the application to your tenant.

4. Adding a deployed widget to the shell application

To add the uploaded widget-plugin to the dashboard in the Cockpit application, follow these steps:

Your custom widget is now available in your version of the Cockpit application. Navigate to the dashboard where the newly added widget is available in the list of widgets to add.

The widget-plugin was installed from within the Administration application. This is the main difference between the regular and the new approach regarding widgets. The micro frontends architecture allows you to add new functionality while the application is running (runtime), whereas the old approach only allowed new functionality to be added before the application was built (compile time).

Custom package blueprint

With micro frontends it is possible to add new functionality while the application is running (run-time), whereas the old approach only allowed new functionality to be added before the application was built (compile-time).

Blueprints are combinations of multiple UI functionalities that can be hosted by the platform (static files) and can be used to scaffold a new solution from scratch. On the other hand, a package is the composition of plugins and blueprints. As a blueprint can export plugins as well, they can be packed together into one package and deployed to the platform.

Initialize the blueprint example

  1. Use the command shown below to start a multi-step process of creating a sample blueprint:
c8ycli new
  1. Select the plugin name, for example, “package-blueprint”:
? Enter the name of the project:  (my-application) package-blueprint
  1. Select the version for which you want to create a sample application, for example, “1016.0.233”, as micro frontends are supported with version higher then 10.16.0.0:
? Which base version do you want to scaffold from?
  1015.0.372 (latest)
  1018.106.0 (next)
  1018.0.47
  1017.0.151
❯ 1016.0.233
  1014.0.359
  other
  1. Select an application template as the basis for your plugin, for example, “package-blueprint”:
? Which base project do you want to scaffold from?
  cockpit
  devicemanagement
  hybrid
> package-blueprint
  tutorial
  widget-plugin
  administration

After a few seconds, you should see the following message:

Application created. Go into the folder "app-blueprint" and run npm install
  1. Navigate to your application folder and execute npm install.

The application folder should look like the example shown below. For this tutorial, the most important files are package.json and README.md.

app.module.spec.ts;
jest.config.js;
README.md;
tsconfig.spec.json;
app.module.ts;
package.json;
setup-jest.js;
index.ts;
polyfills.ts;
tsconfig.json;

You have now created your first package blueprint that uses Module Federation.

Stepper setup (optional)

The HOOK_STEPPER can be additionally provided to allow application customization during the first load of an application. In this optional step we show a small single step example in which the user can select whether the navigator will be collapsed or not on startup.

  1. Create a new setup-step1.component.ts file with the following content:
import { CdkStep } from "@angular/cdk/stepper";
import { Component } from "@angular/core";
import {
  AlertService,
  AppStateService,
  C8yStepper,
  SetupComponent,
} from "@c8y/ngx-components";

@Component({
  selector: "c8y-cockpit-setup-step1",
  templateUrl: "./setup-step1.component.html",
  host: { class: "d-contents" },
})
export class SetupStep1Component {
  config = {
    rootNodes: [],
    features: {
      alarms: true,
      dataExplorer: true,
      groups: true,
    },
    hideNavigator: false,
    userSpecificHomeDashboard: false,
  };
  pending = false;

  constructor(
    public stepper: C8yStepper,
    protected step: CdkStep,
    protected setup: SetupComponent,
    protected appState: AppStateService,
    protected alert: AlertService
  ) {}

  async next() {
    this.pending = true;
    try {
      const newConfig = { ...this.setup.data$.value, ...this.config };
      await this.appState.updateCurrentApplicationConfig(newConfig);
      this.setup.stepCompleted(this.stepper.selectedIndex);
      this.setup.data$.next(newConfig);
      this.stepper.next();
    } catch (ex) {
      this.alert.addServerFailure(ex);
    } finally {
      this.pending = false;
    }
  }

  back() {
    this.stepper.previous();
  }
}
  1. Create a setup-step1.component.html template:
<form #stepForm="ngForm" name="form" class="d-contents">
  <div class="container-fluid flex-no-shrink fit-w">
    <div class="row separator-bottom">
      <div
        class="col-md-8 col-md-offset-2 col-lg-6 col-lg-offset-3 p-t-24 p-l-16 p-r-16"
      >
        <h3 translate class="text-medium l-h-base">Misc</h3>
        <p class="lead text-normal" translate>
          Miscellaneous settings for the current application.
        </p>
      </div>
    </div>
  </div>
  <div class="inner-scroll flex-grow">
    <div class="container-fluid fit-w">
      <div class="row">
        <div class="col-md-8 col-md-offset-2 col-lg-6 col-lg-offset-3">
          <c8y-misc-config [config]="config"></c8y-misc-config>
        </div>
      </div>
    </div>
  </div>
  <div class="card-footer separator d-flex j-c-center">
    <button
      class="btn btn-default"
      type="button"
      (click)="back()"
      *ngIf="index !== 0"
      translate
    >
      Previous
    </button>
    <button class="btn btn-primary" type="submit" (click)="next()" translate>
      Save and continue
    </button>
  </div>
</form>
  1. Finally we extend the app.module.ts file to include the new stepper components:
import { NgModule } from "@angular/core";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { AssetsNavigatorModule } from "@c8y/ngx-components/assets-navigator";
import { RouterModule as ngRouterModule } from "@angular/router";
import {
  CoreModule,
  BootstrapComponent,
  RouterModule,
  SetupStep,
  HOOK_STEPPER,
  Steppers,
  gettext,
} from "@c8y/ngx-components";
import { SetupStep1Component } from "./setup-step1.component";
import { DatapointLibraryModule } from "@c8y/ngx-components/datapoint-library";
import { MiscConfigComponent } from "./misc-config.component";

@NgModule({
  declarations: [SetupStep1Component, MiscConfigComponent],
  imports: [
    BrowserAnimationsModule,
    RouterModule.forRoot(),
    ngRouterModule.forRoot([], { enableTracing: false, useHash: true }),
    CoreModule.forRoot(),
    DatapointLibraryModule.forRoot(),
    AssetsNavigatorModule,
  ],
  providers: [
    {
      provide: HOOK_STEPPER,
      useValue: [
        {
          stepperId: Steppers.SETUP,
          component: SetupStep1Component,
          label: gettext("Step 1"),
          setupId: "exampleId",
          priority: 0,
        },
      ] as SetupStep[],
      multi: true,
    },
  ],
  bootstrap: [BootstrapComponent],
})
export class AppModule {}

Now on the first application start, users will have to complete the single step wizard above.

Differences in approach to creating custom applications

There are a couple of differences between a simple widget and one that is built according to the micro frontend guidelines.

The biggest difference is the package.json file, where fields such as isPackage and package and exports (not in the current blueprint app) are located. The following list shows the fields and what they are responsible for:

Info
A blueprint can also include plugins, which can later be used to extend other applications.

Deployment

Uploading the package is the same as for regular widgets. Execute the following commands sequentially:

npm run build

and

npm run deploy

Follow the console prompt to deploy the application to your tenant.