微前端的好处是不言而喻的,有效的拆分应用,实现敏捷开发和部署。
相比较于微服务的概念,微服务的重点大概有两点:
微前端也是一样,我们可以把数据库的概念换成dom,加上前端应用的特点,可以得出微前端的重点:
该架构落地的方式有iframe、npm包、WebComponent以及现有成熟框架等。
现有框架有single-spa、qiankun、MicroApp,qiankun是基于single-spa开发的,将这两者合并讲解。
这两个框架的基本思路有较大区别:
github上有个mini版微前端教学项目,实现方式虽不完全等于上面两个框架,但却是一个比较好的思路,推荐学习。下文记录了各部分实现的基本思路以及比较完整的工作流程,这里实现了元素隔离、样式隔离、JS沙箱、数据通信。
摘要:
具体流程见下图
qiankun文档的指导手册写的很清晰,不同类型的子应用都有示例
下面给出我的demo。
import { registerMicroApps, start } from 'qiankun';
const reactAppConfig = {
name: 'react app',
entry: 'http://localhost:8002',
container: '#react-app',
activeRule: location => location.pathname.startsWith('/react-app'),
};
const vueAppConfig = {
name: 'vue app',
entry: 'http://localhost:8001',
container: '#vue-app',
activeRule: location => location.pathname.startsWith('/vue-app'),
};
registerMicroApps([reactAppConfig, vueAppConfig]);
start();
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import './public-path';
let root;
function render(props) {
root = ReactDOM.createRoot(
props.container
? props.container.querySelector('#root')
: document.getElementById('root')
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
}
if (!window.__POWERED_BY_QIANKUN__) {
render({});
}
/**
* bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
* 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
*/
export async function bootstrap() {
console.log('react app bootstraped');
}
/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/
export async function mount(props) {
console.log('react app mount');
render(props);
}
/**
* 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
*/
export async function unmount(props) {
console.log('react app unmount');
root?.unmount();
}
/**
* 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
*/
export async function update(props) {
console.log('react app update props', props);
}
public-path.js
,在入口文件引入,主要是避免子应用的静态资源使用相对地址时加载失败的情况if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
@rescripts/cli
修改(当然还有类似的其他库),安装@rescripts/cli之后根目录添加.rescriptsrc.js
const { name } = require('./package');
module.exports = {
webpack: config => {
config.output.library = `${name}-[name]`;
config.output.libraryTarget = 'umd';
config.output.jsonpFunction = `webpackJsonp_${name}`;
config.output.globalObject = 'window';
return config;
},
devServer: _ => {
const config = _;
config.headers = {
'Access-Control-Allow-Origin': '*',
};
config.historyApiFallback = true;
config.hot = false;
config.watchContentBase = false;
config.liveReload = false;
return config;
},
};
这里说三点配置:
historyApiFallback
:解决单页的 404 问题Access-Control-Allow-Origin
:解决主应用访问子应用的跨域问题umd
模式:只在初次渲染时执行所有jsSKIP_PREFLIGHT_CHECK=true
BROWSER=none
PORT=8002
WDS_SOCKET_PORT=8002
大体和React子应用一样
/* eslint-disable */
import { createApp } from 'vue';
import VueRouter from 'vue-router';
import App from './App.vue';
import routes from './router';
import './public-path';
let app, router;
function render(props) {
app = createApp(App);
router = new VueRouter({
base: window.__POWERED_BY_QIANKUN__ ? '/app-vue/' : '/',
mode: 'history',
routes,
});
app.mount(
props.container
? props.container.querySelector('#app')
: document.getElementById('app')
);
}
if (!window.__POWERED_BY_QIANKUN__) {
render({});
}
/**
* bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
* 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
*/
export async function bootstrap() {
console.log('vue app bootstraped');
}
/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/
export async function mount(props) {
render(props);
console.log('vue app mount');
}
/**
* 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
*/
export async function unmount() {
app.unmount();
console.log('vue app unmount');
}
/**
* 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
*/
export async function update(props) {
console.log('vue app update props', props);
}
public-path.js
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
vue.config.js
const packageName = require('./package.json').name;
module.exports = {
configureWebpack: {
devtool: 'source-map',
output: {
library: `${packageName}-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${packageName}`,
},
},
devServer: {
port: 8001,
headers: {
'Access-Control-Allow-Origin': '*',
},
historyApiFallback: true,
},
publicPath: '//localhost:8001/',
};
使用loadMicroApp
// 加载多个子应用
let reactLoadApp, vueLoadApp;
history.listen(history => {
if (history.pathname === '/multiple-app') {
if (!vueLoadApp && !reactLoadApp) {
reactLoadApp = loadMicroApp(reactAppConfig);
vueLoadApp = loadMicroApp(vueAppConfig);
}
} else {
reactLoadApp?.unmount();
vueLoadApp?.unmount();
reactLoadApp = null;
vueLoadApp = null;
}
});
上述代码的效果是当路由跳转到/multiple-app
,就会显示React和Vue子应用。这里要注意,在卸载之后要将reactLoadApp
和vueLoadApp
置为null,否则会出问题。
主应用,initGlobalState
初始化一个对象,通过onGlobalStateChange
属性监听变化,setGlobalState
属性设置数据。
import { initGlobalState } from 'qiankun';
// 初始化 state
const state = { message: 'message' };
const actions = initGlobalState(state);
actions.onGlobalStateChange((state, prev) => {
// state: 变更后的状态; prev 变更前的状态
console.log('主应用监听到变化', state, prev);
});
setTimeout(() => {
actions.setGlobalState({ message: 'message-main' });
}, 2000);
子应用,一般在mount里接收,props上可以接收到onGlobalStateChange
和setGlobalState
// 一般在mount里接收
export async function mount(props) {
console.log('react app mount');
render(props);
props.onGlobalStateChange((state, prev) => {
// state: 变更后的状态; prev 变更前的状态
console.log('react子应用监听到变化', state, prev);
});
setTimeout(() => {
props.setGlobalState({ message: 'message-react' });
});
}
最常见的大概就是这个错误了
Application died in status LOADING_SOURCE_CODE: You need to export the functional lifecycles in xxx entry
文档给出了很全面的解答
接入成本比qiankun低,官网有很详细的手把手教的例子。
下面给出我的demo。
入口文件
import microApp from '@micro-zoe/micro-app';
microApp.start();
新建ReactMicroApp组件
import React from 'react';
export default function ReactMicroApp() {
return (
);
}
新建VueMicroApp组件
import React from 'react';
export default function VueMicroApp() {
return (
);
}
和接入qiankun的某些步骤是一样的,这里不写具体代码了:
public-path.js
文件,入口文件引入Access-Control-Allow-Origin
不需要子应用导出任何函数
本身就是组件化方式引入,多个子应用引入多个组件即可
import React from 'react';
export default function MultipleMicroApp() {
return (
<>
>
);
}
这里以react子应用为例,其他子应用相同使用方式
主应用,简单的microApp.setData
import React from 'react';
import microApp from '@micro-zoe/micro-app';
export default function ReactMicroApp() {
const sendMessageToMicro = () => {
// 传入的数据必须是对象
microApp.setData('react-app', {
message: `父给子传的数据 ${Math.random()}`,
});
};
return (
<>
>
);
}
子应用,设置监听函数window.microApp.addDataListener
import { useEffect } from 'react';
import './App.css';
function App() {
useEffect(() => {
if (window.microApp) {
const dataListener = data => {
alert('主应用传来的数据:' + JSON.stringify(data));
};
window.microApp.addDataListener(dataListener);
return () => {
window.microApp.clearDataListener();
};
}
}, []);
return (
...
);
}
export default App;
子应用,简单的window.microApp.dispatch
import { useEffect } from 'react';
import './App.css';
function App() {
useEffect(() => {
if (window.microApp) {
const dataListener = data => {
alert('主应用传来的数据:' + JSON.stringify(data));
};
window.microApp.addDataListener(dataListener);
return () => {
window.microApp.clearDataListener();
};
}
}, []);
const sendMeaasgeToMain = () => {
window.microApp.dispatch({
message: `react-app子应用传来的数据 ${Math.random()}`,
});
};
return (
...
);
}
export default App;
主应用,给组件加onDataChange
属性。一定要导入jsxCustomEvent
,而且注释也必须加
import React from 'react';
import microApp from '@micro-zoe/micro-app';
/** @jsxRuntime classic */
/** @jsx jsxCustomEvent */
import jsxCustomEvent from '@micro-zoe/micro-app/polyfill/jsx-custom-event';
export default function ReactMicroApp() {
const sendMessageToMicro = () => {
microApp.setData('react-app', {
message: `父给子传的数据 ${Math.random()}`,
});
};
const onDataChange = e => {
console.log(e);
alert('子应用传来的消息' + JSON.stringify(e.detail));
};
return (
<>
>
);
}
对上面两个框架的demo操作过程进行一个简单的对比,可见MicroApp的侵入性较低。
以上两个demo都是最简版的,更多高级操作请参见官方文档。
主应用
qiankun | MicroApp |
---|---|
子应用位置是Container element | 子应用位置是添加自定义标签元素 |
registerMicroApps | 不需要注册,普通的路由切换 |
子应用
qiankun | MicroApp |
---|---|
更改 public path | 更改 public path |
指定history模式路由的 basename | 指定history模式路由的 basename |
入口文件导出生命周期 | - |
配置跨域访问 | 配置跨域访问 |
配置 Webpack 的 output | - |
demo地址