What is a micro front end
Microfront-end is an architecture similar to microservices. It is an architectural style consisting of multiple independently delivered front-end applications, which are broken down into smaller, simpler applications that can be independently developed, tested, and deployed, but are still cohesive as a single product in the view of users. To put it simply, a project can have multiple subprojects embedded within it, which can be developed, deployed and technology framework independent.
Usage scenarios
- Large-scale enterprise Web application development
- Collaborative development across teams and enterprise applications
- I don’t know if you have ever experienced the project known as shit mountain, which is to use old technology and pile up a lot of business and need to spend a lot of cost to reconstruct. I think this scenario is suitable for using micro front-end to solve.
MicroApp introduction
Before Micro-App, there were several open source micro-front-end frameworks in the industry, two of which were popular: Single-spa and Qiankun Single-SPA match sub-applications of rendering and render by listening to URL change events when routing changes. This idea is also the mainstream way to achieve micro front-end at present. Single-spa also requires the child application to modify the rendering logic and exposes three methods: bootstrap, mount, and unmount, which are for initialization, rendering, and unmount respectively. This also causes the child application to modify the entry file. Because the Qiankun was packaged based on single-SPA, these features were also inherited by Qiankun and required some modifications to the Webpack configuration.
Micro-app does not follow the idea of single-spa, but draws on the idea of WebComponent. By combining CustomElement with custom ShadowDom, micro-front-end is encapsulated into a webcomponent-like component, so as to realize component-based rendering of micro-front-end. In addition, due to the isolation characteristics of custom ShadowDom, Micro-App does not require sub-applications to modify the rendering logic and expose the methods like Single-SPA and Qiankun, nor does it need to modify the Webpack configuration. It is the solution with the lowest cost to connect the micro front end on the market at present.
Micro-app encapsulates all functionality in a WebComponent-like component, allowing a single line of code embedded in the base application to render a micro-front-end application. Meanwhile, Micro-App also provides a series of perfect functions such as JS sandbox, style isolation, element isolation, preloading, data communication, static resource completion and plug-in system.
Micro-app is compatible with all framework and scaffold building tools including Vite, Nextjs, and Nuxtjs.
Basic WebComponent concepts
WebComponent is a component form like Vue and React that leverages the official native API. It consists of three main technologies that can be used together to create custom elements that encapsulate functionality and can be reused anywhere you like without worrying about code conflicts.
- Custom Elements: A set of JavaScript apis that allow you to define Custom elements and their behavior, and then use them as needed in your user interface.
- Shadow DOM: A set of JavaScript apis for attaching a wrapped “Shadow” DOM tree to an element (rendered separately from the main document DOM) and controlling its associated functionality. This way, you can keep the functionality of elements private so that they can be scripted and styled without fear of running afoul of the rest of the document.
- HTML Templates (HTML templates) : The
and
elements allow you to write tag templates that are not displayed on rendered pages. They can then be reused many times as a basis for custom element structures.
Write a simple component
-
Define custom components:
class MyButton extends HTMLElement { constructor () { super(a);Create a shadow root const shadow = this.attachShadow({mode: 'open'}); const template = document.getElementById('mybutton'); const content = template.content.cloneNode(true); shadow.appendChild(content); } // Lifecycle // connectedCallback: called when a Custom Element is first inserted into the DOM of the document. // disconnectedCallback: called when a Custom Element is removed from the DOM of the document. // adoptedCallback: called when a Custom Element is moved to a new document. / / attributeChangedCallback: when a custom element increase, delete and modify their own property, is invoked. connectedCallback() { console.log('Custom square element added to page.'); updateStyle(this); } disconnectedCallback() { console.log('Custom square element removed from page.'); } adoptedCallback() { console.log('Custom square element moved to new page.'); } attributeChangedCallback(name, oldValue, newValue) { console.log('Custom square element attributes changed.'); updateStyle(this); }}Copy the code
-
Defining a component template:
<template id="mybutton"> <style> button { color: white; background-color: # 666; padding: 5px; } </style> <button>Add</button> </template> Copy the code
-
Registered components:
window.customElements.define('my-button', MyButton); Copy the code
-
Using components:
<body> <my-button></my-button> </body> Copy the code
Core principles
The core functions of MicroApp are built on the basis of CustomElement, which is used to create custom tags and provides hook functions such as element rendering, unloading and attribute modification. We use the hook functions to know the rendering time of MicroApp and use the custom tags as containers. All elements and style scopes of microapplications cannot escape the container boundary, thus forming a closed environment.
Concept diagram & comparison diagram
The basic use
The React base application is used as an example
The base application
Each custom tag micro-App is rendered as a sub-application of a micro front end, which is used in a similar way to iframe tags. We need to pass three basic attributes to the tag:
- Name: name of the
- Url: indicates the address of the sub-application page
- Baseurl: Baseurl is the route prefix assigned by the base application to its subapplications
// router.js
import { BrowserRouter, Switch, Route } from 'react-router-dom'
import ChildPage from './child-page'
export default function AppRoute () {
return (
<BrowserRouter>
<Switch>// Child /* will be matched to ChildPage. // Child is the baseroute route prefix assigned to child applications<Route path='/child'> <ChildPage /> </Route>
</Switch>
</BrowserRouter>)}export function ChildPage () {
return (
<div>
<h1>The child application</h1>
<micro-app
name='child-app'
url='http://localhost:3000/'
baseroute='/child'// LifecycleonCreated={()= >Console. log('micro-app element is created ')} onBeforemount={() => console.log(' soon to be rendered ')} onMounted={() => console.log(' already rendered ')} OnUnmount = {() = > console. The log (' have unloaded)} onError = {() = > console. The log (' rendering errors')} ></micro-app>
</div>
)}
Copy the code
The child application
If the sub-application is multi-page, you only need to modify the route configuration and add route prefixes. As follows: window.__micro_app_base_url__ is the route prefix delivered by the base application. In a non-micro front-end environment, this value is undefined
React
import { BrowserRouter, Switch, Route } from 'react-router-dom'
export default function AppRoute () {
return (
<BrowserRouter basename={window.__MICRO_APP_BASE_ROUTE__| | '/'} >
<Switch>.</Switch>
</BrowserRouter>)}Copy the code
Vue2
// main.js
import VueRouter from 'vue-router'
import routes from './router'
const router = new VueRouter({
mode: 'history'.// 👇 __MICRO_APP_BASE_ROUTE__ is the base route passed by micro-app
base: window.__MICRO_APP_BASE_ROUTE__ || process.env.BASE_URL,
routes,
})
Copy the code
Full case: github.com/micro-zoe/m…
Data communication
Data in MicroApp is bound to communicate, that is, each micro-App element can only communicate with the sub-application it points to. In this way, each application has a clear data link to avoid data confusion. Meanwhile, MicroApp also supports global communication to transfer data across applications.
The base application sends data to its child applications
Method 1: Send data through the data property
// Because React does not support the object attributes of the tag well, we need to introduce a polyfill.
/ * *@jsxRuntime classic */
/ * *@jsx jsxCustomEvent */
import jsxCustomEvent from '@micro-zoe/micro-app/polyfill/jsx-custom-event'
<micro-app
name='my-app'
url='xx'
data={data} // Data only accepts object types, strictly compares (===), and resends when new data objects are passed in
/>
Copy the code
Mode 2: Manually send data
import microApp from '@micro-zoe/micro-app'
// Send data to the child my-app
microApp.setData('my-app', {type: 'New data'})
Copy the code
The child application gets data from the base application
Method 1: Bind/unbind listener functions
function dataListener (data) {
console.log('Data from the base application', data)
}
/** * bind listener function * dataListener: bind function * autoTrigger: cache data when bind listener function for the first time. AutoTrigger is mainly for child applications, because child applications are rendered asynchronously. * If the base application sends data while the child is not rendered, the child does not fire the binding function after initialization, * but the data is put into the cache, You can set autoTrigger to true to actively trigger a listener function to retrieve data. * /
window.microApp? .addDataListener(dataListener:Function, autoTrigger? : boolean)// Unbind the specified function
window.microApp? .removeDataListener(dataListener)// Clear all binding functions of the current child application (except global data functions)
window.microApp? .clearDataListener()Copy the code
Method 2: Proactively obtain data
window.microApp? .getData()// Return data
Copy the code
The child application sends data to the base application
window.microApp? .dispatch({type: 'Data sent by child application'})
Copy the code
The base application gets data from the child application
Method 1: Listen for custom events
/ * *@jsxRuntime classic */
/ * *@jsx jsxCustomEvent */
import jsxCustomEvent from '@micro-zoe/micro-app/polyfill/jsx-custom-event'
<micro-app
name='my-app'
url='xx'
data={data}
In the event.detail.data field, the child application refires the event each time it sends data
// The onDataChange function will be unbound automatically when the child application is uninstalled. No manual handling is required
onDataChange={(e) = > console.log(e.detail.data)}
/>
Copy the code
Method 2: Manually bind the listener function
import microApp from '@micro-zoe/micro-app'
function dataListener (data) {
console.log('Data from child my-app', data)
}
/** * Bind listener function * appName: application name * dataListener: bind function * autoTrigger: cache data when binding listener function for the first time, whether to actively trigger once, the default is false */
microApp.addDataListener(appName: string, dataListener: Function, autoTrigger? : boolean)// Unbind the function that listens to the my-app child
microApp.removeDataListener(appName: string, dataListener: Function)
// Clear all functions that listen on the appName child application
microApp.clearDataListener(appName: string)
Copy the code
Approach 3: Proactively obtain data
microApp.getData(appName) // Return the data sent by the child application
Copy the code
Global data communication (communication between applications)
To send data
/ * * -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- base application -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - * /
import microApp from '@micro-zoe/micro-app'
microApp.setGlobalData({type: 'Global data'})
/ * * -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- the child application -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - * /
window.microApp? .setGlobalData({type: 'Global data'})
Copy the code
Receive data
/ * * -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- base application -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - * /
import microApp from '@micro-zoe/micro-app'
function dataListener (data) {
console.log('Global data', data)
}
/** * bind listener * dataListener: bind function * autoTrigger: cache data when bind listener function for the first time
microApp.addGlobalDataListener(dataListener: Function, autoTrigger? : boolean)// Unbind the specified function
microApp.removeGlobalDataListener(dataListener)
// Clears the global data functions bound to the base application
microApp.clearGlobalDataListener()
/ * * -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- the child application -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - * /
function dataListener (data) {
console.log('Global data', data)
}
/** * bind listener * dataListener: bind function * autoTrigger: cache data when bind listener function for the first time
window.microApp? .addGlobalDataListener(dataListener:Function, autoTrigger? : boolean)// Unbind the specified function
window.microApp? .removeGlobalDataListener(dataListener)// Clears the global data function bound by the current child application
window.microApp? .clearGlobalDataListener()Copy the code
Jumping between applications
The base sends pushState
import { useEffect } from 'react'
import microApp from '@micro-zoe/micro-app'
export default (props) => {
function pushState (path) {
props.history.push(path)
}
useEffect(() = > {
// 👇 the base issues a method called pushState to the child applicationMicroapp.setdata (subapp name, {pushState})}, [])return (
<div>
<micro-app name='Child application Name' url='... '></micro-app>
</div>)}Copy the code
Subapplications use pushState jumps
window.microApp.getData().pushState(path)
Copy the code
Plug-in system
MicroApp provides a plug-in system, every JS file will pass through the plug-in system, we can use the plug-in system to do some special processing for these JS.
use
import microApp from '@micro-zoe/micro-app'
microApp.start({
plugins: {
// A global plugin that works on all child applications' js files
global? :ArrayThe < {// Optional, strongly isolated global variables (by default, global variables that cannot be found by the underlying application will end up in the base application. ScopeProperties disables this)scopeProperties? : string[],// Optional, can escape to external global variables (the variables in escapeProperties will be assigned to both the child application and the external real window)escapeProperties? : string[],// Optional configuration item to pass to loaderoptions? : any,// Mandatory, js handler function, must return code valueloader? :(code: string, url: string, options: any) = > code
}>
// Child application plug-inmodules? : {// appName is the name of the application. These plug-ins will only apply to the specified application
[appName: string]: ArrayThe < {// Optional, strongly isolated global variables (by default, global variables that cannot be found by the underlying application will end up in the base application. ScopeProperties disables this)scopeProperties? : string[],// Optional, can escape to external global variables (the variables in escapeProperties will be assigned to both the child application and the external real window)escapeProperties? : string[],// Optional configuration item to pass to loaderoptions? : any,// Mandatory, js handler function, must return code valueloader? :(code: string, url: string, options: any) = > code
}>
}
}
})
Copy the code
IO /micro-app/d…