DevUI is a team with both design and engineering perspectives, serving Huawei cloud
DevCloudPlatform and huawei internal several background systems, service designers and front-end engineers.

Official website:
devui.design

Ng Component library:
ng-devui(Welcome Star)

The introduction

Remember when Dan Abramov, the author of Redux, tweeted about the micro front end in nineteen nineteen. There was a lot of debate about the micro front end. Many people said that the micro front end was bogus. It has been proved that the micro front end has been gradually accepted by the industry as another front-end architecture mode after modularization and componentization. It will play an increasingly important role in the middle and back enterprise application development scenarios of large TOBS. Therefore, it is time to talk about the micro front end. This paper is divided into two parts. In the first part, it mainly discusses the origin and application scenarios of micro-front-end, DevUI explores the evolution of micro-front-end and makes a detailed study of single-SPA. In the second part, it takes DevUI’s micro-front-end transformation process as an example to discuss how to develop an enterprise-level micro-front-end solution in detail. It is hoped that this article can serve as an important reference for microfront-end researchers to get into the pit.

The origin of

The concept of microfront-end is that with the rise of back-end microservices, business teams are divided into different small development teams, each team has roles such as front and back end, test, etc. Back-end services can call each other through HTTP or RPC, and interface integration and aggregation can be carried out through API gateway. With that comes the expectation that the front-end team will also be able to independently develop microapplications, and then at some stage (build, Runtime) aggregate these microapplications into a complete, large-scale Web application. The concept came up on the Radar of ThoughtWorks technology in 2016.

                 

For micro front-end concept, its essence is the reuse of web applications and integration, especially after the single-page applications appear, each team can’t according to the server before routing straight out of the set pattern to develop the page template, the routing is been taken over the front, so two of the most important problem is how to integrate the web application and integration, in which phase The final implementation will vary greatly depending on your business scenario, but for most teams, the common appeal of thinking about the micro front end architecture is as follows:

  • Independent development, independent deployment, incremental updates: As shown above, team A, B, and C should be unaware of each other, and each sub-application should be developed, deployed, and updated at its own release pace.
  • Stack agnostic: Teams A, B, and C can develop in whatever framework they want without forcing consistency
  • Runtime isolation and sharing: During the runtime, applications A, B, and C form A complete application and are accessed through the main application portal. Ensure that the JS and CSS corresponding to A, B, and C are isolated from each other and that A, B, and C can communicate with each other or share data.
  • Good experience of single-page application: When switching from one sub-application to another, the route change does not reload the entire page. The switching effect is similar to intra-site route switching of a single-page application.

Web Integration

Generally speaking, there are two stages of application integration, namely construction time and run time. The corresponding implementation methods of different stages are also different. Generally speaking, there are mainly the following:

  • Build-time integration: Git sub Module or NPM package is integrated into the main application repository during the construction phase. Team A, B and C develop independently, which has the advantage of simple implementation and shared dependencies. The disadvantage is that A, B and C cannot be updated independently. As follows:

                     

This approach is more suitable for small teams, because when the package is more and more, it will lead to frequent updates of the main application. In addition, it will increase the construction speed of the main application, and the code maintenance cost will be higher and higher. Therefore, most people choose the micro front-end architecture, hoping to integrate at runtime.

  • Server template integration: define templates on the home page of the main application, and let the server dynamically select which sub-application of the integration team A, B, and C through routing through SSI technology similar to Nginx, as follows:

index.html

<html lang="en" dir="ltr">
  <head>
    <meta charset="utf-8"> <title>Feed me</title> </head> <body> <h1>content here</h1> <! --# include file="$CONTENT.html" -->
  </body>
</html>Copy the code

The corresponding nginx configuration is nginx.conf

server {
    root html;    # SSI configuration starts
    ssi on;  
    ssi_silent_errors on;  
    ssi_types text/shtml;  
    The SSI configuration is complete
    index index.html index.htm;    rewrite ^/$ http://localhost/appa redirect;

    location /appa {
      set $CONTENT 'appa';
    }
    location /appb {
      set $CONTENT 'appb';    }
    location /appc {
      set $CONTENT 'appc'}}Copy the code

