What is a micro front end?
The micro front end is to divide different functions into sub-applications according to different dimensions. These subapplications are loaded through the main application (base). The core of the micro front end lies in the demolition, demolition after the closing
Why use him?
- How do different technology stacks work for different teams developing the same application?
- Each team can develop and deploy independently
- Some old application code in the project
The realization idea of micro-front-end is to divide an application into several sub-applications and package the sub-applications into lib. Load different subapplications when the path is switched. In this way, each sub-application is independent and the technology stack is not restricted! Thus, the problem of front-end collaborative development is solved
Several schemes of realizing micro front end
plan | describe | advantages | disadvantages |
---|---|---|---|
Nginx route forwarding | Nginx configures reverse proxy to map different paths to different applications | Simple, fast and easy to configure | The browser refreshes when switching apps, affecting the experience |
Nested iframe | The parent application is a single page, and each child application has a nested IFrame | Simple implementation, the sub application between their own sandbox, natural isolation, mutual influence | Frame style display and compatibility have limitations. Too simple and low |
NPM package form | The source code of the sub-project is distributed in the form of NPM package. Packaged build releases are still managed by the base project and integrated as packaged. | The package deployment is slow and cannot be independently deployed | The package deployment is slow and cannot be independently deployed |
Universal center routing pedestal type | Sub-projects can use different technology stacks; The sub-projects are completely independent without any dependence; The unified management is carried out by the base project, which is completed according to the registration, mounting and unmounting of DOM nodes. | Unrestricted technology stack, deployed separately | Communication is not flexible enough |
Base type for specific center routing | Use the same technology stack between sub-lines of business; Base engineering and sub-engineering can be developed and deployed separately; The sub-project has the ability to reuse the common infrastructure of the base project. | Multiple communication modes are deployed separately | Restricted technology stack |
The current mainstream micro front-end framework
- Single-spa Single-SPA is a JavaScript front-end solution for front-end microservitization (no style isolation itself, JS execution isolation) that implements route hijacking and application loading
- With a more out-of-the-box API (Single-SPA + Sandbox + import-HTML-entry), technology stack independent and easy to access (as simple as iframe)
single-spa
Both the base and subprojects use the VUE technology stack. React works the same way: the base provides registration logic and the child application provides three protocol access methods and a packaging format
Base (VUE)
From the base we call the registerApplication and start methods given to us by single-SPA 1. The registerApplication parameter has four components: appNameOrConfig, appOrLoadApp, activeWhen, and customProps. This corresponds to the registered subproject name and some configuration. Fn executed when the subproject is loaded, rules executed, and parameters communicated
main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import {registerApplication,start} from 'single-spa';
// Load the URL dynamically
async function loadScript(url){
return new Promise((resolve,reject) = >{
let script = document.createElement('script');
script.src = url;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script); })}// singleSpa is not flexible enough to load JS files dynamically
// Styles do not segregate without js sandbox mechanism
registerApplication('myVueApp'.async() = > {// When the match is successful, load the js of the child application
await loadScript(`http://localhost:10000/js/chunk-vendors.js`);
await loadScript(`http://localhost:10000/js/app.js`)
return window.singleVue; // Subapplications are packaged in UMD format. bootstrap mount unmount
},
// Execute the above method when /vue is matched
location= > location.pathname.startsWith('/vue'),// Start the application
start();
new Vue({
router,
render: h= > h(App)
}).$mount('#app')
Copy the code
subprojects
The most important part of the subproject is to provide three methods bootstrap, mount, unmount, and package formats
main.js
Use single-SPa-vue and React. It exports three methods by default.
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import singleSpaVue from 'single-spa-vue';
const appOptions = {
el:'#vue'.// Mount to the tag vue in the parent application
router,
render: h= > h(App)
}
const vueLifeCycle = singleSpaVue({
Vue,
appOptions
})
// In microfront-end mode, single-spa will hang a singleSpaNavigate property on the window.
// We need to change public_path to the address in the subproject.
if(window.singleSpaNavigate){
__webpack_public_path__ = 'http://localhost:10000/'
}
// This allows subprojects to run independently
if(!window.singleSpaNavigate){
delete appOptions.el;
new Vue(appOptions).$mount('#app');
}
// Protocol access I set up the protocol parent application to call these methods
export const bootstrap = vueLifeCycle.bootstrap;
export const mount = vueLifeCycle.mount;
export const unmount = vueLifeCycle.unmount;
Copy the code
Configure the packaging method of the subproject vue.config.js
module.exports = {
configureWebpack: {output: {library:'singleVue'.libraryTarget:'umd'
},
devServer: {port:10000}}}Copy the code
The defects of single – spa
- As can be seen from the above figure, the base needs to manually add JS when matching the path. If the subproject packages 10,000 JS,….
- Styles not isolated
- There is no JS sandbox mechanism
qiankun
As simple as accessing iframe, THE FETCH method inserts HTML directly into the container, so subprojects need to allow cross-domain access
base
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import ElementUI from "element-ui";
import "element-ui/lib/theme-chalk/index.css";
Vue.use(ElementUI);
// Two methods are derived from Qiankun
import { registerMicroApps, start } from "qiankun";
const apps = [
{
name: "vueApp".// The application name
entry: "//localhost:10000".// The HTML path to load
container: "#vue"./ / container
activeRule: "/vue".// Active path
},
{
name: "reactApp".entry: "//localhost:20000".container: "#react".activeRule: "/react",},]; registerMicroApps(apps);// Register the application
start(); / / open
new Vue({
router,
render: (h) = > h(App),
}).$mount("#app");
Copy the code
subprojects
import Vue from 'vue'
import App from './App.vue'
import router from './router'
// Vue.config.productionTip = false
let instance = null
function render(props) {
instance = new Vue({
router,
render: h= > h(App)
}).$mount('#app'); // This is mounting to its OWN HTML and the base is going to take this mounted HTML and insert it into it
}
if (window.__POWERED_BY_QIANKUN__) { // Dynamically add publicPath
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
if (!window.__POWERED_BY_QIANKUN__) { // Run independently by default
render();
}
// Subcomponent protocol is ok
export async function bootstrap(props) {};export async function mount(props) {
render(props)
}
export async function unmount(props) {
instance.$destroy();
}
Copy the code
Subprojects package configuration
module.exports = {
devServer: {port:10000.// Cross-domain needs to be allowed
headers: {'Access-Control-Allow-Origin':The '*'}},configureWebpack: {output: {library:'vueApp'.libraryTarget:'umd'}}}Copy the code
Application of communication
- Data transmission is based on URL, but message transmission ability is weak
- Implement communication based on CustomEvent
- Communication between props master sub-applications
- Use global variables, Redux for communication
CSS Isolation Scheme
Style isolation between child applications:
- Dynamic Stylesheet, which removes the old application style and adds the new one when the application switches
Style isolation between primary and child applications:
- BEM (Block Element Modifier) Specifies the project prefix
- Css-modules package generates non-conflicting selector names
- Shadow Dom is truly isolated
Js sandbox mechanism
- Snapshot sandbox: Records snapshots when the sandbox is used for mounting or unmounting, and restores the environment based on the snapshot during switchover. (Multiple realtime is not supported
Example)
- Proxy The Proxy sandbox does not affect the global environment
The snapshot sandbox
- 1. Snapshot the current Window property during activation
- 2. During deactivation, compare the snapshot with the current Window property
- 3. If the property changes, save them to modifyPropsMap and restore the Window property using the snapshot
- 4. During the second activation, take a snapshot again and restore Windows using the last modification result
class SnapshotSandbox{
constructor(){
this.proxy = window;
this.modifyPropsMap = {};
this.active();
}
active(){ / / activation
this.windowSnapshot = {}; / / photo
for(const prop in window) {if(window.hasOwnProperty(prop)){
this.windowSnapshot[prop] = window[prop]; }}Object.keys(this.modifyPropsMap).forEach(p= >{
window[p] = this.modifyPropsMap[p]; })}inactive(){ / / the deactivation
for(const prop in window) {if(window.hasOwnProperty(prop)){
if(window[prop] ! = =this.windowSnapshot[prop]){
this.modifyPropsMap[prop] = window[prop];
window[prop] = this.windowSnapshot[prop]
}
}
}
}
}
Copy the code
Agent sandbox
Each application creates a proxy to represent the Window. The advantage is that each application is relatively independent and does not need to change the global Window property directly!
class ProxySandbox {
constructor() {
const rawWindow = window;
const fakeWindow = {}
const proxy = new Proxy(fakeWindow, {
set(target, p, value) {
target[p] = value;
return true
},
get(target, p) {
returntarget[p] || rawWindow[p]; }});this.proxy = proxy
}
}
Copy the code
Micro-front-end reconstruction based on jquery project
Project background
The project uses the JQ framework of Backbonejs, including a series of dependencies such as Bootstrap. Jquery is a shuttle, but it can be a bit of a chore to maintain. This led to the idea of refactoring with a micro front end.
- The routing mode used in the project is hash mode
Firstly, as this project is an interface similar to background management system, there are many menus on the left side. If the jquery project is a sub-project, it might change a lot, so the JQ project is used as the base station directly. When the left route is clicked, the child application is loaded via a matching mechanism and embedded into the container.
jquery
First, qiankun was introduced
import {registerMicroApps, addGlobalUncaughtErrorHandler, start} from 'qiankun';
Copy the code
mian.js
function genActiveRule(routerPrefix) {
return location= > location.hash.startsWith(The '#' + routerPrefix);
}
$(function() {
/ / layout
createLayout();
/ / routing
createRoute();
// Go to the previous page
gotoLastPage();
registerMicroApps([
{
name: 'micro'.entry: '//xxx.com/home/vue/'.container: '#main-app'.activeRule: genActiveRule('/vue')}]); addGlobalUncaughtErrorHandler(event= > {
console.error(event);
const {message: msg} = event;
// Load failed
if (msg && msg.includes('died in status LOADING_SOURCE_CODE')) {
console.error('Child application failed to load, please check if application is running'); }}); start(); monitorOrders.init(); });Copy the code
The first entry content is that the address that can be accessed by our sub-application after packaging and deployment is added with an error message provided by Qiankun
Sub-project (VUE)
main.js
import Vue from "vue";
import VueRouter from "vue-router";
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
// import "./public-path";
import App from "./App.vue";
import routes from "./routes";
Vue.use(VueRouter);
Vue.use(ElementUI);
Vue.config.productionTip = false;
let instance = null;
let router = null;
/** * Render functions * two cases: run in the main application lifecycle hook/run when the micro-application starts alone */
function render() {
console.log("window.__POWERED_BY_QIANKUN__".window.__POWERED_BY_QIANKUN__);
// Create VueRouter in render to ensure that the location event listener is removed when the microapplication is uninstalled to prevent event contamination
router = new VueRouter({
// Add the routing namespace /vue when running in the main application
base: window.__POWERED_BY_QIANKUN__ ? "/vue" : "/".mode: "hash",
routes,
});
// Mount the application
instance = new Vue({
router,
render: (h) = > h(App),
}).$mount("#app");
}
// Mount the application directly when running independently
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
/** * Bootstrap will only be called once during the micro-application initialization. The next time the micro-application re-enters, the mount hook will be called directly. Bootstrap will not be triggered again. * This is usually where we can initialize global variables, such as application-level caches that will not be destroyed during the unmount phase. * /
export async function bootstrap() {
console.log("VueMicroApp bootstraped");
}
/** * The application calls the mount method every time it enters, and usually we trigger the application's render method here */
export async function mount(props) {
console.log("VueMicroApp mount", props);
render(props);
}
/** * The method that is called each time the application is cut/unloaded, usually here we unload the application instance of the microapplication */
export async function unmount() {
console.log("VueMicroApp unmount");
instance.$destroy();
instance = null;
router = null;
}
Copy the code
vue.config.js
const path = require("path");
function resolve(dir) {
return path.join(__dirname, dir);
}
const stage = process.env.ENV === "testing";
const publicPath =
process.env.ENV === "production"
? xxxx:xxx
let output = {
library: "Micro".// Expose your library in a way that all module definitions work
libraryTarget: "umd".WebpackJsonp_VueMicroApp = webpackJsonp_VueMicroApp
jsonpFunction: `webpackJsonp_customerMicro`};let cssExtract = {};
if (stage) {
output.filename = `js/[name].js? v=[hash]`;
output.chunkFilename = `js/[name].js? v=[hash]`;
cssExtract = {
filename: "css/[name].css? v=[hash]".chunkFilename: "css/[name].css? v=[hash]"}; }module.exports = {
publicPath,
devServer: {
// Listen on the port
port: 10200.// Turn off host checking so that microapplications can be fetched
disableHostCheck: true.// Configure cross-domain request headers to solve cross-domain problems in the development environment
headers: {
"Access-Control-Allow-Origin": "*",}},css: {
extract: cssExtract,
},
configureWebpack: () = > {
const conf = {
resolve: {
alias: {
"@": resolve("src"),}},output: output,
};
return conf;
},
chainWebpack(config) {
config.plugins.delete("preload");
config.plugins.delete("prefetch");
config.module
.rule("vue")
.use("vue-loader")
.loader("vue-loader")
.tap((options) = > {
options.compilerOptions.preserveWhitespace = true;
return options;
})
.end();
config.when(process.env.NODE_ENV === "development".(config) = > config.devtool("cheap-source-map")); }};Copy the code
My configuration above distinguishes between a test environment and a formal environment. You can also do it based on the actual situation. The entry on the base station side can also be based on the environment.
A collection of microfront-end resources
A collection of micro front ends
Thank you for your
1. If the article is helpful to you, please move your little hands to point out a praise 👍, your praise is the power of my creation.
2. Pay attention to the public account “front-end elite”! Push high-quality articles from time to time.