This article has been in preparation for 11 months, because although we started using Angular’s micro front-end architecture at the beginning of the year, the product has not been officially released, and we have not been able to verify the feasibility through production practice. On November 16th, our product was officially released in gray scale. So it’s time to share what we learned about using the Angular micro front end. We hope to grow and advance with the Angular community. If you know about the micro front end and have already tried it on your project, you can skip the previous chapters.

What is a micro front end

The term “micro-front-end” has been frequently mentioned in the past two years. The concept was first proposed in the technical radar of ThoughtWork, which mainly introduced the concept of micro-service into the front-end, decoupled multiple modules or applications in the front-end, and enabled the front-end sub-modules to be stored, operated and deployed independently.

So what’s the difference between a microfront-end and a microservice?

The following figure is a schematic diagram of microservices. Microservices are mainly business modules divided according to certain rules, independently developed and deployed, and routed and forwarded through Nginx after deployment. The difficulty of microservices is how to call multiple modules, as well as authentication, logging, and even joining the gateway layer

For micro services, module decoupling is basically complete, but the micro front end is different, the front end application is a whole when running, need aggregation, even need interaction, communication.

Why you need a Micro front-end

  1. System modules increase, single applications become bloated, development efficiency is low, construction speed is slow;
  2. The expansion of staff requires multiple front-end teams to develop and deploy independently. If they are all developed in one warehouse, a series of problems will be brought.
  3. To solve legacy systems, new modules need to use the latest frameworks and technologies, while old systems continue to be used.

Comparison of several schemes of micro front end

way describe advantages disadvantages The difficulty coefficient
Routing forwarding Routing and forwarding does not belong to the micro front end strictly. Multiple sub-modules can share a navigation Simple and easy to implement Experience is not good, switch to apply the entire page refresh 🌟
Nested iframe Each child applies an iframe nested Apps are sandbox isolated Reloading scripts and styles 🌟 🌟
Build-time composition Independent storage, independent development, integrated packaging during construction, combined application Easy dependency management, extraction of common modules Cannot be independently deployed, technology stack, dependent versions must be unified 🌟 🌟
Runtime composition Each sub-application is built independently, and the main application is responsible for application management, loading, starting, uninstalling, and communication mechanism during runtime Good experience, truly independent development, independent deployment Complex, need to design loading, communication mechanism, can not achieve complete isolation, need to solve dependency conflicts, style conflicts 🌟 🌟 🌟
Web Components Each subapplication needs to be written using Web Components technology or generated using a framework For the future Immature, need to step on the pit 🌟 🌟 🌟

The above is just a brief comparison of several implementations. Of course, these solutions are not mutually exclusive. Choosing one solution depends on your business scenario.

  • Is it SPA monomer application?
  • Is the technology stack unified and need to support cross-framework calls?
  • Is there a need for complete isolation between applications?

We do enterprise SaaS platform, it must be SPA single application, technology stack is Angular, applications do not need to be completely isolated, but need to share common styles and components, to avoid repeated loading.

So the choice is: runtime composition.

Worktile’s path to microfront-end technology selection

There are not many microfront-end solutions on the market right now, and the one with the most attention and maturity is probably Single-SPA