Team A, B, and C ultimately produce A template file located on the server, similar to PHP, JSP server integration is basically the same principle, at the server side through routing to select different templates, assemble the home page content and return. This mode first goes against the general trend of separation of front and back ends, resulting in coupling. Meanwhile, it requires a certain amount of work to maintain the server end, and is not suitable for large-scale single-page application integration scenarios.

  • Runtime Iframe integration: It can be said that this method should be the simplest and most effective in the early stage. Different teams develop and deploy independently. Only one master application needs to point to the corresponding address of application A, B, and C through Iframe. That is as follows:
<html>
  <head>    
  <title>index.html</title>
  </head>
<body>   
 <iframe id="content"></iframe>    
 <script type="text/javascript">        
    const microFrontendsByRoute = {           
    '/appa': 'https://main.com/appa/index.html'.'/appb': 'https://main.com/appb/index.html'.'/appc': 'https://main.com/appc/index.html'}; const iframe = document.getElementById('content');       
    iframe.src = microFrontendsByRoute[window.location.pathname];        
    window.addEventListener("message", receiveMessage, false);       
   function receiveMessage(event) {            
   var origin = event.origin           
   if (origin === "https://main.com"{/ /do something       
    }             
  }        
 }    
  </script>
 </body>
</html>Copy the code

However, this approach also has obvious disadvantages, especially poor user experience. The reload of the whole page brought by iframe and problems in some scenarios (such as some global dialog or modal display in IFrame, loss of secondary routing state, session sharing, difficult development and debugging), etc. Still, it is not the preferred web integration solution in a microfront-end mode.

  • Runtime JS integration: Generally, there are two modes of this integration. The first mode is to package application A, B, and C into different bundles, load different bundles through loader, dynamically run the bundle logic, and render the page as follows:

                

At this time, application A,B, AND C are completely unaware of each other, and can be developed using any framework. The application switch caused by route switching will not cause page reload. In runtime, if A,B, and C want to communicate, CustomEvent or custom EventBus can be used. C can also implement application isolation through the isolation mechanisms of the different frameworks themselves, or through some sandbox mechanism, which looks good.

The second mode of runtime integration is the use of Web Components. Applications A, B, and C eventually write their business logic into A Web Component and package it into A bundle, which is then loaded, executed, and rendered by the main application, as follows:

<html> 
 <body>    
<script src="https://main.com/appa/bundle.js"></script>    
<script src="https://main.com/appb/bundle.js"></script>    
<script src="https://main.com/appc/bundle.js"></script>    
<div id="content"></div>    
<script type="text/javascript">          
const routeTypeTags = {        
'/appa': 'app-a'.'/appb': 'appb'.'/appc': 'app-c'}; const componentTag = routeTypeTags[window.location.pathname]; const content = document.getElementById('content');     
 const component = document.createElement(componentTag);      
 content.appendChild(component);   
 </script>  
 </body>
</html>Copy the code

This approach will generally have browser compatibility problems (polyfill needs to be introduced), which is not very friendly to users of the three frameworks (component writing mode, transformation cost, WebComponents version component library, development efficiency, ecology, etc.). For complex applications, this development mode will encounter many pitfalls. It’s not worth trying, though, and it can be done if some areas of the page (small pieces) need to be deployed independently (DevUI currently uses Web Components for rendering some areas of the page). There are many frameworks out there that have taken some of these limitations into account and are optimized to work as best as possible out of the box. Stencil is recommended to help you develop quickly.

To sum up, from the perspective of Web application integration, the current micro front-end architecture is more suitable for the runtime to construct a master-slave application model structure through JavaScript, and then integrate different sub-applications through different ways. Corresponding to the different methods mentioned above, DevUI has actually gone through the following stages.

DevUI front-end integration pattern evolution



As shown in the figure above, the devUI front end features prominently:

1) There are many services, and each service has its own front-end code repository, which needs to be independently developed, tested and deployed;

2) Each service is a single-page Angular application with a header and content area at the front. Only the Content area is different and the headers are the same.

In simple terms, it is independently developed by each business team and distributed to corresponding different services through routing. The front end of each service is a complete single-page application. For such a business scenario, the service integration and reuse pattern generally goes through the following stages.

Stage 1: Common componentization + hyperlinks between services

At this stage, header and other areas used by each service are separated into components to solve the reuse problem. The most common hyperlinks are still used to jump between services, as shown below:



The biggest remaining problems at this stage are as follows: obvious blank screen for inter-service hops, disconnection of session management between services, re-verification of inter-service hops, and poor user experience.

