If you still don’t know about the “micro front end” in 2021, please sit down and listen to the lecture. Xiaobian specially invited the front end leader of our Ligaai team to give you a fun tour of the micro front end.

What is a micro front end?

Techniques, Strategies and recipes for building a modern web app with multiple teams that can ship features independently. — Micro Frontends

Micro-front-end is a kind of technical means and method strategy for multiple teams to jointly build modern Web applications by publishing functions independently.

Rather than simply being a front-end framework/tool, a micro-front-end is an architectural architecture, a concept first proposed by ThoughtWorks in late 2016. Micro-front-end is an architecture similar to micro-service, which applies the concept of micro-service to the browser side and transforms the Web application from a whole “single application” to a “aggregation” of several small front-end applications.

Each front-end application is “atomized” and can be run, developed and deployed independently to meet the needs of rapid business changes and distributed, multi-team parallel development.

Core values (why use a micro front end?)

– There are no technical stacks

The master application does not restrict access to the technology stack of child applications, and the child applications have full autonomy. The connected sub-applications are also independent of each other, without any direct or indirect technology stack, dependencies, or implementation coupling.

– Can be independently developed and deployed

The micro application warehouse is independent, the front and back ends can be independently developed, and the main framework automatically completes synchronous update after deployment. The ability to deploy independently is critical in a micro-front-end architecture to reduce the scope of changes in a single development and thus the associated risks. Each micro front end should have its own continuous delivery pipeline that builds, tests, and deploys the micro front end into production.

– Incremental upgrade

In the face of complex scenarios, it is often difficult to do a full stack upgrade or refactoring of an existing system. As a result, Micro Front-End is a great tool and strategy for implementing incremental refactoring that can gradually upgrade our architecture, dependencies, and user experience. When major changes occur to the main framework, each module of the micro front end can be upgraded independently and on demand, without the need to go offline as a whole or upgrade everything at once. If we want to try new technologies or modes of interaction, we can also do it in a more isolated environment.

– Simple, decoupled and easy to maintain

Codebases in micro-front-end architectures tend to be smaller/simpler and easier to develop, avoiding unnecessary coupling between unrelated components and making code cleaner. By defining clear application boundaries, the possibility of accidental coupling can be reduced to better avoid such unintentional coupling problems.

Under what circumstances?

The micro front-end architecture is designed to solve the problem of unmaintainable applications that evolve from a common application to a Frontend Monolith application due to the increase of participants and changes in teams over a relatively long time span. This type of problem is particularly common in enterprise Web applications.

– Compatible with legacy systems

Today’s technology is constantly changing, and teams need to develop new features using a new framework that is compatible with the existing system if they want to keep the technology stack intact. However, the legacy system is already functional and stable, and the team has no need or energy to rebuild the legacy system again. At this point, if the team needs to develop new applications using new frameworks and new technologies, using a micro front end is a good solution.

– Applied Polymerization

Large Internet companies, or commercial SaaS platforms, provide users/customers with many applications and services. How to present a unified user experience and one-stop application aggregation for users has become a problem that must be solved. Front-end aggregation has become a technology trend, and the ideal solution at present is micro-front-end.

– Different teams develop the same application using different technology stacks

Teams need to integrate third-party SaaS applications or third-party private server applications (such as GitLab deployed internally) and, where there are already multiple applications, need to aggregate them into a single application.

Photo source: https://micro-frontends.org/

What is Qiankun?

Qiankun is a single-spa-based microfront-end implementation library designed to make it easier and painless to build a production-usable microfront-end architecture system.

Qiankun was incubated from Ant Fintech’s unified access platform for cloud products based on micro-front-end architecture. After being fully tested and polished by a number of online applications, the team extracted its micro front-end kernel and opened it up. The team hopes to help other products with similar needs to build their own micro front-end system more easily, and also hopes to polish Qiankun into a more mature and complete system with the help of the community.

