先來張圖片壓壓驚
在線demo:wzs.bengdada.com/
單獨訪問在線子應用:
一.導讀
1.什么是微前端
- 微前端是一種多個團隊通過獨立發(fā)布功能的方式來共同構建現(xiàn)代化 web 應用的技術手段及方法策略熊镣。
- 微前端架構具備以下幾個核心價值:
技術棧無關
: 主框架不限制接入應用的技術棧前域,微應用具備完全自主權
獨立開發(fā)、獨立部署
: 微應用倉庫獨立,前后端可獨立開發(fā),部署完成后主框架自動完成同步更新
增量升級
:在面對各種復雜場景時,我們通常很難對一個已經存在的系統(tǒng)做全量的技術棧升級或重構灾挨,而微前端是一種非常好的實施漸進式重構的手段和策略
獨立運行時
: 每個微應用之間狀態(tài)隔離,運行時狀態(tài)不共享 - 微前端架構旨在解決單體應用在一個相對長的時間跨度下竹宋,由于參與的人員涨醋、團隊的增多、變遷逝撬,從一個普通應用演變成一個巨石應用(Frontend Monolith)后,隨之而來的應用不可維護的問題乓土。這類問題在企業(yè)級 Web 應用中尤其常見宪潮。
2.qiankun是什么
- qiankun 是一個基于 single-spa 的微前端實現(xiàn)庫,旨在幫助大家能更簡單趣苏、無痛的構建一個生產可用微前端架構系統(tǒng)狡相。
官網: https://qiankun.umijs.org/zh - qiankun特性
基于 single-spa 封裝,提供了更加開箱即用的 API食磕。
技術棧無關尽棕,任意技術棧的應用均可 使用/接入,不論是 React/Vue/Angular/JQuery 還是其他等框架彬伦。
HTML Entry 接入方式滔悉,讓你接入微應用像使用 iframe 一樣簡單。
樣式隔離单绑,確保微應用之間樣式互相不干擾回官。
JS 沙箱,確保微應用之間 全局變量/事件 不沖突搂橙。
?? 資源預加載歉提,在瀏覽器空閑時間預加載未打開的微應用資源,加速微應用打開速度区转。
umi 插件苔巨,提供了 @umijs/plugin-qiankun 供 umi 應用一鍵切換成微前端架構系統(tǒng)。 - 了解完理論基礎废离,讓我們動手實踐一下···
二.建立項目
如圖: 我建立了一個主應用和三個子應用
主應用 main vue3搭建
"vue": "^3.0.0",
子應用 micro-react react18搭建
"react": "^18.1.0",
子應用 micro-vue2 vue2搭建
"vue": "^2.6.11",
子應用 micro-vue3 vue3搭建
"vue": "^3.0.0",
注意 :
vue3技術選型我使用的是vue3 + webpack 侄泽,vite目前對于qiankun還不是太友好 ,硬要搞vite代價會很大厅缺,后續(xù)等官網優(yōu)化后我們在去使用vite
由于搭建項目太簡單我就不說明了 ~ ovo
三.主應用
注意:
qiankun 需要一個主應用 來注入所有的子應用
先安裝乾坤的依賴包
yarn add qiankun # 或者 npm i qiankun -S
目前乾坤是2.0版本 安裝后package.json 是2.72版本
在安裝 element-plus 把項目的布局簡單做一下
npm install element-plus --save
注意:
vue3 安裝element-plus蔬顾, vue2安裝element-ui
src下新建micro-app.js 用于存放所有子應用
const microApps = [
{
name: 'micro-react', //應用名 項目名最好也是這個
entry: '//localhost:20000', //默認會加載這個html 解析里面的js 動態(tài)的執(zhí)行 (子應用必須支持跨域)內部用的fetch
activeRule: '/react', // 激活的路徑
container: '#micro-react', // 容器名
props: {}, //父子應用通信
},
{
name: 'micro-vue2',
entry: '//localhost:30000',
activeRule: '/vue2',
container: '#micro-vue2',
props: {},
},
{
name: 'micro-vue3',
entry: '//localhost:40000',
activeRule: '/vue3',
container: '#micro-vue3',
props: {},
},
];
export default microApps;
新建vue.config.js
module.exports = {
devServer: {
port: 8000,
headers: {
// 重點1: 允許跨域訪問子應用頁面
'Access-Control-Allow-Origin': '*',
},
},
};
Main頁面
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
// createApp(App).use(store).use(router).mount('#app')
//-----------------------上面是原先的,下面是新增的-----------------------------
import ElementPlus from 'element-plus'; //element-plus
import 'element-plus/dist/index.css'; //element-plus
import { registerMicroApps, start } from 'qiankun';
import microApps from './micro-app';
let app = createApp(App);
app.use(store);
app.use(router);
app.use(ElementPlus);
app.mount('#app');
registerMicroApps(microApps, {
//還有一些生命周期 如果需要可以根據官網文檔按需加入
beforeMount(app) {
console.log('掛載前', app);
},
afterMount(app) {
console.log('卸載后', app);
},
});
start({
prefetch: false, //取消預加載
sandbox: { experimentalStyleIsolation: true }, //沙盒模式
});
進入App頁面簡單調下布局
<template>
<el-menu :router="true" mode="horizontal">
<!-- 子應用的跳轉路徑 -->
<el-menu-item index="/">主應用 main</el-menu-item>
<el-menu-item index="/react">子應用 react18</el-menu-item>
<el-menu-item index="/vue2">子應用 vue2</el-menu-item>
<el-menu-item index="/vue3">子應用 vue3</el-menu-item>
<el-menu-item index="/about">{{ $store.state.GlobalData }}</el-menu-item>
</el-menu>
<router-view />
<!-- 子應用的容器 -->
<div id="micro-react"></div>
<div id="micro-vue2"></div>
<div id="micro-vue3"></div>
</template>
<script>
export default {
name: 'App',
setup() {
return {};
},
};
</script>
<style lang="less">
html,
body,
#app {
width: 100%;
height: 100%;
margin: 0;
}
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
}
</style>
需要注意
: app里的容器名和跳轉路徑都不是隨便起的 需要和micro-app.js 定義好的子應用一一對應
到此主應用搭建完畢~~~ovo
四.子應用
1.react
安裝npm install react-app-rewired 重寫默認的react配置文件
npm install react-app-rewired --save
修改package.json,原本的react-script 改為react-app-rewired
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-app-rewired eject"
},
安裝npm i react-router-dom 我安裝的是最新版本 "react-router-dom": "^6.3.0"
npm i react-router-dom --save
根目錄下新建.env文件
PORT=20000
# 防止熱更新出錯
WDS_SOCKET_PORT=20000
src下新建public-path.js (用于修改運行時的 publicPath)
//判斷是否是qiankun加載
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
src下新建 config-overrides.js
const { name } = require('./package');
module.exports = {
webpack: config => {
config.output.library = `${name}-[name]`;
config.output.libraryTarget = 'umd';
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;
},
};
進入src下index.js
// import logo from './logo.svg';
// import './App.css';
// function App() {
// return (
// <div className="App">
// <header className="App-header">
// <img src={logo} className="App-logo" alt="logo" />
// <p>
// Edit <code>src/App.js</code> and save to reload.
// </p>
// <a
// className="App-link"
//
// target="_blank"
// rel="noopener noreferrer"
// >
// Learn React
// </a>
// </header>
// </div>
// );
// }
// export default App;
// ------------------------上面原先的宴偿,下面最新的------------------------------------
import logo from './logo.svg';
import './App.css';
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
function App() {
return (
<>
{/* basename 判斷如果是qiankun加載 basename為react 相當于加個標識*/}
<Router basename={window.__POWERED_BY_QIANKUN__ ? '/react' : '/'}>
{/* */}
<Link to="/">首頁</Link>
<Link to="/about">關于頁面</Link>
<Routes>
<Route path="/" element={<Home />}></Route>
<Route path="/about" element={<About />}></Route>
</Routes>
</Router>
</>
);
}
function About() {
return <div>about</div>;
}
function Home() {
return (
<div>
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a className="App-link" target="_blank" rel="noopener noreferrer">
Learn React
</a>
</header>
</div>
);
}
export default App;
2.vue2
src下新建public-path.js 用于修改運行時的 publicPath
// eslint-disable-next-line no-undef
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
在main頁面 引入public-path.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')
// ·················上面原先的 下面新增的·····················
import './public-path';
import Vue from 'vue';
import App from './App.vue';
import router from './router';
// Vue.config.productionTip = false
let instance = null;
function render(props = {}) {
const { container } = props;
instance = new Vue({
router,
render: (h) => h(App),
}).$mount(container ? container.querySelector('#app') : '#app');
}
// 如何獨立運行微應用?
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
export async function bootstrap(props) {
// 啟動
}
export async function mount(props) {
// 掛載 onGlobalStateChange 可通過這個屬性來進行父子應用通信 發(fā)布訂閱機制
render(props);
}
export async function unmount(props) {
// 卸載
instance.$destroy();
}
新增vue.config.js文件
const { name } = require('./package');
module.exports = {
devServer: {
port: 30000,
headers: {
'Access-Control-Allow-Origin': '*', //開發(fā)時增加跨域 表示所有人都可以訪問我的服務器
},
},
configureWebpack: {
output: {
library: `${name}-[name]`,
libraryTarget: 'umd', // 把子應用打包成 umd 庫格式
jsonpFunction: `webpackJsonp_${name}`,
},
},
};
router.js文件
const router = new VueRouter({
mode: 'history',
// base: process.env.BASE_URL,
base: '/vue2',
routes,
});
3.vue3
src下新建public-path.js 用于修改運行時的 publicPath
// eslint-disable-next-line no-undef
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
在main頁面 引入public-path.js文件
import './public-path'; // 注意需要引入public-path
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
let instance = null;
function render({ container } = {}) {
instance = createApp(App);
instance.use(router);
instance.use(store);
instance.mount(container ? container.querySelector('#app') : '#app');
}
// 如何獨立運行微應用诀豁?
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
export async function bootstrap(props) {
// 啟動
}
export async function mount(props) {
// 掛載
render(props);
}
export async function unmount(props) {
// 卸載
instance.unmount();
instance = null;
}
新增vue.config.js文件
const { name } = require('./package');
module.exports = {
devServer: {
port: 40000,
headers: {
'Access-Control-Allow-Origin': '*', //開發(fā)時增加跨域 表示所有人都可以訪問我的服務器
},
},
configureWebpack: {
output: {
library: `${name}-[name]`,
libraryTarget: 'umd', // 把子應用打包成 umd 庫格式
jsonpFunction: `webpackJsonp_${name}`,
},
},
};
到這里項目搭建完畢窄刘,基礎跳轉沒有問題 ,可以在主應用和子應用跳轉
bug
:主應用和子應用使用不同版本的vue后路由切換報錯 ?
bug
:主應用樣式與子應用樣式沖突 ?
需求
:父子組件傳參如何實現(xiàn) ?
需求
:如何部署 ?
別擔心 下面我一一解答
5.bug
[Bug]主應用和子應用使用不同版本的vue后路由切換報錯
問題的原因
: vue-router 3.x與vue-router 4.x設置的history.state的數據結構不同
低版本的 vue-router 在 pushState 的時候舷胜,會覆蓋丟失主路由的 history.state娩践,導致主路由跳轉異常
解決辦法
: 主應用監(jiān)聽router.beforEach 手動修改history.state數據結構
import _ from "lodash"
router.beforeEach((to, from, next) => {
if (_.isEmpty(history.state.current)) {
_.assign(history.state, { current: from.fullPath });
}
next();
});
[Bug]主應用樣式與子應用樣式沖突
可以通過給css樣式名加前綴來實現(xiàn)隔離
https://blog.csdn.net/zjscy666/article/details/107864891
https://blog.csdn.net/m0_54854484/article/details/123442168
6.需求
[需求] 父子組件傳參如何實現(xiàn)
qiankun
通過initGlobalState, onGlobalStateChange, setGlobalState實現(xiàn)主應用的全局狀態(tài)管理,然后默認會通過props
將通信方法傳遞給子應用烹骨。先看下官方的示例用法:
主應用
// main/src/main.js
import { initGlobalState } from 'qiankun';
// 初始化 state
const initialState = {
user: {} // 用戶信息
};
const actions = initGlobalState(initialState);
actions.onGlobalStateChange((state, prev) => {
// state: 變更后的狀態(tài); prev 變更前的狀態(tài)
console.log(state, prev);
});
actions.setGlobalState(state);
actions.offGlobalStateChange();
子應用
// 從生命周期 mount 中獲取通信方法翻伺,props默認會有onGlobalStateChange和setGlobalState兩個api
export function mount(props) {
props.onGlobalStateChange((state, prev) => {
// state: 變更后的狀態(tài); prev 變更前的狀態(tài)
console.log(state, prev);
});
props.setGlobalState(state);
}
這兩段代碼不難理解,父子應用通過onGlobalStateChange這個方法進行通信沮焕,這其實是一個發(fā)布-訂閱的設計模式吨岭。
ok,官方的示例用法很簡單也完全夠用峦树,純JavaScript的語法辣辫,不涉及任何的vue或react的東西,開發(fā)者可自由定制魁巩。
如果我們直接使用官方的這個示例急灭,那么數據會比較松散且調用復雜,所有子應用都得聲明onGlobalStateChange對狀態(tài)進行監(jiān)聽谷遂,再通過setGlobalState進行更新數據葬馋。
因此,我們很有必要對數據狀態(tài)做進一步的封裝設計
主應用src下新建actions.js
//src/actions.js
// 父子應用通信
import { initGlobalState } from 'qiankun';
import store from './store';
const state = {
//這里寫初始化數據
name: 'wang',
age: 123,
count: 0,
};
const actions = initGlobalState(state);
actions.onGlobalStateChange((state, prev) => {
console.log('主應用變更前:', state);
console.log('主應用變更后:', prev);
store.commit('setGlobalData', state);
});
store.commit('setGlobalData', state);
export default actions;
將初始化的數據存到vuex中 如果數據變更了 在將變更后的數據存到vuex
主應用main store文件夾下index.js中
//store/index.js
import { createStore } from 'vuex';
export default createStore({
state: {
GlobalData: {},
},
mutations: {
setGlobalData(state, value) {
state.GlobalData = value;
},
},
actions: {},
modules: {},
});
最后在main.js 中導入
//main.js
import './actions.js'
子應用 (vue3)
核心
:通過將主應用的onGlobalStateChange肾扰,setGlobalState方法掛載到全局就可以使用了
import './public-path'; // 注意需要引入public-path
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
let instance = null;
//核心
function render(props) {
const { container, onGlobalStateChange, setGlobalState } = props;
console.log(props);
instance = createApp(App);
instance.config.globalProperties.$onGlobalStateChange = onGlobalStateChange;
instance.config.globalProperties.$setGlobalState = setGlobalState;
instance.use(router);
instance.use(store);
instance.mount(container ? container.querySelector('#app') : '#app');
}
// 如何獨立運行微應用畴嘶?
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
export async function bootstrap(props) {
// 啟動
}
export async function mount(props) {
// 掛載
render(props);
}
export async function unmount(props) {
// 卸載
instance.unmount();
instance = null;
}
使用
主應用
<template>
<div>
<h1>主應用/vue3子應用 的全局數據</h1>
<div>姓名 : {{ $store.state.GlobalData.name }}</div>
<div>年齡 : {{ $store.state.GlobalData.age }}</div>
<div>數量 : {{ $store.state.GlobalData.count }}</div>
<el-button type="primary" @click="revampData">修改全局數據</el-button>
</div>
</template>
<script setup>
import actions from '../actions';
const revampData = () => {
actions.setGlobalState({ name: '主應用' });
};
</script>
子應用(vue3)
<template>
<div>我是vue3項目</div>
<button @click="revampData">修改全局數據</button>
</template>
<script>
import { getCurrentInstance } from 'vue';
export default {
name: 'Home',
components: {
HelloWorld,
},
setup() {
const { proxy } = getCurrentInstance();
const revampData = () => {
proxy.$setGlobalState({ name: 'vue3子應用應用' });
};
return {
revampData,
};
},
};
</script>
[需求] 如何部署
qiankun部署的帖子網上根本找不到, 可能是感覺簡單就都不想說了吧,筆者這里也是部署了很多遍才跑通白对,這里說下我的思路掠廓。
考慮到主應用和子應用共用域名時可能會存在路由沖突的問題,子應用可能會源源不斷地添加進來甩恼,因此我們將子應用都放在xx.com/subapp/
這個二級目錄下蟀瞧,根路徑/留給主應用。
步驟如下:
1.主應用main和所有子應用都打包出一份html,css,js,static条摸,分目錄上傳到服務器悦污,子應用統(tǒng)一放到subapp目錄下,最終如:
├── main
│ └── index.html
└── subapp
├── sub-react
│ └── index.html
└── sub-vue
└── index.html
2.配置nginx钉蒲,預期是xx.com根路徑指向主應用切端,xx.com/subapp指向子應用,子應用的配置只需寫一份,以后新增子應用也不需要改nginx配置顷啼,以下應該是微應用部署的最簡潔的一份nginx配置了踏枣。
server{
listen 80; #偵聽端口
server_name http://wzs.bengdada.com/; #定義使用www.xx.com訪問
charset utf-8;
location / {
root /data/wzs/main; # 主應用所在的目錄
try_files $uri $uri/ /index.html;
}
location /subapp {
alias /data/wzs/subapp; # 主應用所在的目錄
try_files $uri $uri/ /index.html;
}
}
nginx -s reload后就可以了昌屉。
本文特地做了線上demo展示:
整站(主應用):wzs.bengdada.com/
單獨訪問子應用:
最后
本人從開始弄微前端反復查閱大量資料和視頻,踩過很多坑茵瀑,忍不住感嘆 : 真是學無止境.....
最后的最后间驮,喜歡本文的同學還請能順手給個贊鼓勵一下,非常感謝看到這里马昨。