The root causes of this problem are as follows:
1) Angular client-only rendering requires Angular runtime and header static resources to load before rendering. This process usually takes about 1 second, during which time there are no elements on the page. The solution is usually SSR or pre-rendering.
2) Each service subdomain name is different. The previous login status (sessionId) is stored based on each service subdomain name, which cannot be shared, resulting in repeated login and authentication.

Phase 2: App Shell (Pre Render) + Session sharing

There are standard solutions to the problem of single-page applications rendering white screens, usually using SSR(server rendering) and Prerender. The difference between the two is that SSR performs some logic on the server (usually Node) to generate the HTML corresponding to the current route first and then return it to the browser. Prerender is usually HTML generated during build based on some rules that are returned to the browser when the user accesses it, as follows:



In terms of user experience and performance, SSR is optimal, but it costs a lot to have an ENTIRE site (each service requires an additional Node rendering layer, SSR has high code quality requirements, and Angular’s SSR is not mature enough), so we choose Prerender. Solve the blank screen problem by generating an App Shell in the build phase as follows:



At this stage, we divided the logic of the header and other services into two parts. One part is the left part of the header that can be seen directly when the page is refreshed. This part, together with some global state and a built-in Event bus(for communication), is all made into an NPM package. The right side of the header is still an Angular component, and the business needs to import it into its component tree at runtime. When the user accesses index.html, the shell part of the entire application is rendered. Then the angular static resources are loaded. Then the header drop-down menu on the right is rendered and the business content is rendered. When the contents of the drop-down menu to the right of the header are rendered successfully, we append them to the entire header area.

At the same time, the way we passed through a subdomain session sharing between also solve the jump between service login to check the problem, by the progressive rendering + pre-rendered model, enhance the user experience, although is a multi-page application, but the service jump between optimized the sense that gives a person or station to jump, at the same time to ensure that different independent development team, Deployment. The biggest legacy of this phase is that common components such as header are still distributed to different services in the form of NPM packages. Once the public logic on header is updated, each business will have to passively release version, resulting in a waste of manpower. Therefore, everyone hopes to decouple the common components.

Stage 3: Widgets (Microapplications)

At this point, a common component like header is a very complex component that represents most of devUI’s common logic. In addition to rendering some of its own views, it also needs to perform most of the common logic, cache interface request data and so on for business consumption, so at this stage, We want components like header to be independently developed, deployed, and integrated with each business at run time by a common team to form a complete application, as follows:



What we want is for the business to develop its own logic, for the header to develop common logic, not to interfere with each other, to publish updates independently, and then at run time, the business references the header through something like a header-loader (runtime reference here) in such a way that, In this way, services are not aware of the passive upgrade caused by the common logical update of the header. So the core issue here is how the header is integrated, and according to the previous chapter, there are two ways to integrate using iframe and dynamic rendering using javascript. Iframe is obviously not suitable for such scenarios, both in terms of implementation and complexity of business communication, so we will implement it in a way similar to Web Components (you can choose any framework as long as it satisfies the mode of loading bundles, executing logic and rendering).



In this stage, we solved the problem of passive business update caused by public logic update, greatly reduced the work of the business and greatly improved the response speed of public logic update rollback, and the business and public logic were independently developed and deployed. Basically, within a large application, the various sub-businesses and the common team coexist in a friendly way. But there are always challenges, and the business is aiming higher.

Stage 4: Choreography and integration across applications

Imagine A scenario like this. For A large enterprise with many middle and background applications, it can be regarded as an application pool (application market). For some businesses, I hope to take C, D and E from the application market and integrate them into A large business A for users to use. At the same time, I hope to take D, E and F out of the application market and integrate them into A large business B, providing A unified entrance for users to use. Among them, applications A, B, C, D, E and F are developed and maintained by different teams. In this case, there needs to be a mechanism to define what rules a standard sub-application should follow and how the main application should integrate (load, render, execute logic, isolation, communication, response routing, dependency sharing, framework independence, etc.)



Such a mechanism, it is need to discuss the content of the micro front-end itself, at this stage, but how to realize the complexity depends on your business, it can be simple, can also be very complex, can even do a service products and provide a whole set of solutions to help you achieve the goal of this micro front-end architecture system (reference), DevUI as a whole is currently in this exploratory phase, some of which will be covered in the second half of this article. At present, according to such requirements, we first need to study how to realize the micro front end under the master-slave application mode.

