什么是微前端
微前端是指存在于浏览器中的微服务,其借鉴了微服务的架构理念,将微服务的概念扩展到了前端。
如果对微服务的概念比较陌生的话,可以简单的理解为微前端就是将一个大型的前端应用拆分成多个模块,每个微前端模块可以由不同的团队进行管理,并可以自主选择框架,并且有自己的仓库,可以独立部署上线。
基于qiankun的微前端实战
这里准备三个项目,基座使用react项目,两个子应用一个使用react18,一个使用vue3。
├── base-mrc // 基座
├── mrc-react // react子应用,create-react-app创建的react应用,使用webpack打包
├── mrc-vue // vue子应用,vite创建的子应用
基座配置,这里用react项目配置基座
主要负责集成所有的子应用,提供一个入口能够访问你所需要的子应用的展示,尽量不写复杂的业务逻辑 - 子应用:根据不同业务划分的模块,每个子应用都打包成umd
模块的形式供基座(主应用)来加载。
- 安装qiankun
npm i qiankun // 或者 yarn add qiankun
2、入口文件配置(index.js)
import { start, registerMicroApps } from 'qiankun';
const apps = [
{
name: "mrcReact", // 子应用的名称
entry: 'http://localhost:8081/mrcReact/', // 默认会加载这个路径下的html,解析里面的js
activeRule: "/mrcReact", // 匹配的路由
container: "#container" // 加载的容器
},
{
name: "mrcVue", // 子应用的名称
entry: 'http://localhost:8082/mrcVue/', // 默认会加载这个路径下的html,解析里面的js
activeRule: "/mrcVue", // 匹配的路由
container: "#container" // 加载的容器
},
]
// 2. 注册子应用
setTimeout(() => {
registerMicroApps(apps, {
beforeLoad: [async app => console.log('before load', app.name)],
beforeMount: [async app => console.log('before mount', app.name)],
afterMount: [async app => console.log('after mount', app.name)],
})
start();
})
3、在页面上添加id为container的占位符,用来加载子应用
<div className="root">
<div className="menu">
<div className="menu-item" onClick={()=>nav('/mrcReact')}>首页</div>
<div className="menu-item" onClick={()=>nav('/mrcVue')}>新闻</div>
</div>
<div className="cont"><Outlet /><div id='container'></div></div>
</div>
react子应用配置
使用create-react-app
脚手架创建,webpack
进行配置,为了不eject所有的webpack配置,我们选择用react-app-rewired
工具来改造webpack配置。
1入口文件配置(index.js)
// 防止资源加载错位
import './public-path.js';
//qiankun环境下应用挂在到基座的root元素下
let root;
function render(props) {
const { container } = props
const dom = container ? container.querySelector('#root') : document.getElementById('root')
root = ReactDOM.createRoot(dom)
root.render(
<BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? '/mrcReact' : '/'}>
<App />
</BrowserRouter>
)
}
// 判断是否在qiankun环境下,非qiankun环境下独立运行
if (!window.__POWERED_BY_QIANKUN__) {
render({});
}
export async function bootstrap() {
console.log('react app bootstraped');
}
// qiankun环境下,应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
export async function mount(props) {
render(props);
}
// 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
export async function unmount(props) {
root.unmount();
}
2、资源加载(public-path.js)
if (window.__POWERED_BY_QIANKUN__) {
// 动态设置 webpack publicPath,防止资源加载出错
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}
3、配置webpack改造打包方式(config-overrides.js)
子应用打包方式改成umd方式并在请求头添加跨域设置
const { name } = require("./package");
process.env.PORT = 8081;
module.exports = {
webpack: (config) => {
config.output.library = `${name}-[name]`;
config.output.libraryTarget = 'umd';
// If you are using webpack 5, please replace jsonpFunction with chunkLoadingGlobal
config.output.chunkLoadingGlobal = `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;
},
};
vue子应用配置
1入口文件配置(main.js)
import './public-path'
let app;
if (!window.__POWERED_BY_QIANKUN__) {
createApp(App).mount('#app');
}
export async function bootstrap() {
console.log('[vue] vue app bootstraped');
}
export async function mount(props) {
app = createApp(App);
console.log("props.container.querySelector('#app'):", props.container.querySelector('#app'));
app.mount(props.container.querySelector('#app'));
}
export async function unmount() {
app?.unmount();
}
2、资源加载(public-path.js)
if (window.__POWERED_BY_QIANKUN__) {
// 动态设置 webpack publicPath,防止资源加载出错
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}
3、配置webpack改造打包方式(vue.config.js)
const { defineConfig } = require('@vue/cli-service')
const { name } = require('./package');
module.exports = defineConfig({
transpileDependencies: true,
publicPath: '/mrcVue',
devServer: {
port: 8082,
headers: {
'Access-Control-Allow-Origin': '*',
},
},
configureWebpack: {
output: {
library: `${name}-[name]`,
libraryTarget: 'umd', // bundle the micro app into umd library format
chunkLoadingGlobal: `webpackJsonp_${name}`, // // If you are using webpack 5, please replace jsonpFunction with chunkLoadingGlobal
},
},
})
问题汇总
1、在基座中刷新某个子应用时而能出来时而加载不出来(子应用加载快于基座,导致找不到根节点),解决方案:延迟加载子应用
setTimeout(() => {
registerMicroApps(apps, {
beforeLoad: [async app => console.log('before load', app.name)],
beforeMount: [async app => console.log('before mount', app.name)],
afterMount: [async app => console.log('after mount', app.name)],
})
start();
})
2、qiankun实现了各个子应用之间的样式隔离,但是基座和子应用之间的样式隔离没有实现,所以基座和子应用之前的样式还会有冲突和覆盖的情况,解决方法:1、每个应用的样式使用固定的格式 2、通过css-module
的方式给每个应用自动加上前缀
// 子应用配置css module
css: {
loaderOptions: {
css: {
modules: {
auto: () => true /* 样式会被编译独一无二的字段,需要通过引用变量的形式加载样式 */
}
}
}
}
// 加载样式
import styles from "./Header.module.css";
export default function Header() {
return <h2 className={styles.title}>Header 组件</h2>;
}
3、父子应用的通信
基座引用qiankun框架的initGlobalState属性注册全局状态,子应用通过生命周期props可以发送和接收数据。
// 基座通过onGlobalStateChange监听数据
let state = {msg: ''}
const actions = initGlobalState(state);
// 主项目项目监听和修改
actions.onGlobalStateChange((state, prev) => {
console.log("父应用接受到数据:", state, prev);
});
// 子应用通过setGlobalState发送数据
export async function mount(props) {
setInterval(() => {
props.setGlobalState({msg: 666});
}, 5000);
}