1 背景
『微前端』这个概念最早是2016年底出现在ThoughtWork上,它把『微服务』的概念扩展到了前端世界。下面有两张图来类比『微服务』和『微前端』。
图1: 从整体一个team → 前端后端2个team → 1个前端Team 对应多个微服务
图2:展示的是一个纵向的组织结构,各Team借助『微前端』分别负责独立的部分
2 微前端是什么?
Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently. -- Micro Frontends
微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。
3 为什么要用微前端?
随着前端项目越来越大、功能越来越复杂、技术框架越来越多,相互之间互不兼容,对于开发和维护是个巨大的挑战,微前端的技术刚好可以解决这些问题。微前端主要有如下优势:
- 技术栈无关 主框架不限制接入应用的技术栈,微应用具备完全自主权
- 独立开发、独立部署 微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新
- 增量升级 在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略
- 独立运行时 每个微应用之间状态隔离,运行时状态不共享
4 微前端与微服务
- 『微服务』是指后端服务,它们在自己的操作系统中运行,管理自己的数据库并通过网络进行彼此间的通信。『微前端』是指存在于浏览器中的微服务,他们之间的通信发生在内存中,而不是通过网络进行通信。
- 他们都可以独立的构建和部署。将DOM视为微前端使用的共享资源。一个微前端的DOM不能够被其他微前端触及,类似于一个微服务的数据库不应该被其他没有权限的微服务触及。
- 每个微前端都拥有独立的git仓库、package.json和构建工具配置,每个微前端可以由不同的团队进行管理,并可以自主选择框架。
5 市面上都有哪些微前端解决方案?
当前较具影响的方案有:
- single-spa 微前端方案鼻祖,实现了路由劫持和应用加载
- qiankun 基于single-spa封装,提供了更加开箱即用的 API
- Isomorphic Layout Composer - 一个将微前端组成部分支持SSR完整的解决方案
6 微前端single-spa的主要构成
6.1 single-spa 项目类型
- single-spa applications:为一组特定路由渲染组件的微前端。
- single-spa parcels: 不受路由控制,渲染组件的微前端,官方不推荐使用。
- utility modules: 非渲染组件,用于暴露共享javascript逻辑的微前端。
6.2 single-spa 项目结构
- root-config 根配置,可以理解为主应用,包含各应用共享的根 HTML 页面,和各应用的注册信息,如6.3.1.
- app-parcel 子应用,独立应用程序,对外暴露了生命周期,便于主应用统一管理,如6.3.2、6.3.3
- util-module 公共模块,为其他微前端应用导出要导入的功能,常见示例包括样式指南、身份验证助手和API助手。这些模块不需要向single-spa注册,但是对于维护几个single-spa应用程序和parcel之间的一致性非常重要。
6.3 single-spa如何使用?
6.3.1 主应用 root-config
- 通用的HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Root Config</title>
<!--
Remove this if you only support browsers that support async/await.
This is needed by babel to share largeish helper code for compiling async/await in older
browsers. More information at https://github.com/single-spa/create-single-spa/issues/112
-->
<script src="https://cdn.jsdelivr.net/npm/regenerator-runtime@0.13.7/runtime.min.js"></script>
<!--
This CSP allows any SSL-enabled host and for arbitrary eval(), but you should limit these directives further to increase your app's security.
Learn more about CSP policies at https://content-security-policy.com/#directive
-->
<meta http-equiv="Content-Security-Policy" content="default-src 'self' https: localhost:*; script-src 'unsafe-inline' 'unsafe-eval' https: localhost:*; connect-src https: *:* ws://*:*; style-src 'unsafe-inline' https:; object-src 'none';">
<meta name="importmap-type" content="systemjs-importmap" />
<!-- If you wish to turn off import-map-overrides for specific environments (prod), uncomment the line below -->
<!-- More info at https://github.com/joeldenning/import-map-overrides/blob/master/docs/configuration.md#domain-list -->
<!-- <meta name="import-map-overrides-domains" content="denylist:prod.example.com" /> -->
<!-- Shared dependencies go into this import map. Your shared dependencies must be of one of the following formats:
1. System.register (preferred when possible) - https://github.com/systemjs/systemjs/blob/master/docs/system-register.md
2. UMD - https://github.com/umdjs/umd
3. Global variable
More information about shared dependencies can be found at https://single-spa.js.org/docs/recommended-setup#sharing-with-import-maps.
-->
<script type="systemjs-importmap">
{
"imports": {
"single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.9.0/lib/system/single-spa.min.js"
}
}
</script>
<link rel="preload" href="https://cdn.jsdelivr.net/npm/single-spa@5.9.0/lib/system/single-spa.min.js" as="script">
<!-- Add your organization's prod import map URL to this script's src -->
<!-- <script type="systemjs-importmap" src="/importmap.json"></script> -->
<% if (isLocal) { %>
<script type="systemjs-importmap">
{
"imports": {
"react": "https://cdn.jsdelivr.net/npm/react@16.13.1/umd/react.production.min.js",
"react-dom": "https://cdn.jsdelivr.net/npm/react-dom@16.13.1/umd/react-dom.production.min.js",
"@demo/root-config": "//localhost:9000/demo-root-config.js",
"@demo/app-react": "//localhost:8081/demo-app-react.js",
"@demo/app-vue": "//localhost:8082/js/app.js"
}
}
</script>
<% } %>
<!--
If you need to support Angular applications, uncomment the script tag below to ensure only one instance of ZoneJS is loaded
Learn more about why at https://single-spa.js.org/docs/ecosystem-angular/#zonejs
-->
<!-- <script src="https://cdn.jsdelivr.net/npm/zone.js@0.11.3/dist/zone.min.js"></script> -->
<script src="https://cdn.jsdelivr.net/npm/import-map-overrides@2.2.0/dist/import-map-overrides.js"></script>
<% if (isLocal) { %>
<script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/system.js"></script>
<script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/extras/amd.js"></script>
<% } else { %>
<script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/system.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/extras/amd.min.js"></script>
<% } %>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<main>
<a href="/react">React子应用</a> <a href="/vue">Vue子应用</a>
</main>
<script>
System.import('@demo/root-config');
</script>
<import-map-overrides-full show-when-local-storage="devtools" dev-libs></import-map-overrides-full>
</body>
</html>
- 应用注册
// single-spa-config.js
import { registerApplication, start } from 'single-spa';
// Simple usage
registerApplication(
'app2',
() => import('src/app2/main.js'),
(location) => location.pathname.startsWith('/app2'),
{ some: 'value' }
);
// Config with more expressive API
registerApplication({
name: 'app1',
app: () => import('src/app1/main.js'),
activeWhen: '/app1',
customProps: {
some: 'value',
}
);
start();
6.3.2 React 子应用
import React from "react";
import ReactDOM from "react-dom";
import singleSpaReact from "single-spa-react";
import Root from "./root.component";
const lifecycles = singleSpaReact({
React,
ReactDOM,
rootComponent: Root,
errorBoundary(err, info, props) {
// Customize the root error boundary for your microfrontend here.
return null;
},
});
export const { bootstrap, mount, unmount } = lifecycles;
6.3.3 Vue子应用
import { h, createApp } from 'vue';
import singleSpaVue from 'single-spa-vue';
import App from './App.vue';
const vueLifecycles = singleSpaVue({
createApp,
appOptions: {
render() {
return h(App, {
});
},
},
});
export const { bootstrap, mount, unmount } = vueLifecycles;
6.3.4 父应用向子应用传递数据
registerApplication({
name: "@demo/app-react",
app: () => System.import("@demo/app-react"),
activeWhen: ["/react"],
customProps: { authToken: "d83jD63UdZ6RS6f70D0" } // 传递authToken到子应用
});
registerApplication({
name: "@demo/app-vue",
app: () => System.import("@demo/app-vue"),
activeWhen: ["/vue"],
customProps: { authToken: "d83jD63UdZ6RS6f70D0" } // 传递authToken到子应用
});
// react 接收 父应用的参数 authToken,根组件的props可以直接获取
// root.component.js
export default function Root(props) {
return <section>{props.name} is mounted! authToken:{props.authToken}</section>;
}
// vue 接收 父应用的参数 authToken,需要在根组件传入一下
const vueLifecycles = singleSpaVue({
createApp,
appOptions: {
render() {
return h(App, {
authToken: this.authToken
});
},
},
});
6.3.5 utility modules 通用模块
- 通用模块实例代码
export function authenticatedFetch(url, init) {
return fetch(url, init).then(r => {
// Maybe do some auth stuff here
return r.json()
})
}
- 在single-spa应用中引用的实例代码
import React from 'react'
import { authenticatedFetch } from '@org-name/api';
export function Foo(props) {
React.useEffect(() => {
const abortController = new AbortController()
authenticatedFetch(`/api/clients/${props.clientId}`, {signal: abortController.signal})
.then(client => {
console.log(client)
})
return () => {
abortController.abort()
}
}, [props.clientId])
return null
}
7. qiankun的项目构成
qiankun 是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统
7.1 qiankun 项目类型
相比single-spa,qiankun更简单好用,没有那么多复杂的概念,只需要了解两个概念:
- 主应用,负责微应用注册、路由接管
- 微应用,入口文件添加 bootstrap、mount、unmount 三个生命周期钩子,并导出为umd模块
7.2 qiankun 如何使用
7.2.1 在主应用注册微应用
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'reactApp',
entry: '//localhost:3000',
container: '#container',
activeRule: '/app-react',
},
{
name: 'vueApp',
entry: '//localhost:8080',
container: '#container',
activeRule: '/app-vue',
},
{
name: 'angularApp',
entry: '//localhost:4200',
container: '#container',
activeRule: '/app-angular',
},
]);
// 启动 qiankun
start();
7.2.2 React 微应用
- 在 src 目录新增 public-path.js:
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
- 设置 history 模式路由的 base:
<BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? '/app-react' : '/'}>
- 修改入口文件 index.js ,导出生命周期
import './public-path';
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
function render(props) {
const { container } = props;
ReactDOM.render(<App />, container ? container.querySelector('#root') : document.querySelector('#root'));
}
if (!window.__POWERED_BY_QIANKUN__) {
render({});
}
export async function bootstrap() {
console.log('[react16] react app bootstraped');
}
export async function mount(props) {
console.log('[react16] props from main framework', props);
render(props);
}
export async function unmount(props) {
const { container } = props;
ReactDOM.unmountComponentAtNode(container ? container.querySelector('#root') : document.querySelector('#root'));
}
- 修改 webpack 配置
- 解决跨域问题
// webpack-dev-server
headers: {
'Access-Control-Allow-Origin': '*'
}
- 设置导出为umd模块
output: {
path: projectRoot,
filename: 'js/[name].js',
chunkFilename: 'chunk/[name].chunk.js',
publicPath: '/',
+ library: `${name}-[name]`,
+ libraryTarget: 'umd',
+ jsonpFunction: `webpackJsonp_${name}`,
},
7.2.3 Vue 微应用
- 同react创建public-path
- 修改入口文件导出生命周期
import './public-path';
import Vue from 'vue';
import VueRouter from 'vue-router';
import App from './App.vue';
import routes from './router';
import store from './store';
Vue.config.productionTip = false;
let router = null;
let instance = null;
function render(props = {}) {
const { container } = props;
router = new VueRouter({
base: window.__POWERED_BY_QIANKUN__ ? '/app-vue/' : '/',
mode: 'history',
routes,
});
instance = new Vue({
router,
store,
render: (h) => h(App),
}).$mount(container ? container.querySelector('#app') : '#app');
}
// 独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
export async function bootstrap() {
console.log('[vue] vue app bootstraped');
}
export async function mount(props) {
console.log('[vue] props from main framework', props);
render(props);
}
export async function unmount() {
instance.$destroy();
instance.$el.innerHTML = '';
instance = null;
router = null;
}
- 打包配置修改(vue.config.js):
const { name } = require('./package');
module.exports = {
devServer: {
headers: {
'Access-Control-Allow-Origin': '*',
},
},
configureWebpack: {
output: {
library: `${name}-[name]`,
libraryTarget: 'umd', // 把微应用打包成 umd 库格式
jsonpFunction: `webpackJsonp_${name}`,
},
},
};
8 项目部署
建议:主应用和微应用都是独立开发和部署,即它们都属于不同的仓库和服务。每个应用程序(又名微服务,又名ES模块)都可以独立开发和部署。团队可以按照自己的进度进行工作,独立开发、测试和部署