Single – SPA use

There are many solutions for micro-front-end implementation in the whole industry. Single-spa is widely accepted as the first implementation of micro-front-end solutions based on the master-slave mode, and has also been used for reference by various subsequent solutions (such as Qiankun, Mooa, etc.). It is no exaggeration to say that if we want to study micro-front-end, Single-spa and its principles need to be studied in depth first.

Single-spa classifies microfronts into three categories:

  • Single-spa standard subapp: Can render different components for different routes through single-SPA, usually a complete subapp;
  • Single-spa sing or sing a parcel is not usually associated with routing but is simply an area on the page (similar to the widget).
  • Utility Modules: Independently developed submodules that do not render pages and only perform common logic.

The first two categories are the focus of our research. Here, we use Angular8 as an example to demonstrate the use of single-SPA and the above concepts

Step1 create a child application: first create a root directory

mkdir microFE && cd microFE

Then use the Angular CLI to generate two projects in that directory:

ng new my-app --routing --prefix my-app

Introduce single-SPa-Angular at the root of the project. (Because single-SPA is a micro-front-end framework independent of the specific framework, each framework project is rendered differently. In order to abstract the child application written by each framework into a standard single-SPA child application, Single-spa-angular is an angular adaptation library. Other frameworks can be found here.)

ng add single-spa-angular

This operation does the following things:

1) Change an Angular app entry from main.ts to main.single-spa.ts, as shown below:

import { enableProdMode, NgZone } from '@angular/core';

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { Router } from '@angular/router'; Import {ɵAnimationEngine as AnimationEngine} from'@angular/animations/browser'; 
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
import singleSpaAngular from 'single-spa-angular';
import { singleSpaPropsSubject } from './single-spa/single-spa-props';


if (environment.production) {
  enableProdMode();
}
const lifecycles = singleSpaAngular({
  bootstrapFunction: singleSpaProps => {
    singleSpaPropsSubject.next(singleSpaProps);
    return platformBrowserDynamic().bootstrapModule(AppModule);
  },
  template: '<my-app-root />',
  Router,
  NgZone: NgZone,
  AnimationEngine: AnimationEngine,
});

export const bootstrap = lifecycles.bootstrap;
export const mount = lifecycles.mount;
export const unmount = lifecycles.unmount;Copy the code

From this point of view, a standard single-SPA sub-application needs to expose three life-cycle operations, namely bootstrap, mount, and unmount phases.

2) Create two files in SRC/single-sp-props to pass custom properties, and asset-url.ts to dynamically obtain static resource paths for the current application

3) Create an empty route in the SRC directory, so that a single application cannot find a route when switching between applications

app-routing.module.ts

const routes: Routes = [  { path: '* *', component: EmptyRouteComponent }];Copy the code

4) Add two commands in package.json: Build :single-spa and serve:single-spa to build and start a single-SPA child, respectively.

5) Create a custom webpack configuration file in the root directory, introducing the single-SPa-Angular Webpack configuration (which we will examine later)

Add a base href/to app-routing.module.ts to avoid collisions between the entire angular route and the entire angular route:

@NgModule({  
 imports: [RouterModule.forRoot(routes)],  
 exports: [RouterModule],  
 providers: [{ provide: APP_BASE_HREF, useValue: '/' }]})
 export class AppRoutingModule { }Copy the code

Using the NPM run serve:single-spa command will launch a single-spa child application on the corresponding port (4201 in this case) as follows:



Nothing is rendered on the page, but the corresponding single-spa is built as a main.js bundle and mapped to port 4201.

At the same time, follow the above steps to create another application, my-app2, and map its bundle to port 4202. At this time, our directory structure is as follows:

  • My-app: Single-SPA subapp 1
  • My-app2: Single-SPA sub-app 2
Step2 create a master application:

We create a root-html file in the project root directory to generate a package.json file

npm init -y && npm i serve -g

{  "name": "root-html"."version": "1.0.0"."description": ""."main": "index.js"."scripts": {    
      "start": "serve -s -l 4200"  
     }, 
   "keywords": []."author": ""."license": "ISC"
}Copy the code

In scripts, serve is called to start a Web server mapping the contents below the directory

