If you still don’t know about “micro front end” in 2021, please feel free to sit in front of the class. Xiaobian specially invited the head of the front end of our LigaAI team to take you to play micro front end easily.

What is a micro front end?

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

The micro front end is a technique and strategy for multiple teams to jointly build modern Web applications by independently publishing their capabilities.

Rather than being a simple front-end framework/tool, a micro front-end is a set of architectural architecture that was first introduced at ThoughtWorks in late 2016. A microfront-end is a microservice-like architecture that applies the concept of microservices to the browser, transforming Web applications from an entire “single application” to an “aggregate” of several small front-end applications.

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

Core values (why use a micro front end?)

– No technology stack

The master application does not restrict access to the sub application technology stack, the sub application has complete 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 rear end can be developed independently, and the main framework will be updated automatically after the deployment is completed. The ability to deploy independently is critical in a micro front end system, narrowing the scope of change in a single development and thereby reducing the associated risks. Each micro front end should have its own continuous delivery pipeline that can be built, tested, and deployed into the production environment.

– Incremental upgrade

In the face of a variety of complex scenarios, it is often difficult to do a full technical stack upgrade or refactoring of an existing system. As such, the micro front end is a great means and strategy for implementing incremental refactoring to gradually upgrade our architecture, dependencies, and user experience. When major changes are made to the main framework, each module of the micro front can be independently upgraded on demand, without the need to go offline or upgrade everything at once. If we want to try out new technologies or modes of interaction, we can do it in a more isolated environment.

– Simple, decoupled, and easy to maintain

Code bases under microfront-end architectures tend to be smaller/simpler, easier to develop, avoid unnecessary coupling between unrelated components, and make the code cleaner. This kind of unintended coupling problem can be better avoided by defining clear application boundaries to reduce the possibility of unintended coupling.

In what situations?

The micro Frontend architecture is designed to solve the problem of a single application not being maintainable over a relatively long period of time due to the number of people and teams involved in the evolution of the application from a common application to a Frontend Monolith application. This type of problem is particularly common in enterprise-level Web applications.

– Compatible with legacy systems

As technology changes, teams need to use new frameworks to develop new features that are compatible with existing systems if they want to keep up with the technology stack. The legacy system is already functional and stable, and the team has no need or energy to refactor the legacy system again. When teams need to develop new applications using new frameworks and technologies, using a micro front end is a good solution.

– Application polymerization

Large Internet companies, or commercial Saas platforms, offer many applications and services to users/customers. How to present users with a unified user experience and one-stop application aggregation becomes a problem that must be solved. Front-end aggregation has become a technology trend, and the ideal solution is the 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, which is deployed within the company, etc.) and, where multiple applications already exist, they need to aggregate them into a single application.

Photo source: micro-frontends.org/

What is Qiankun?

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

Qiankun is a unified access platform for cloud products based on micro-front-end architecture incubated by Ant Fintech. After fully testing and polishing a number of online applications, the team extracted the micro front-end kernel and open source, hoping to help similar products with similar needs to build their own micro front-end system more easily, and also hope to polish qiankun more mature with the help of the community.

Currently, Qiankun has served over 200+ online applications inside ant, and it is absolutely reliable in terms of ease of use and completeness.

📦 is based on a single-SPA package that provides a more out-of-the-box API.

📱 is available on any technology stack, whether it is React/Vue/Angular/JQuery or other frameworks.

💪 HTML Entry access allows you to access microapps as easily as using an iframe.

🛡 Style isolation ensures that styles between microapplications do not interfere with each other.

🧳 JS sandbox to ensure that global variables/events between microapplications do not conflict.

⚡️ Resource preloading: The browser preloads unused micro-application resources in idle time to speed up the opening of micro-applications.

🔌 Umi plugin provides @umijs/ plugin-Qiankun for UMI applications to switch to a micro front-end architecture system with one click.

Encountered problems and solutions

Subapplication static resource 404

