1. Why you should learn micro front-end
What is micro frontend
Micro frontend is to split different functions into multiple sub-applications according to different dimensions. These sub-applications are loaded through the main application.
The core of micro front-end lies in disassembly, and then put it together after disassembly!
Why use micro frontends
- How to solve the problem of different technology stacks when different teams develop the same application?
- I hope each team can develop independently. How to break independent deployment?
- How can I break the old application code that is still needed in the project?
Can we divide an application into several sub-applications and package the sub-applications into libs? Load the same sub-application when the path is switched. In this way, each sub-application is independent, and there is no need to limit the technology stack! Thus solving the problem of front-end collaborative development
How to implement micro front-end
The inspiration of micro front-end comes from computer applications. Every time a user opens an application, it is equivalent to opening a new page.
- Single-SPA was born in 2018. Single-spa is a JavaScript front-end solution for front-end microservices (it does not handle style isolation, js execution isolation) and implements route hijacking and application loading.
- In 2019, qiankun provided a more out-of-the-box API (single-spa + sandbox + import-html-entry) based on Single-SPA. It has nothing to do with the technology stack and is easy to access (as simple as an iframe)
Summary: Sub-applications can be built independently and loaded dynamically at runtime. The main and sub-applications are completely decoupled and have nothing to do with the technology stack. They rely on protocol access (sub-applications must export bootstrap, mount, and unmount methods)
Here are the answers to your questions:
-
Isn't this an iframe?
If iframe is used, it will be awkward for the user to refresh the page when the sub-application in the iframe switches routes.
More referenceWhy Not Iframe
-
How to communicate between applications
- Data transfer is based on URL, but the ability to transfer messages is weak
- Implement communication based on CustomEvent
- Communication between master and child applications based on props
- Communicate using global variables, Redux
-
public dependency
- CDN - externals
- webpack federation module
The micro front-end architecture has the following core values:
-
Technology stack independent
The main framework does not limit the technology stack of the access application, and micro-applications have complete autonomy.
-
Independent development and independent deployment
The micro-application warehouse is independent, and the front-end and back-end can be developed independently. After the deployment is completed, the main framework automatically completes synchronous updates.
-
Incremental upgrade
When faced with various complex scenarios, it is usually difficult for us to fully upgrade or reconstruct the technology stack of an existing system, and micro-frontends are a very good means and strategy for implementing progressive reconstruction.
-
standalone runtime
The state of each micro application is isolated and the runtime state is not shared.
2. SingleSpa in practice
Build a sub-application
We need the parent application to load the child application and need to expose three methods
1. bootstrap
2. mount
3. unmount
- Build a sub-application
vue create single-child
npm i --save single-spa-vue
// 在子应用的 main.js 中导入依赖并进行配置
import singleSpaVue from 'single-spa-vue'
const appOptions = {
el: '#vue', // 挂载到父应用中的 id 为 vue 的标签中
router,
render: h => h(App)
}
const vueLifeCycle = singleSpaVue({
// 返回single-spa 的生命周期也就是 bootstrap/mount/unmount
Vue,
appOptions
});
// single规定的协议,父应用会调用这些方法
export const bootstrap = vueLifeCycle.bootstrap;
export const mount = vueLifeCycle.mount;
export const unmount = vueLifeCycle.unmount;
// 这样做就有一个严重的问题,子应用无法启动了??
- Configure the packaging path in the sub-application
// 配置vue.config.js
module.exports = {
configureWebpack: {
output: {
library: 'singleVue',
libraryTarg: 'umd'
},
devServer: {
port: 10000
}
}
};
- Configure routing for sub-applications
const router = new VueRouter({
mode: 'history',
base: '/vue', // 配置子应用路由的基础路径
routes
})
- Parent application construction
vue create single-parent
npm i --save single-spa // 注意这里是 single-spa
- Mount the sub-application into
id="vue"
the container of
<div id="app">
<!-- 当路由切换到 /vue 时加载子应用 -->
<router-link to="/vue">加载vue引用</router-link>
<router-view/>
<!-- 子应用加载的位置 -->
<div id="vue"></div>
</div>
- Configure the parent application to load the child application
// 在父应用中导入依赖
import {
registerApplication, start } from 'single-spa'
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);
});
}
// 注册一个应用
registerApplication(
'myVueApp',
async () => {
console.log('加载模块');
// 加载子应用中的脚本
await loadScript(`http://localhost:10000/js/chunk-vendors.js`)
await loadScript(`http://localhost:10000/js/app.js`)
// 这里需要要返回 bootstrap/mount/unmount
return window.singleVue
},
location => location.pathname.startsWith('/vue'), // 此路径用来判断当前路由切换到 /vue 的路径下,需要加载我们定义的子应用
{
a: 1 } // 选传,传给子应用 props 的参数,可以是对象或值
);
start(); // 启动应用
- Configure the path of the sub-application
// 设置路径
if (window.singleSpaNavigate) {
// 如果是父应用加载子应用,那会自动挂载一个属性,值为true
__webpack_public_path__ = 'http://localhost:10000/'
}
- If you want the sub-application to run independently, add a configuration in the sub-application
if(!window.singleSpaNavigate){
delete appOptions.el; // 子应用中没有#vue,所以需要手动删除,挂载到 #app 中
new Vue(appOptions).$mount('#app');
}
singleSpa defect
- JS files cannot be loaded dynamically
- Styles are not isolated
- Global object, no JS sandbox mechanism
3. Qiankun actual combat
Features
1. 简单:任意 js 框架均可使用。微应用接入像使用接入一个 iframe 系统一样简单,但实际不是 iframe。
2. 完备:几乎包含所有构建微前端系统时所需要的基本能力,如 样式隔离、js 沙箱、预加载等。
3. 生产可用:已在蚂蚁内外经受过足够大量的线上系统的考验及打磨,健壮性值得信赖。
Project build
- Main application construction
qiankun-base
vue create qiankun-base
npm i --save qiankun
// 配置主项目的加载 main.js
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';
import {
registerMicroApps, start} from 'qiankun';
Vue.config.productionTip = false
Vue.use(ElementUI);
const apps = [
{
name: 'vueApp', // 应用的名字
entry: 'http://localhost:10000/', // 默认加载这个html,解析里面的js动态的执行(子应用必须支持跨域,内部使用的是 fetch)
container: '#vue', // 要渲染到的容器名id
activeRule: '/vue' // 通过哪一个路由来激活
},
{
name: 'reactApp',
entry: 'http://localhost:20000/',
container: '#react',
activeRule: '/react'
}
];
registerMicroApps(apps); // 注册应用
start(); // 开启应用
new Vue({
router,
render: h => h(App)
}).$mount('#app')
<!-- 设置容器 -->
<template>
<div>
<el-menu :router="true" mode="horizontal">
<!-- 主应用中也可以放自己的路由 -->
<el-menu-item index="/">首页</el-menu-item>
<!-- 引用其他的子应用 -->
<el-menu-item index="/vue">vue应用</el-menu-item>
<el-menu-item index="/react">react应用</el-menu-item>
</el-menu>
<router-view v-show="$route.name"></router-view>
<div id="vue"></div>
<div id="react"></div>
</div>
</template>
- Build Vue subproject
vue create qiankun-vue
// 子项目中不需要安装任何依赖,父组件会给window设置一些环境变量
// mian.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
Vue.config.productionTip = false
/*
new Vue({
router,
render: h => h(App)
}).$mount('#app')
*/
let instance = null;
function render(props) {
// props 组件通信
instance = new Vue({
router,
render: h => h(App)
}).$mount('#app') // 这里是挂载到自己的HTML中,基座会拿到这个挂载后的HTML,将其插入进去
}
if (!window.__POWERED_BY_QIANKUN__) {
// 如果是独立运行,则手动调用渲染
render();
}
if(window.__POWERED_BY_QIANKUN__){
// 如果是qiankun使用到了,则会动态注入路径
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
// 根据 qiankun 的协议需要导出 bootstrap/mount/unmount
export async function bootstrap(props) {
};
export async function mount(props) {
render(props);
};
export async function unmount(props) {
instance.$destroy();
};
// 设置router路径
const router = new VueRouter({
mode: 'history',
base: '/vue',
routes
})
// 配置打包 vue.config.js
module.exports = {
devServer: {
port: 10000,
headers:{
'Access-Control-Allow-Origin': '*' // 允许跨域
}
},
configureWebpack: {
output: {
library: 'vueApp',
libraryTarget: 'umd'
}
}
};
- Build a React project
npx create-react-app qiankun-react
npm i --save-dev react-app-rewired
// 入口配置 /src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
function render(){
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
}
if(!window.__POWERED_BY_QIANKUN__){
render();
}
export async function bootstrap(){
}
export async function mount() {
render()
}
export async function unmount(){
ReactDOM.unmountComponentAtNode( document.getElementById('root'));
}
// 配置启动 config-overrides.js
module.exports = {
webpack:(config)=>{
config.output.library = 'reactApp';
config.output.libraryTarget = 'umd';
config.output.publicPath = 'http://localhost:20000/';
return config;
},
devServer:(configFunction)=>{
return function (proxy,allowedHost){
const config = configFunction(proxy,allowedHost);
config.headers = {
"Access-Control-Allow-Origin":'*'
}
return config
}
}
}
添加react环境变量 .env
PORT=20000
WDS_SOCKET_PORT=20000
// 配置react路由
import {
BrowserRouter, Route, Link } from "react-router-dom"
const BASE_NAME = window.__POWERED_BY_QIANKUN__ ? "/react" : "";
function App() {
return (
<BrowserRouter basename={
BASE_NAME}>
<Link to="/">首页</Link>
<Link to="/about">关于</Link>
<Route path="/" exact render={
() => <h1>hello home</h1>}></Route>
<Route path="/about" render={
() => <h1>hello about</h1>}></Route>
</BrowserRouter>
);
}