Create an index.html file in this directory with the following contents:

<! DOCTYPE html> <html> <head> <meta http-equiv="Content-Security-Policy" content="default-src * data: blob: 'unsafe-inline' 'unsafe-eval'; script-src * 'unsafe-inline' 'unsafe-eval'; connect-src * 'unsafe-inline'; img-src * data: blob: 'unsafe-inline'; frame-src *; style-src * data: blob: 'unsafe-inline'; font-src * data: blob: 'unsafe-inline';">
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>Your application</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="importmap-type" content="systemjs-importmap">
    <script type="systemjs-importmap">
      {
        "imports": {
          "app1": "http://localhost:4201/main.js"."app2": "http://localhost:4202/main.js"."single-spa": "https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.5/system/single-spa.min.js"
        }
      }
    </script>
    <link rel="preload" href="https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.5/system/single-spa.min.js" as="script" crossorigin="anonymous" />
    <script src='https://unpkg.com/[email protected]/minified.js'></script>
    <script src="https://unpkg.com/zone.js"></script>
    <script src="https://unpkg.com/[email protected]/dist/import-map-overrides.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/4.0.0/system.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/4.0.0/extras/amd.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/4.0.0/extras/named-exports.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/4.0.0/extras/named-register.min.js"></script>
  </head>
  <body>
    <script>
      System.import('single-spa').then(function (singleSpa) {
        singleSpa.registerApplication(
          'app1'.function () {
            return System.import('app1');
          },
          function (location) {
            return location.pathname.startsWith('/app1'); }); singleSpa.registerApplication('app2'.function () {
            return System.import('app2');
          },
          function (location) {
            return location.pathname.startsWith('/app2');
          }
        )
        
        singleSpa.start();
      })
    </script>
    <import-map-overrides-full></import-map-overrides-full>
  </body>
</html>Copy the code

When the page is refreshed, we use SystemJS to load single-SPA first. When the file is loaded successfully, we define the entry files of the two sub-applications one and two. Each sub-application needs to provide an activity function for single-SPA to determine which sub-application is active under the current route. Loading function, which static resources need to be loaded when switching to the corresponding sub-application. Relevant knowledge of SystemJS and import maps can be checked by yourself. Here, it can be simply interpreted as a bundle loader. You can achieve the same effect in simple cases using dynamic script tag inserts. NPM run start localhost:4200/app1

At this point, it is already possible to route between App1 and App2 to achieve the effect of in-site hops. If you want to configure secondary routing for sub-applications, refer to the code later in this article.

Step3 create a parcel application.

The above two steps enable switching between routing sub-applications. If you want a single team to develop a page fragment and integrate it into any of the above applications, single-SPA provides the concept of parcel after 5.x, In this way, a component written by another framework can be loaded and displayed in any child application.

We first create a new project in the root directory using vue-cli:

vue create my-parcelCopy the code

Then add single-SPA under this item (I won’t go into details here, but you can see what the documentation does).

vue add single-spa

Then build and start the Parcel application, NPM Run Serve

This will also start a child application bundle from the vue project on port localhost:8080. We will configure it in the root application’s index.html so that SystemJS can find it.



Then we load and show it in my-app2.

My – app2 app.com ponent. Ts

import { Component,ViewChild, ElementRef, OnInit, AfterViewInit } from '@angular/core';
import { Parcel, mountRootParcel } from 'single-spa';
import { from } from 'rxjs';
@Component({  
selector: 'my-app2-root', 
templateUrl: './app.component.html',  
styleUrls: ['./app.component.css']})
export class AppComponent implements OnInit, AfterViewInit {
  title = 'my-app2';  
  @ViewChild('parcel', { static: true }) private parcel: ElementRef;  
  ngOnInit() {    
     from(window.System.import('parcel')).subscribe(app => { mountRootParcel(app, { domElement :this.parcel.nativeElement}); }}})Copy the code

In init, we get the mount point of a parcel on the component, load the Vue child application bundle, and then call the mountRootParcel method provided by Single-SPA to mount the child component (application). The second argument passed by this method is the DOM element of the mount point. The first argument is a parcel child. The important difference between a parcel child and a single-SPA child is that a Parcel application can expose an optional update method

Vue project main.js

