什么是微前端
微前端是指存在于瀏覽器中的微服務(wù),其借鑒了微服務(wù)的架構(gòu)理念旦装,將微服務(wù)的概念擴展到了前端奇唤。
如果對微服務(wù)的概念比較陌生的話簇宽,可以簡單的理解為微前端就是將一個大型的前端應(yīng)用拆分成多個模塊,每個微前端模塊可以由不同的團隊進行管理婆跑,并可以自主選擇框架此熬,并且有自己的倉庫,可以獨立部署上線。
基于qiankun的微前端實戰(zhàn)
這里準(zhǔn)備三個項目犀忱,基座使用react項目募谎,兩個子應(yīng)用一個使用react18,一個使用vue3阴汇。
├── base-mrc // 基座
├── mrc-react // react子應(yīng)用数冬,create-react-app創(chuàng)建的react應(yīng)用,使用webpack打包
├── mrc-vue // vue子應(yīng)用搀庶,vite創(chuàng)建的子應(yīng)用
基座配置拐纱,這里用react項目配置基座
主要負責(zé)集成所有的子應(yīng)用,提供一個入口能夠訪問你所需要的子應(yīng)用的展示哥倔,盡量不寫復(fù)雜的業(yè)務(wù)邏輯 - 子應(yīng)用:根據(jù)不同業(yè)務(wù)劃分的模塊秸架,每個子應(yīng)用都打包成umd
模塊的形式供基座(主應(yīng)用)來加載。
- 安裝qiankun
npm i qiankun // 或者 yarn add qiankun
2咆蒿、入口文件配置(index.js)
import { start, registerMicroApps } from 'qiankun';
const apps = [
{
name: "mrcReact", // 子應(yīng)用的名稱
entry: 'http://localhost:8081/mrcReact/', // 默認會加載這個路徑下的html东抹,解析里面的js
activeRule: "/mrcReact", // 匹配的路由
container: "#container" // 加載的容器
},
{
name: "mrcVue", // 子應(yīng)用的名稱
entry: 'http://localhost:8082/mrcVue/', // 默認會加載這個路徑下的html,解析里面的js
activeRule: "/mrcVue", // 匹配的路由
container: "#container" // 加載的容器
},
]
// 2. 注冊子應(yīng)用
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的占位符缭黔,用來加載子應(yīng)用
<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子應(yīng)用配置
使用create-react-app
腳手架創(chuàng)建,webpack
進行配置芽突,為了不eject所有的webpack配置试浙,我們選擇用react-app-rewired
工具來改造webpack配置。
1入口文件配置(index.js)
// 防止資源加載錯位
import './public-path.js';
//qiankun環(huán)境下應(yīng)用掛在到基座的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環(huán)境下寞蚌,非qiankun環(huán)境下獨立運行
if (!window.__POWERED_BY_QIANKUN__) {
render({});
}
export async function bootstrap() {
console.log('react app bootstraped');
}
// qiankun環(huán)境下田巴,應(yīng)用每次進入都會調(diào)用 mount 方法,通常我們在這里觸發(fā)應(yīng)用的渲染方法
export async function mount(props) {
render(props);
}
// 應(yīng)用每次 切出/卸載 會調(diào)用的方法挟秤,通常在這里我們會卸載微應(yīng)用的應(yīng)用實例
export async function unmount(props) {
root.unmount();
}
2壹哺、資源加載(public-path.js)
if (window.__POWERED_BY_QIANKUN__) {
// 動態(tài)設(shè)置 webpack publicPath,防止資源加載出錯
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}
3艘刚、配置webpack改造打包方式(config-overrides.js)
子應(yīng)用打包方式改成umd方式并在請求頭添加跨域設(shè)置
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子應(yīng)用配置
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__) {
// 動態(tài)設(shè)置 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箩朴、在基座中刷新某個子應(yīng)用時而能出來時而加載不出來(子應(yīng)用加載快于基座,導(dǎo)致找不到根節(jié)點)秋度,解決方案:延遲加載子應(yīng)用
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實現(xiàn)了各個子應(yīng)用之間的樣式隔離,但是基座和子應(yīng)用之間的樣式隔離沒有實現(xiàn)荚斯,所以基座和子應(yīng)用之前的樣式還會有沖突和覆蓋的情況埠居,解決方法:1查牌、每個應(yīng)用的樣式使用固定的格式 2、通過css-module
的方式給每個應(yīng)用自動加上前綴
// 子應(yīng)用配置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纸颜、父子應(yīng)用的通信
基座引用qiankun框架的initGlobalState屬性注冊全局狀態(tài),子應(yīng)用通過生命周期props可以發(fā)送和接收數(shù)據(jù)绎橘。
// 基座通過onGlobalStateChange監(jiān)聽數(shù)據(jù)
let state = {msg: ''}
const actions = initGlobalState(state);
// 主項目項目監(jiān)聽和修改
actions.onGlobalStateChange((state, prev) => {
console.log("父應(yīng)用接受到數(shù)據(jù):", state, prev);
});
// 子應(yīng)用通過setGlobalState發(fā)送數(shù)據(jù)
export async function mount(props) {
setInterval(() => {
props.setGlobalState({msg: 666});
}, 5000);
}