1. Upload all static resources such as pictures to the CDN. The CSS directly references the CDN address (recommended) 2. Package font files and images as Base64 (suitable for projects with small font files and images) (there are always some non-conforming resources, 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();Copy the code

3. Inject the full path to it when packaging (applicable to projects with 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; };Copy the code

CSS Style Isolation

By default, The sandbox mode is automatically enabled in Qiankun. However, the sandbox mode cannot be used to isolate the main application and sub-applications, and cannot be used when multiple sub-applications are loaded at the same time. The sandbox should be configured: {strictStyleIsolation: true}

Strict style isolation based on ShadowDOM is not a brainless scheme. In most cases, it needs to be connected to the application to do some adaptation before it can run normally in ShadowDOM. For example, these problems need to be solved in react scenarios, and users need to know what strictStyleIsolation means when it is enabled. Here are some examples of how I solved ShadowDom.

fix shadow dom

getComputedStyle

Error when getting the shadow DOM compute style and passing in element DocumentFragment

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

elementFromPoint

When retrieving a child element based on coordinates (x, y), 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; };Copy the code

Document event target is shadow

When we add click, mouseDown, mouseup, and other events to the Document, the event.target in the callback function is not the actual target element, but the shadow root 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, 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]); };Copy the code

Js sandbox

The main thing is to isolate variables mounted on Windows, but Qiankun has handled this internally for you. The window accessed while the child application is running is actually a Proxy object. All child application global variable changes are generated in the closure and are not actually written back to the window, thus avoiding contamination across multiple instances.

Figure source: front end preference

Reuse common dependencies

For example, common dependencies such as util, core, Request, UI and so on in the enterprise. In the micro front end, we don’t need to load every child application once, which wastes resources and can cause singleton objects to become multiple cases. Configure externals in Webpack. If you want to reuse any of the excludings, then load the excludings in the index.html. (If you want to add a script or style tag with the ignore attribute, the sub-project will not load the JS/CSS. These JS/CSS can still be loaded)