import './set-public-path';
import Vue from 'vue';
import singleSpaVue from 'single-spa-vue';
import App from './App.vue';
Vue.config.productionTip = false;
const vueLifecycles = singleSpaVue({
 Vue,
 appOptions: {render: (h) => h(App),
},
});
export const bootstrap = vueLifecycles.bootstrap;
export const mount = vueLifecycles.mount;
export const unmount = vueLifecycles.unmount;Copy the code

When we switch to the App2 child app, our View component is also displayed:



Principle analysis of single-SPA

In the previous chapter, we used single-SPA to enable different sub-applications to load a Parcel application in routing switching and non-routing mode. Now that you know what single-SPA is and how it works, you must be curious about what goes on inside single-SPA to implement such a mechanism. Let’s take a look at the internal logic of single-SPA and single-spa-Angular.



Applicaitons and sing module: Singlebus SPA exposes two kinds of apis. One is the Applicaitons API that can be directly introduced from single-SPA, usually for child and master applications, and the other is the n-sing API, usually for parcels. Corresponding to these two modules, the relevant API can be refer to here;

Devtools module: After Single-SPA5 comes a DevTools that allows you to view the status of your child application directly from Chrome, etc. So the DevTools module is mainly the developer tools need to use some API package assigned to window.__single_spa_devtools__. ExposedMethods variable, devTools calls;

Utils module: Utils module is mainly for browser compatibility, implement some method functions;

Lifecycles module: The Lifecycles module abstracts the lifecycle of single-SPA and Parcel subapps, defining the following phases:

  • For single-SPA sub-applications: Load -> Bootstrap ->Mount ->Unmount->Unload
  • For parcel subcomponent (application):bootstrap->Mount->Unmount ->Update

For both parcel and single-SPA child applications, there are at least three phases of methods exposed — bootstrap, mount, and unmount — that single-SPA calls in different life cycles as it switches between applications. All three phases are implemented differently by different frameworks, and single-SPA cannot bridge the gap, only through additional single-spa-Angular or single-spa-vue library functions.

Navigation module: When route switching is applied to a single page, two different events are usually triggered, hashchange and popState, corresponding to both hash and history routes. Single-spa listens for these events globally in the Navigation module. When a sub-application route is switched (matching the route), the index. HTML is first entered, the single-SPA takeover of the current route is performed, the activity function configured when the sub-application is registered is called according to the current route to determine which sub-application it belongs to, and then the loading function is called to load the sub-application. According to the previous life cycle of the subapplication, unmount the old application from the current route. At the same time, bootstrap is called to start the new application and mount the new application. Singles – SPA also provides an API to manually trigger application switching, which is the same as the passive route refreshing mechanism. The module also provides a reroute method as an entry point, which performs the above operations in turn when switching routes.

Jquery-support. js: Since jquery uses event handlers, many event handlers are bound to Windows, which requires special handling if hashchange and popState events are registered using jquery.

Start. js: Explicitly start single-SPA by bringing in all reroute logic in navigation.

Single-spa.js: Aggregates the apis exposed by the above modules as an entry point to single-SPA and exports them for external invocation.

For single-SPA applications, the bootstrap, mount and Unmout lifecycle methods must be implemented, as shown below:



The app2 module loaded with systemJS has bootstrap, mount, and unmount methods.

According to the above analysis, the general principle and process of single-SPA are as follows:



Steps 5,7, and 8 need to be implemented with a library like single-spa-Angular due to framework differences. Let’s see how this is implemented in single-spa-Angular.

Single – SPA – presents analysis

Single-spa-angular is divided into four parts. The SRC directory structure is as follows:



Each of these sections corresponds to what we did in the previous section with ng add single-spa-angular:

The webpack directory: index.ts contains the following contents:

import * as webpackMerge from 'webpack-merge';
import * as path from 'path'