Qiankun has served more than 200+ online applications in Ant, and is absolutely reliable in terms of ease of use and completeness.

📦 is a more out-of-the-box API based on the Single-SPA package. 📱 There are no tech stacks. Any tech stack application can use/access, whether it’s React/Vue/Angular/ jQuery or some other framework. 💪 HTML Entry allows you to access microapps as easily as using an iframe. 🛡 style isolation, to ensure that micro applications do not interfere with each other style. 🧳 JS sandbox to ensure that global variables/events between micro applications do not conflict. âš¡ resource preload, preload the unopened micro application resources in the idle time of the browser, and accelerate the opening speed of micro application. The 🔌 umi plugin provides @umijs/plugin-qiankun for umi to switch to a micro front-end architecture system with one click.

Problems encountered and solutions

Child application static resource 404

1. All images and other static resources are uploaded to CDN, and CDN address is directly referenced in CSS (recommended) 2. Pack font files and images into Base64 (suitable for projects with small font files and images) (but there are always some inappropriate resources, please use the third one)

/ / webpack config loader, the following rule is added to the rules of {test: / \. (PNG |, jpe." g|gif|webp|woff2? |eot|ttf|otf)$/i, use: [{ loader: 'url-loader', options: {}, }] } // chainWebpack config.module.rule('fonts').use('url-loader').loader('url-loader').options({}).end(); config.module.rule('images').use('url-loader').loader('url-loader').options({}).end();

3. Inject the full path when packaging (for large font files and images)

const elementFromPoint = document.elementFromPoint; document.elementFromPoint = function (x, y) { const result = Reflect.apply(elementFromPoint, this, [x, y]); / / if the coordinates of elements for the shadow is the shadow again to obtain the if (result && result. ShadowRoot) {return result. ShadowRoot. ElementFromPoint (x, y); } return result; };

CSS Style Isolation

By default, Qiankun automatically turns on sandbox mode, but this mode does not separate the main application from the children, nor does it accommodate multiple children loaded at the same time. Qiankun also gives the shadow DOM solution, which requires configuring the sandbox: {StrictStyleIsolation: true}

Strictly style isolation based on ShadowDOM is not a solution that can be used mindlessly. In most cases, it needs to be connected to the application to do some adaption before it can run normally in ShadowDOM. In the React scenario, for example, these issues need to be addressed, and the user needs to be clear about what it means to have StrictStyleIsolation turned on. Here are some examples of my Shadowdom solution.

fix shadow dom

getComputedStyle

When retrievingthe shadow DOM’s computational style, the element passed in is a DocumentFragment, which will cause an error.

const getComputedStyle = window.getComputedStyle; window.getComputedStyle = (el, ... If (el instanceof DocumentFragment) {return {}; } return Reflect.apply(getComputedStyle, window, [el, ...args]); };

elementFromPoint

Based on the coordinates (x, y), when fetching an element from a child application, the shadow root is returned, not the actual element.

const elementFromPoint = document.elementFromPoint; document.elementFromPoint = function (x, y) { const result = Reflect.apply(elementFromPoint, this, [x, y]); / / if the coordinates of elements for the shadow is the shadow again to obtain the if (result && result. ShadowRoot) {return result. ShadowRoot. ElementFromPoint (x, y); } return result; };

The document event target is shadow

When we add events like click, mousedown, mouseup, and so on to the document, the event.target in the callback function is not the actual target element, but the shadow root element.

// fix: click event target is shadow element const {addEventListener: oldaddEventListener, removeEventListener: oldRemoveEventListener} = document; const fixEvents = ['click', 'mousedown', 'mouseup']; const overrideEventFnMap = {}; const setOverrideEvent = (eventName, fn, overrideFn) => { if (fn === overrideFn) { return; } if (! overrideEventFnMap[eventName]) { overrideEventFnMap[eventName] = new Map(); } overrideEventFnMap[eventName].set(fn, overrideFn); }; const resetOverrideEvent = (eventName, fn) => { const eventFn = overrideEventFnMap[eventName]? .get(fn); if (eventFn) { overrideEventFnMap[eventName].delete(fn); } return eventFn || fn; }; Document. The addEventListener = (event, fn, options) = > {const callback = (e) = > {/ / the current event object for qiankun box, and the current object has a shadowRoot elements, The fix event object is the real element if (e.target.id? .startsWith('__qiankun_microapp_wrapper') && e.target? .shadowRoot) { fn({... e, target: e.path[0]}); return; } fn(e); }; const eventFn = fixEvents.includes(event) ? callback : fn; setOverrideEvent(event, fn, eventFn); Reflect.apply(oldAddEventListener, document, [event, eventFn, options]); }; document.removeEventListener = (event, fn, options) => { const eventFn = resetOverrideEvent(event, fn); Reflect.apply(oldRemoveEventListener, document, [event, eventFn, options]); };

Js sandbox

The main thing is to isolate the variables mounted on the window, which Qiankun handles internally for you. The window accessed during the child application runtime is actually a Proxy object. All child applications’ global variable changes are made in the closure and are not actually written back to the window, thus avoiding contamination between multiple instances.

Source: front-end preferred

Reusing Common Dependencies

For example: Util, Core, Request, UI and other common dependencies in the enterprise. In the micro front end, we do not need to load every child application once, which will not only waste resources but also lead to the singleton object to become multiple instances. Configure the externals in Webpack. If you want to reuse the exclude.html, then load the exclude.html lib link. (If a child application uses the ignore attribute on the script or style tag, Qiankun will not load the js/ CSS file again. These JS/CSS can still be loaded.

<link ignore rel="stylesheet" href="//element-ui.css">
<script ignore src="//element-ui.js"></script>
externals: { 'element-ui': { commonjs: 'element-ui', commonjs2: 'element-ui', amd: 'element-ui', root: 'elementUI' // The external link CDN loads the variable name mounted on the window}}

Sharing of father and son (as exemplified by internationalization)

Dependencies are passed to subprojects when the application is registered or loaded

/ / registered registerMicroApps ([{name: 'micro - 1, entry:' http://localhost:9001/micro-1 ', the container: '#' micro - 1, activeRule: '/micro-1', props: { i18n: this.$i18n } }, ]); // manually load loadMicroApp({name, entry, container: '#${this.boxid}', props: {i18n: this.$i18n}});

Get props parameters initialized when the child application starts

let { i18n } = props; if (! I18n) {// Load dynamically local internationalized const module = await import('@/common-module/lang'); i18n = module.default; } new Vue({ i18n, router, render });

The child is obtained in the bootstrap or mount hook functions. If the variable is not obtained from the props, the child loads the local variable dynamically.

Keep alive – (Vue)

It’s not recommended to keepAlive, but I do it anyway, what can I say…

There are other solutions on the web that I have not adopted, but I will present my solution (a combination of the solutions on the web), which uses LoadMicroApp to manually load and unload child applications. There are a few difficulties here:

// microapp.js (CI/CD or interface from the server) const apps = [{name: 'micro-1', activeRule: '/micro-1'}, {name: 'micro-2', activeRule: '/micro-2', prefetch: true }, { name: 'micro-3', activeRule: '/micro-3', prefetch: False, // Preload resource preload: false, // Prerender keepAlive: true // Caches child application}]; export default apps.map(app => ({ ... app, entry: getEntryUrl(app.name) }));
<template> <div v-show="isActive" :id="boxId" :class="b()" /> </template> <script> import { loadMicroApp } from 'qiankun'; export default { name: 'MicroApp', props: { app: { type: Object, required: true } }, inject: ['appLayout'], computed: { boxId() { return `micro-app_${this.app.name}`; }, activeRule() { return this.app.activeRule; }, currentPath() { return this.$route.fullPath; IsActive () {const {activeRule, currentPath} = this; const rules = Array.isArray(activeRule) ? [ ...activeRule ] : [activeRule]; return rules.some(rule => { if (typeof rule === 'function') { return rule(currentPath); } return currentPath.startsWith(`${rule}`); }); }, isKeepalive() { return this.app.keepalive; } }, watch: { isActive: { handler() { this.onActiveChange(); $this.$once('started', () => {this.init();}, created () {this.init(); }); // Add the current instance to the layout this.applayout.apps.set (this.app.name, this); }, methods: {init() {// Preload if (this.app.preload) {this.load(); } // mount this.onActiveChange() here if the route goes directly to the current application; }, /** * Returns * @Returns {Promise<void>} */ async Load () {if (! this.appInstance) { const { name, entry, preload } = this.app; this.appInstance = loadMicroApp({ name, entry, container: `#${this.boxId}`, props: { ... , appName: name, preload, active: this.isActive } }); await this.appInstance.mountPromise; }}, /** * Returns {Promise<void>} */ async onActiveChange() {$emit(' ${this.isActive? ');}, /** * Returns {Promise<void>} */ async onActiveChange(); 'activated' : 'deactivated'}:${this.app.name}`); // Load if (this.isActive) {await this.load(); await this.load(); } // If the current application is invalid and the current application is loaded and configured to not cache, unload the current application if (! this.isActive && this.appInstance && ! this.isKeepalive) { await this.appInstance.unmount(); this.appInstance = null; } // Notifies the current state of the layout change this.$emit('active', this.isActive); }}}; </script>
// App.vue (layout) <template> <template v-if="! isMicroApp"> <keep-alive> <router-view v-if="keepAlive" /> </keep-alive> <router-view v-if="! keepAlive" /> </template> <micro-app v-for="app of microApps" :key="app.name" :app="app" @active="onMicroActive" /> </template> <script> computed: { isMicroApp() { return !! this.currentMicroApp; }}, mounted () {// Starting ({singularity: false, sandbox: {StrictStyleIsolation:) {// Starting ({singularity: false, sandbox: {StrictStyleIsolation:) {// Starting ({singularity: false, sandbox: {StrictStyleIsolation:); true } }); Const prefetchChapplist = this. microapps. filter(item => item.prefetch); If (prefetchChapplist.length) {// delay execution, Resource loading place affect current access (window. RequestIdleCallback | | setTimeout) (() = > prefetchApps (prefetchAppList)); $emit(' Started '); $emit(' Started '); $emit(' Started '); }, methods: { onMicroActive() { this.currentMicroApp = this.appValues.find(item => item.isActive); } } </script>

If we do not unload the child of KeepAlive, the child will still respond to the routing change, so that the child’s current route is no longer the one it left on.

/** * Let Vue-Router support KeepAlive. When the main route is changed, if the child application does not have this route, it will not handle it. * Because moving forward or backward through the browser will trigger the main route's listening first, and the child application route will not be notified to the child application DEActivated in time, the child application route will not stop listening in time. Will deal with this by the changes of the main * @ param router * / const supportKeepAlive = (router) = > {const old = router. History. TransitionTo; router.history.transitionTo = (location, cb) => { const matched = router.getMatchedComponents(location); if (! matched || ! matched.length) { return; } Reflect.apply(old, router.history, [location, cb]); }; }; // Overwrite listener routing change event supportKeepAlive(instance.$router); // If it is pre-mounted and is not currently active, stop listening on the route and set _startLocation to null so that it can respond to if (preload &&! $router.history.tearDown (); // If the child application is not preloaded, stop the routing if it is not connected to the child. instance.$router.history._startLocation = ''; }

The page is activated and deactivated.

If (eventBus) {eventBus.$on(' Activated :${appName} ', Activated); eventBus.$on(`deactivated:${appName}`, deactivated); } /** * Returns Returns {*} */ const getCurrenTrouteInstance = () => {const {matched} = instance? .$route || {}; if (matched? .length) { const { instances } = matched[matched.length - 1]; if (instances) { return instances.default || instances; }}}; / * * * trigger current routing component hook * @ param hook * / const fireCurrentRouterInstanceHook = (hook) = > {const com = getCurrentRouteInstance(); const fns = com? .$options? .[hook]; if (fns) { fns.forEach(fn => Reflect.apply(fn, com, [{ micro: true }])); }}; /** * const Activated = () => {instance? .$router.history.setupListeners(); console.log('setupListeners'); fireCurrentRouterInstanceHook('activated'); }; /** * is called when a keep-alive cached component is deactivated. */ const deactivated = () => { instance? .$router.history.teardown(); console.log('teardown'); fireCurrentRouterInstanceHook('deactivated'); };

Vuex global state sharing

(carefully! The child application uses its own Vuex and is not really using the Vuex of the main application. The main application of the Vuex module that needs to be shared is the same file as the child application in theory. We mark in this Vuex module whether it needs to be shared, and watch the main application and the child application of this module.

When the state of the child application changes, the state of the main application is updated, and the state of the child application is also modified when the state of the child application changes.

/ * * * for namespace state data * @ param state state data * @ param * @ returns the namespace namespace {*} * / const getNamespaceState = (state, namespace) => namespace === 'root' ? state : get(state, namespace); /** * UpdateStoreState = @Returns {*} * const UpdateStoreState = @Returns {*} * const UpdateStoreState = const UpdateStoreState (store, namespace, value) => store._withCommit(() => setVo(getNamespaceState(store.state, namespace), value)); / * * * * @ param listening state storage store state storage * @ param fn change event functions * @ param * @ returns the namespace namespace @ private} {* * * / const _watch = (store, fn, namespace) => store.watch(state => getNamespaceState(state, namespace), fn, { deep: true }); const updateSubStoreState = (stores, ns, value) => stores.filter(s => s.__shareNamespaces.has(ns)).forEach(s => updateStoreState(s, ns, value)); export default (store, MainStore) => {if (mainStore) {if (mainStore) {if (mainStore) {if (mainStore) {if (mainstore.__isshare! == true) {// All child application state mainStore.__subStores = new Set(); // Listed namespace mainStore.__subwatchs = new Map(); mainStore.__isShare = true; } // Put the current child store in the main app mainStore.__subStore.add (store); const shareNames = new Set(); const { _modulesNamespaceMap: moduleMap } = store; // Listen to the current store, update the main app store, ForEach (key => {const names = key.split('/'). Filter (k =>!! k); Const has = names.some(name => shareNames. Has (name)); if (has) { return; } const { _rawModule: { share } } = moduleMap[key]; if (share === true) { const namespace = names.join('.'); // Listens the namespace of the current child application store, _watch(store, value => updateStoreState(mainStore, namespace, value), namespace); shareNames.add(namespace); }}); // Store. __Sharenamespaces = Sharenames; // Store. __Sharenamespaces = Sharenames; ForEach (ns => {// UpdateStoreState (store, ns, getNamespaceState(mainstore. state, ns)); if (mainStore.__subWatchs.has(ns)) { return; } // Listen to the state of the main application, Watch (state => getNamespaceState(state, ns), value => updateSubStoreState([...mainStore.__subStores], ns, value), { deep: true }); Console. log(' Main App Store listens for [${ns}] data '); mainStore.__subWatchs.set(ns, w); }); } return store; };

See here, you must also be amazed at the subtle micro front end! If you have any problems, please pay attention to us at LigaAI@sf to communicate and make progress together. For more details, please click on our official website LIGAAI – a new generation of intelligent research and development management platform

Micro Frontends, Micro Frontends from martinfowler.com, the core value of Micro Frontends, and an introduction to Qiankun

Alone Zhou

Link to this article: https://blog.cn-face.com/2021/06/17/ Qiankun taste fresh /

Copyright Notice: All posts on this blog are licensed under the BY-NC-SA license unless otherwise stated. Reprint please indicate the source!