<link ignore rel="stylesheet" href="//element-ui.css">
<script ignore src="//element-ui.js"></script>
Copy the code
externals: { 'element-ui': { commonjs: 'element-ui', commonjs2: 'element-ui', amd: 'element-ui', root: 'ElementUI' // link CDN to load variable name mounted to window}}Copy the code

Father-son sharing (take internationalization as an example)

Dependencies are passed to subprojects at application registration or load time

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

The props parameter is obtained when the child application is started

let { i18n } = props; if (! I18n) {// Dynamically load locally internationalization const Module = await import('@/common-module/lang') when running independently or when the main application is not shared; i18n = module.default; } new Vue({ i18n, router, render });Copy the code

The main application passes the shared variable to the child application using props when registering the child application or manually loading the child application. The child application obtains the variable from the bootstrap or mount hook function. If the variable is not obtained from the props function, the child application dynamically loads the local variable.

Keep alive – (Vue)

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

There are other solutions on the Internet, but I didn’t take them. I’ll give you my solution here (based on the solutions on the Internet), using loadMicroApp to manually load and unload the child applications. Here are some difficult points:

// microapp.js const apps = [{name: 'micro-1', activeRule: '/micro-1'}, {name: '/micro-1'} 'micro-2', activeRule: '/micro-2', prefetch: true }, { name: 'micro-3', activeRule: '/micro-3', prefetch: Preload: false, // prerender keepalive: true // Cache subapplication}]; export default apps.map(app => ({ ... app, entry: getEntryUrl(app.name) }));Copy the code
<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(); }}}, created () {this.$once('started', () => {this.init(); }); // Add the current instance to the layout this.applayout.apps.set (this.app.name, this); }, methods: {init() {// premount if (this.app.preload) {this.load(); } // This.onActivechange () is mounted here if the route goes directly to the current application; }, /** * load microapplication * @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; }}, /** * state change * @returns {Promise<void>} */ async onActiveChange() {$emit(' ${this.isactive? 'activated' : 'deactivated'}:${this.app.name}`); If (this.isactive) {await this.load(); } // Uninstall the current application if it is invalid and the current application is loaded and configured not to cache. this.isActive && this.appInstance && ! this.isKeepalive) { await this.appInstance.unmount(); this.appInstance = null; $emit('active', this.isactive); $emit('active', this.isactive); }}}; </script>Copy the code
// 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 () start({singular: false, sandbox: {strictStyleIsolation: true } }); Const prefetchAppList = this.microapps.filter (item => item.prefetch); If (prefetchApplist.length) {// Delay execution, Resource loading place affect current access (window. RequestIdleCallback | | setTimeout) (() = > prefetchApps (prefetchAppList)); } $emit('started');} $emit('started');} $emit('started');} $emit('started'); }, methods: { onMicroActive() { this.currentMicroApp = this.appValues.find(item => item.isActive); } } </script>Copy the code

If we do not uninstall the keepAlive child, the child will still respond to the change of the route, so that the current route of the child is not the one it left.

/** * Enable the uE-router to support keepalive. If the primary route is changed, do not handle the change if the current child does not have the primary route. * If the browser moves forward or back, the primary route will be monitored first. 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]); }; }; SupportKeepAlive (instance.$router); // Stop listening for routes if they are premounted and not currently active, and set _startLocation to null so that they can respond to if (preload &&! $route.history.tearDown (); $route.history.tearDown (); $route.history.tearDown (); instance.$router.history._startLocation = ''; }Copy the code

Activated and deactivated are triggered on the page.

$on(' activated:${appName} ', activated); eventBus.$on(`deactivated:${appName}`, deactivated); } /** * Obtain the current route component * @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 }])); }}; /** * Activate the current subapp callback */ const activated = () => {instance? .$router.history.setupListeners(); console.log('setupListeners'); fireCurrentRouterInstanceHook('activated'); }; /** * called when a component cached by keep-alive is disabled. */ const deactivated = () => { instance? .$router.history.teardown(); console.log('teardown'); fireCurrentRouterInstanceHook('deactivated'); };Copy the code

Vuex Global status sharing

(carefully! Destroys the idea of VUEX and does not apply to large amounts of data)

The sub-app uses its own vuex, not the actual vuex of the main app. According to the theory of main application and sub-application of the VUEX module that needs to be shared, the same file is referenced. We mark whether it needs to be shared in this VUEX module and watch the module of the main application and sub-application.

When the state in the child application changes, the primary application’s state is updated. When the primary application’s state changes, the child application’s state is also changed.

@param namespace namespace * @returns {*} */ const getNamespaceState = (state, namespace) => namespace === 'root' ? state : get(state, namespace); /** * Update status data * @param Store Status storage * @param Namespace namespace * @param value New value * @returns {*} */ const updateStoreState = (store, namespace, value) => store._withCommit(() => setVo(getNamespaceState(store.state, namespace), value)); /** * Listen status store * @param store Status store * @param FN Change event function * @param Namespace Namespace * @returns {*} * @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) => {// Enable sharing if the primary application is available. If (mainStore) {// Check whether the primary application is shared if multiple sub-applications are shared with the primary application. If (mainStore.__isShare! == true) {mainStore.__substores = new Set(); Mainstore.__subwatchs = new Map(); mainStore.__subwatchs = new Map(); mainStore.__isShare = true; } mainstore.__substores. Add (store); const shareNames = new Set(); const { _modulesNamespaceMap: moduleMap } = store; // Listen to the current store, update the main app store, Object.keys(moduleMap).foreach (key => {const names = key.split('/').filter(k =>!! k); Const has = names.some(name => shareNames. Has (name)); const has = names.some(name => shareNames. if (has) { return; } const { _rawModule: { share } } = moduleMap[key]; if (share === true) { const namespace = names.join('.'); // Listen to the current subapplication store namespace, _watch(store, value => updateStoreState(mainStore, namespace, value), namespace); shareNames.add(namespace); }}); Store. __shareNamespaces = shareNames; ShareNames. ForEach (ns => {// Synchronize data from the main application updateStoreState(store, ns, getNamespaceState(mainStore.state, ns)); if (mainStore.__subWatchs.has(ns)) { return; } // Listen for the status of the main application, Const w = mainStore.watch(state => getNamespaceState(state, ns), value => updateSubStoreState([...mainStore.__subStores], ns, value), { deep: true }); Console. log(' main app store listens to module [${ns}] data '); mainStore.__subWatchs.set(ns, w); }); } return store; };Copy the code

See here, you must also marvel at the subtle micro front! The paper comes zhongjue shallow, look forward to your practical action, if you encounter any problems, welcome to pay attention to our LigaAI@juejin, exchange together, common progress ~ 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

By Alone Zhou at blog.cn-face.com/2021/06/17/…

Copyright Notice: All articles in this blog are licensed under by-NC-SA unless otherwise stated. Reprint please indicate the source!