export default (config, options) => {
  const singleSpaConfig = {
    output: {
      library: 'app3',
      libraryTarget: 'umd',
    },
    externals: {
      'zone.js': 'Zone',
    },
    devServer: {
      historyApiFallback: false,
      contentBase: path.resolve(process.cwd(), 'src'),
      headers: {
          'Access-Control-Allow-Headers': The '*',
      },
    },
    module: {
      rules: [
        {
          parser: {
            system: false
          }
        }
      ]
    }
  }
  // @ts-ignore
  const mergedConfig: any = webpackMerge.smart(config, singleSpaConfig)
  removePluginByName(mergedConfig.plugins, 'IndexHtmlWebpackPlugin');
  removeMiniCssExtract(mergedConfig);

  if (Array.isArray(mergedConfig.entry.styles)) {
    // We want the global styles to be part of the "main" entry. The order of strings in this array
    // matters -- only the last item in the array will have its exports become the exports for the entire
    // webpack bundle
    mergedConfig.entry.main = [...mergedConfig.entry.styles, ...mergedConfig.entry.main];
  }

  // Remove bundles
  delete mergedConfig.entry.polyfills;
  delete mergedConfig.entry.styles;
  delete mergedConfig.optimization.runtimeChunk;
  delete mergedConfig.optimization.splitChunks;

  return mergedConfig;
}
function removePluginByName(plugins, name) {
  const pluginIndex = plugins.findIndex(plugin => plugin.constructor.name === name);
  if(pluginIndex > -1) { plugins.splice(pluginIndex, 1); }}function removeMiniCssExtract(config) {
  removePluginByName(config.plugins, 'MiniCssExtractPlugin');
  config.module.rules.forEach(rule => {
    if (rule.use) {
      const cssMiniExtractIndex = rule.use.findIndex(use => typeof use === 'string' && use.includes('mini-css-extract-plugin'));
      if (cssMiniExtractIndex >= 0) {
        rule.use[cssMiniExtractIndex] = {loader: 'style-loader'}}}}); }Copy the code

We introduced this configuration in the previous section with a webPack custom configuration file and let Angular-CLI use this configuration packaging. All this configuration does is package our last exported bundle in UMD format and give it an exports called app3 that extracts zone js. To avoid webpack overwriting the system global variable, set the system under parser to false. The only thing left to do is to remove all entries including global CSS and keep the main entry. This ensures that an Angular child app ends up with a single main.js package.

The Schmatics directory: Schematics extends or overrides the Angular CLI’s add command to perform custom operations on it. I won’t stick with the core code that executes in schematics, but it turns out that when you type ng add single-spa-angular, it does four things:

1) Update package.json in the project root directory to write single-spa-angular dependencies such as @angular-builders/custom-webpack, single-spa-angular, etc.

Ts, single-spa-props. Ts, asset-url.ts, extra-webpack.config.js;

3) Update angular.json to use @angular-builders/custom-webpack:browser and @Angular-builders /custom-webpack:dev-server Builder

Json adds two new commands: Build :single-spa and serve:single-spa to build and launch single-SPA child applications.

Builder directory: What is angular Builder? You need to know that you can override the Build and serve commands that extend the Angular CLI. The build:single-spa and serve:single-spa commands were implemented using Builder before Angular8 and custom WebPack after Angular8. If you use Angular8 or above, This code is not executed here.

Browser-lib directory: the core code is as follows

/* eslint-disable @typescript-eslint/no-use-before-define */
import { AppProps, LifeCycles } from 'single-spa'

const defaultOpts = {
  // required opts
  NgZone: null,
  bootstrapFunction: null,
  template: null,
  // optional opts
  Router: undefined,
  domElementGetter: undefined, // only optional if you provide a domElementGetter as a custom prop
  AnimationEngine: undefined,
  updateFunction: () => Promise.resolve()
};

export default function singleSpaAngular(userOpts: SingleSpaAngularOpts): LifeCycles {
  if(typeof userOpts ! = ="object") {
    throw Error("single-spa-angular requires a configuration object"); } const opts: SingleSpaAngularOpts = { ... defaultOpts, ... userOpts, };if(typeof opts.bootstrapFunction ! = ='function') {
    throw Error("single-spa-angular must be passed an opts.bootstrapFunction")}if(typeof opts.template ! = ="string") {
    throw Error("single-spa-angular must be passed opts.template string");
  }

  if(! opts.NgZone) { throw Error(`single-spa-angular must be passed the NgZone opt`); }return {
    bootstrap: bootstrap.bind(null, opts),
    mount: mount.bind(null, opts),
    unmount: unmount.bind(null, opts),
    update: opts.updateFunction
  };
}