There are a lot of teams in the country that have their own microfront-end frameworks, Such as the open source Single-Spa-based Qiankun – probably the most complete micro front end solution you’ve ever seen, phodal’s Mooa, and numerous internal solutions (ali Feibing also recently opened source icestark, a micro front end solution for large worktables, Only React and Vue are supported.

Single-spa and MOOa should be our primary consideration when selecting the technology. Single-spa should have the highest maturity, complete sample documentation, and Mooa’s master-subordinate micro-front-end framework for Angular is the best fit for our business and technology. After a period of research, we finally chose to develop a set of micro front-end libraries that fit our own requirements (we dare not call it a framework because it is relatively simple), mainly because our business has the following requirements that are not satisfied or difficult to meet in the above framework, and even need to be highly customized.

  • The product is master-slave, and Portal contains left navigation, message notification, and sub-application management
  • Multiple sub-applications need to communicate with each other. The main application or a sub-application needs to open the details page or redirect routes of other sub-applications
  • A page of child application A might load A component of child application B
  • Based on the above two features, A co-existence mode is required. Although application B is displayed, application A can be called normally. If it is destroyed, it cannot be called by other applications
  • Preloading needs to be provided
  • Styles for child applications also need to be loaded independently
  • The experience of routing must be the same as that of individual applications, whether in the primary application or sub-application

I have run the examples of single-SPA and MOOa, which are mainly some simple rendering displays. Once the above features are met, a lot of things need to be modified. The mooA implementation should be more comprehensive and suitable for us. The packaging has abandoned the Angular CLI, the code level references many things in single-SPA, and the API can be simplified again. Since it is customized for Angular, I think it should be implemented in Angular way. There is no denying that Phodal himself has a deep understanding of microfrontends, has written many good articles on microfrontends, and has even written the only book on microfrontends, Front-end Architectures – From Getting Started to MicroFrontends. When I realize the micro front end, I also refer to its many ideas and implementation methods.

Build a micro front-end application with Angular

Implementing a micro front end with Angular is actually more difficult than React and Vue because Angular includes AOT compilation, Module, zone. js, Service sharing, and so on. React and Vue directly apply JS to a region of the render page.

Choose to dynamically load modules after compiling or load the entire application

In a single Angular application, there must be a root AppModule, followed by FeatureModule for each FeatureModule. Each FeatureModule can have its own route. You can load feature modules lazy-by route, but in the micro-front-end architecture, each submodule is stored independently. How to load submodules into the root module at run time is a technical choice difficulty.

  1. The first option is to treat each submodule as a feature module and then package and compile it with the main application when it is packaged. This is easiest, but it cannot be deployed independently and is fully updated every time it is deployed
  2. The second option is to treat the submodule as a feature module, load the submodule in the main application with the SystemJsNgModuleLoader, and then compile and run the submodule.
  3. The third option is for each submodule to be a separate app, with its own AppModule and route, just like the main app. Choosing this option would require multiple app route synchronization issues, and Angular’s current dependency libraries are not directly available at runtime, requiring each subapp to compile. Unable to do common dependency library extraction (alternative possible)
  4. The fourth solution is to compile all the submodules into Web Components. I haven’t studied it in depth yet. There is no problem with using Components directly in this solution, but I don’t know how to deal with it after using Web Components.

We ended up choosing the third solution, which is the most complicated, because the new Ivy rendering engine will solve the problem of direct use by third party dependent library runtime when it is released. As for Web Components, we didn’t go into the details because the third solution works well so far.

Apply register, load, and destroy mechanisms

This is the foundation and core of all micro-front-end applications, but I think it is the simplest and easiest to implement. The main things to do are:

  • Provides dynamic loading of static resources
  • Configure rules for sub-applications, including application name, route prefix, and static resource file

    this.planet.registerApps([
      {
          name: 'app1',
          hostParent: '#app-host-container',
          routerPathPrefix: '/app1',
          selector: 'app1-root',
          scripts: ['/static/app1/main.js'],
          styles: ['/static/app1/styles.css']
      },
      // ...
    ]);
    Copy the code
  • Application loading: Find the corresponding sub-application according to the URL of the current page, load the static resources of the application, and call the pre-defined startup function to directly start the application. In Angular you start the root module platformBrowserDynamic().bootstrapModule(AppModule).

  • Application preloading: After the current application is rendered, other applications will be preloaded and started without being displayed
  • Destroy application useappModuleRef.destroy();

The above steps are enough for simple scenarios, but if you want apps to coexist, bootStrapped will be hidden, not destroyed, and only apps that are Active will be displayed on the current page.

routing

Because each child app is a separate Angular app and can coexist with multiple child apps, it becomes difficult to synchronize multiple apps’ routes, jump, and support scenarios such as route jump between apps, communication between apps, and component rendering. I think routing is one of the most complex issues we encounter in using a microfront-end architecture.

The current approach is to configure all child application routes in the main application route. The component is set to EmptyComponent, so that when switching to the child application route, the main application will match the empty route state without error. Each child application needs to add a generic empty route EmptyComponent

{
        path: '* *',
        component: EmptyComponent
}
Copy the code

In addition, you need to update the routes of other applications during route switchover. Otherwise, the current route status of each application is inconsistent, and the switchover fails.

  • During route switchover of the primary application, the command is used to find all the currently started sub-applicationsrouter.navigateByUrlSynchronous jump
  • During the subapplication route switchover, the primary application route and other subroutes in the enabled state are synchronized

The Angular example of single-SPA basically switches and destroys Angular applications because there is no co-location. So you don’t have to deal with multiple application routes, but as a framework independent micro-front-end solution, you can only do so much.

When the Ivy rendering engine is released, it will be possible to compile sub-applications into directly runnable modules. The entire application will be much simpler if there is only one route.

Sharing global Services

For some global data, we usually store it in the service, and then sub-applications can share it directly, for example: Simple data sharing, such as current login user and multi-language service, can be directly mounted on the Window. In order to make each sub-application use the same global service as the service within the module, we instantiate these services in the main application. However, provide is then used in the AppModule of each child application to reset the value of the main application. Of course, these Settings do not need to be set by the business developer of the child application, but are encapsulated in the business component library and configured globally.

{
  provide: AppContext,
  useValue: window.portalAppContext
}
Copy the code

Interapplication communication

There are many ways to communicate between applications. We use the CustomEvent of the browser at the bottom and encapsulate the GlobalEventDispatcher service on top of this (of course you can also use the global object mounted on the Window object). A scenario is when one child application opens the details page of another child application

// App1
globalEventDispatcher.dispatch('open-task-detail', { taskId: 'xxx' });

// App2
globalEventDispatcher.register('open-task-detail').subscribe((payload) => {
    // open dialog of task detail
});
Copy the code

Components call each other between applications

In our Agile development sub-product, a user story detail page needs to show the associated test cases and test execution of the test management application, so this test case list component is most appropriate in the test management sub-application, then the user story detail page must be in the Agile development application. How to load a component of a test management application is a problem.

This section uses DomPortalOutlet in the Angular CDK to dynamically create the component and specify that the rendering is in a container, ensuring that the dynamic component is created in the test management module and rendered in another application.

const portalOutlet = new DomPortalOutlet(container, componentFactoryResolver, appRef, injector);
const testCasesPortalComponent = new ComponentPortal(TestCasesComponent, null);
portalOutlet.attachComponentPortal(testCasesPortalComponent);
Copy the code

engineering

Using a micro front end to develop applications solves not only Angular’s technical problems, but also the engineering problems of development, collaboration, and deployment, such as:

  • Common dependency library extraction
  • How do I start development locally
  • How to package and deploy, and how to notify the main application of the generated hash resource file

The application common dependency library extraction avoids repackaging libraries and reduces packaging volume. This requires a custom Webpack Config implementation. At first, we completely customized Webpack to package Angular applications, which would lose many convenient features provided by CLI. I stumbled across a library called Angular-Builders that incorporates custom Webpack Config in the Angular CLI-generated Webpack Config with only a few custom configurations. The rest use the CLI’s packaging capabilities entirely, almost writing a similar tool yourself. Put the required common dependencies into scripts in the main application, and then configure externals in the child application, such as a library like Moment Lodash RXJS.

const webpackExtraConfig = {
    optimization: {
        runtimeChunk: false// The child application must be setfalse}, externals: {moment:'moment',
        lodash: '_',
        rxjs: 'rxjs'.'rxjs/operators': 'rxjs.operators',
        highcharts: 'Highcharts'
    },
    devtool: options.isDev ? 'eval-source-map' : ' ',
    plugins: [new WebpackAssetsManifest()]
};
return webpackExtraConfig;
Copy the code

The main function of WebpackAssetsManifest is to generate the manifest.json file. The purpose is to make the corresponding relationship between the generated Hash file and enable the main application to load the correct resource file.

Local development configures proxy.conf.js proxies to access resource files for each child application, including API calls.

Ngx-planet, a micro-front-end library based on Angular

The above are some of the technical difficulties we encountered in using Angular to build micro-front-end applications and our solutions. After research, we finally chose to develop a set of micro-front-end libraries tailored to our business scenarios and Angular.

Github storage: ngx-planet online Demo:planet.ngnice.com

I can’t say “you’ve seen the most perfect micro-front-end solution”, but at least the Angular community has seen a solution that can be used in production environment. The API is in line with Angular Style, and many major domestic manufacturers do micro-front-end solutions that basically ignore the existence of Angular framework. Worktile’s four development sub-products are built entirely on ngX-Planet and are almost fully operational after nearly a year of tramping and practice.

We hope the Angular community can come up with more micro-front end solutions and make progress together. We certainly have a lot of problems with our solutions, and we welcome suggestions and jokes. We will continue to work on the Angular micro-front end as well. Try NGX-Planet.

Optimizations and improvements under the Ivy rendering engine will be investigated in the future.


Worktile’s website: Worktile.com

Author: Haifeng Xu, Senior engineer at Worktile

This article was first published on Worktile’s official blog.