function bootstrap(opts, props) {
  return Promise.resolve().then(() => {
    // In order for multiple Angular apps to work concurrently on a page, they each need a unique identifier.
    opts.zoneIdentifier = `single-spa-angular:${props.name || props.appName}`;

    // This is a hack, since NgZone doesn't allow you to configure the property that identifies your zone. // See https://github.com/PlaceMe-SAS/single-spa-angular-cli/issues/33, // https://github.com/single-spa/single-spa-angular/issues/47, // https://github.com/angular/angular/blob/a14dc2d7a4821a19f20a9547053a5734798f541e/packages/core/src/zone/ng_zone.ts#L144,  // and https://github.com/angular/angular/blob/a14dc2d7a4821a19f20a9547053a5734798f541e/packages/core/src/zone/ng_zone.ts#L257 opts.NgZone.isInAngularZone = function() { // @ts-ignore return window.Zone.current._properties[opts.zoneIdentifier] ===  true; } opts.routingEventListener = function() { opts.bootstrappedNgZone.run(() => { // See https://github.com/single-spa/single-spa-angular/issues/86 // Zone is unaware of the single-spa navigation change and so  Angular change detection doesn't work
        // unless we tell Zone that something happened
      })
    }
  });
}

function mount(opts, props) {
  return Promise
    .resolve()
    .then(() => {
      const domElementGetter = chooseDomElementGetter(opts, props);
      if(! domElementGetter) { throw Error(`cannot mount angular application'${props.name || props.appName}' without a domElementGetter provided either as an opt or a prop`);
      }

      const containerEl = getContainerEl(domElementGetter);
      containerEl.innerHTML = opts.template;
    })
    .then(() => {
      const bootstrapPromise = opts.bootstrapFunction(props)
      if(! (bootstrapPromise instanceof Promise)) { throw Error(`single-spa-angular: the opts.bootstrapFunction mustreturn a promise, but instead returned a '${typeof bootstrapPromise}' that is not a Promise`);
      }

      return bootstrapPromise.then(module => {
        if(! module || typeof module.destroy ! = ='function') { throw Error(`single-spa-angular: the opts.bootstrapFunction returned a promise that did not resolve with a valid Angular module. Did you call platformBrowser().bootstrapModuleFactory() correctly? `) } opts.bootstrappedNgZone = module.injector.get(opts.NgZone) opts.bootstrappedNgZone._inner._properties[opts.zoneIdentifier] =true;
        window.addEventListener('single-spa:routing-event', opts.routingEventListener)

        opts.bootstrappedModule = module;
        return module;
      });
    });
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
function unmount(opts, props) {
  return Promise.resolve().then(() => {
    if (opts.Router) {
      // Workaround for https://github.com/angular/angular/issues/19079
      const routerRef = opts.bootstrappedModule.injector.get(opts.Router);
      routerRef.dispose();
    }
    window.removeEventListener('single-spa:routing-event', opts.routingEventListener)
    opts.bootstrappedModule.destroy();
    if (opts.AnimationEngine) {
      const animationEngine = opts.bootstrappedModule.injector.get(opts.AnimationEngine);
      animationEngine._transitionEngine.flush();
    }
    delete opts.bootstrappedModule;
  });
}Copy the code

Bootstrap, mount, and unmout are implemented at the core. The Boostrap phase only flags multiple angular instances after the child application loading is complete and tells Zonejs that single-SPA triggers a child application switch and needs to start change detection. The mount phase calls angular’s platformBrowserDynamic().bootstrapModule(AppModule) method to manually launch the Angular app and save the launched Module instance. In the Unmout phase, the child application is destroyed by calling the destroy method of the launched Module instance, and some handling is done for special cases. The core point here is mount.

Conclusion:

In the last part of this article, we described the origin of micro-front-end and various integration methods of Web applications. By telling the case of DevUI’s Web integration mode, we deepened our understanding of this part of content. At the same time, we implemented a micro-front-end model using Single-SPA and analyzed the principle of single-SPA. In the next part, we will focus on the DevUI micro-front-end transformation process to have an in-depth discussion, telling how to develop an enterprise-level micro-front-end solution. The code https://github.com/myzhibie/microFE-single-spa.

Join us

We are DevUI team, welcome to come here and build elegant and efficient human-computer design/research and development system with us. Email: [email protected].

The text/DevUI myzhibie

Previous articles are recommended

Agile Design, Efficient Collaboration, Highlighting the Collaborative Value of the Design End cloud

Modularity In Modern Rich Text Editor Quill