微前端框架 qiankun 項(xiàng)目實(shí)戰(zhàn)(一)--本地開發(fā)篇
回復(fù)1,加入高級(jí) Node 進(jìn)階交流群
作者:黑化程序員
https://juejin.cn/post/6970310177517993998
?
大家好,我是小黑。
公司使用技術(shù)棧是vue,最近遇到了一個(gè)需求,要把原有后臺(tái)管理系統(tǒng)的功能模塊搬遷到新的后臺(tái)管理系統(tǒng)上面去。原本這沒(méi)有多復(fù)雜的事,直接復(fù)制粘貼改改就可以,但是有這么幾個(gè)坑點(diǎn),我瞬間陷入了沉思:
-
新的后臺(tái)使用的是vue3,原有的后臺(tái)使用的是vue2 -
新的后臺(tái)有自己的一套登錄角色權(quán)限管理方案,舊的后臺(tái)也有 -
由于vue3和vue2區(qū)別還是比較大的,vue3相當(dāng)于整個(gè)vue重寫了,雖說(shuō)做了向下兼容,但是直接復(fù)制粘貼過(guò)去不太現(xiàn)實(shí)(主要是我試過(guò)復(fù)制了一個(gè)模塊過(guò)去devtools的紅色慘不忍睹)
怎么辦,把vue2寫好的模塊重新用vue3寫一次(令人窒息)?正準(zhǔn)備含淚敲鍵盤的時(shí)候,我想到了以前看過(guò)的微前端的相關(guān)文章,不如試試這個(gè)玩意吧,然后,微前端正式踩坑。
什么是微前端?
按照網(wǎng)上的說(shuō)法和小黑的理解,微前端就是應(yīng)用分割,獨(dú)立運(yùn)行,獨(dú)立部署,將原本把所有功能集中于一個(gè)項(xiàng)目中的方式轉(zhuǎn)變?yōu)榘压δ馨礃I(yè)務(wù)劃分成一個(gè)主項(xiàng)目和多個(gè)子項(xiàng)目,每個(gè)子項(xiàng)目負(fù)責(zé)自身功能,同時(shí)具備和其它子項(xiàng)目和主項(xiàng)目進(jìn)行通信的能力,達(dá)到更細(xì)化更易于管理的目的。總的來(lái)說(shuō)微前端就是
?一個(gè)完整應(yīng)用劃分成一個(gè)主應(yīng)用和一個(gè)或多個(gè)微應(yīng)用,應(yīng)用間相互獨(dú)立,可相互通信。
?
如何實(shí)現(xiàn)微前端?
符合上面條件,最容易想到的就是iframe,下面貼上兩段最簡(jiǎn)單的iframe及其通訊代碼
// parent.html
<div>我是parent</div>
<button id="parentBtn">parent btn</button>
<iframe src="./child.html" id="frame"></iframe>
<script>
function parentFunc(msg) {
console.log("parent 的方法:", msg)
}
var btn = document.querySelector("#parentBtn")
btn.addEventListener('click', function() {
console.log("我是parent的button")
console.log("我調(diào)用了:")
document.getElementById('frame').contentWindow.childFunc('parent');
})
</script>
// child.html
<div>我是child</div>
<button id="childBtn">child btn</button>
<script>
function childFunc(msg) {
console.log("child 的方法:", msg)
}
var btn = document.querySelector("#childBtn")
btn.addEventListener('click', function() {
console.log("我是child的button")
console.log("我調(diào)用了:")
parent.window.parentFunc('child');
})
</script>
以上兩段代碼放到本地服務(wù)器中就是這樣的
然后點(diǎn)擊兩個(gè)按鈕,就可以互相通信傳參了
?以上的兩個(gè)html必須放到有域名的環(huán)境中運(yùn)行,否則會(huì)報(bào)錯(cuò)。
?
當(dāng)然了,這次的項(xiàng)目遷移我不是直接用iframe改造的,而是站在巨人的肩膀上,我用了一個(gè)叫qiankun的微前端框架改造,因?yàn)楣镜拇a我不能貼上來(lái),下面我會(huì)建一個(gè)vue3項(xiàng)目和一個(gè)vue2項(xiàng)目來(lái)大概還原一下我是如何改造公司項(xiàng)目的,還有我遇到的坑是怎么填的。
微前端框架qiankun
首先,用vue官方的腳手架建立一個(gè)vue3的基本后臺(tái)界面和一個(gè)vue2的基本后臺(tái)界面,注意這里因?yàn)関ue3打包使用了vite的原因,所以qiankun框架不能使用vue3作為微應(yīng)用,這里我們主應(yīng)用是vue3,微應(yīng)用是vue2,這跟我改造的也是一致的,兩個(gè)項(xiàng)目大概結(jié)構(gòu)是一樣的,如下:
為了方便大家,貼上我建好的模板倉(cāng)庫(kù)地址
vue3模板:https://gitee.com/jimpp/vue3-main-app(主應(yīng)用,主應(yīng)用必須安裝qiankun)
vue2模板:https://gitee.com/jimpp/vue2-micro-app(微應(yīng)用)
上面master分支都是未改造前能獨(dú)立運(yùn)行的項(xiàng)目,dev分支是最終改造后的項(xiàng)目,當(dāng)然自己從頭到尾建立也是可以的,但是要保證兩個(gè)倉(cāng)庫(kù)都具備router,store,登錄攔截的功能
兩個(gè)模板都具備這樣的界面
1.登錄界面
咳咳,簡(jiǎn)陋了點(diǎn),為了顯示請(qǐng)不要打我哈哈。
2.左側(cè)菜單和router-view界面
好了,下面開始基于qiankun框架改造兩個(gè)項(xiàng)目
主應(yīng)用啟動(dòng)qiankun
這里我使用了qiankun官網(wǎng)的registerMicroApps注冊(cè)微應(yīng)用
在主應(yīng)用的src文件夾下新建一個(gè)micros文件夾,在micros文件夾新建index.js,app.js
// index.js
import NProgress from "nprogress";
import "nprogress/nprogress.css";
import {
registerMicroApps,
addGlobalUncaughtErrorHandler,
start,
} from "qiankun";// 微應(yīng)用注冊(cè)信息
import apps from "./app";
registerMicroApps(apps, {
beforeLoad: (app) => {
// 加載微應(yīng)用前,加載進(jìn)度條
NProgress.start();
console.log("before load", app.name);
return Promise.resolve();
},
afterMount: (app) => {
// 加載微應(yīng)用前,進(jìn)度條加載完成
NProgress.done();
console.log("after mount", app.name);
return Promise.resolve();
},
});
addGlobalUncaughtErrorHandler((event) => {
console.error(event);
const { message: msg } = event
if (msg && msg.includes("died in status LOADING_SOURCE_CODE")) {
console.error("微應(yīng)用加載失敗,請(qǐng)檢查應(yīng)用是否可運(yùn)行");
}
});
export default start;
首先yarn add nprogress安裝nprogress這個(gè)庫(kù),是為了到時(shí)候在加載微應(yīng)用的時(shí)候有進(jìn)度條顯示,這里用到了官方的幾個(gè)api
-
registerMicroApps:包含兩個(gè)參數(shù),第一個(gè)參數(shù)是微應(yīng)用的一些注冊(cè)信息,第二個(gè)參數(shù)是全局的微應(yīng)用生命周期鉤子。 -
addGlobalUncaughtErrorHandler:全局的未捕獲異常處理器,微應(yīng)用發(fā)生報(bào)錯(cuò)的時(shí)候亦可以用這個(gè)api捕捉。 -
start:我們用來(lái)啟動(dòng)qiankun的方法,包含一個(gè)參數(shù),具體的參數(shù)用途不再詳述。
以上詳細(xì)的api請(qǐng)點(diǎn)擊這里:
// app.js
const apps = [
{
name: "vue-micro-app",
entry: "http://localhost:8081",
container: "#micro-container",
activeRule: "#/vue2-micro-app",
},
];
export default apps;
app.js導(dǎo)出的是上面registerMicroApps的第一個(gè)參數(shù),是一個(gè)對(duì)象數(shù)組,其中數(shù)組每個(gè)字段的作用如下:
-
name:微應(yīng)用的名稱,后面改造微應(yīng)用的時(shí)候一定要與這個(gè)name對(duì)應(yīng) -
entry:微應(yīng)用運(yùn)行的域名加端口,我用的是本地8081端口 -
container:?jiǎn)?dòng)微應(yīng)用需要一個(gè)dom容器,里面就是這個(gè)dom容器的id,用class應(yīng)該也是可以的 -
activeRule:觸發(fā)啟動(dòng)微應(yīng)用的規(guī)則,當(dāng)檢測(cè)到url中含有activeRule的值時(shí),將啟動(dòng)微應(yīng)用
添加完上述兩個(gè)js后,我們回到main.js,目前的main.js應(yīng)該是這樣的
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import '@/assets/main.css'
createApp(App).use(store).use(router).mount('#app')
改造也非常簡(jiǎn)單,把上面micros中的index.js引入,然后運(yùn)行一下start函數(shù)就大功告成了
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import '@/assets/main.css'
import start from '@/micros'
createApp(App).use(store).use(router).mount('#app')
start()
刷新一下瀏覽器,發(fā)現(xiàn)主應(yīng)用和改造前并無(wú)差異!
主應(yīng)用添加微應(yīng)用容器和微應(yīng)用菜單
目前主應(yīng)用app的菜單代碼結(jié)構(gòu)如下
<div class="nav" v-if="token">
<div class="menu">
<router-link to="/">Parent Home</router-link>
</div>
<div class="menu">
<router-link to="/about">Parent About</router-link>
</div>
</div>
現(xiàn)在我們添加兩個(gè)菜單,分別對(duì)應(yīng)子應(yīng)用的home和about
<div class="nav" v-if="token">
<div class="menu">
<router-link to="/">Parent Home</router-link>
</div>
<div class="menu">
<router-link to="/about">Parent About</router-link>
</div>
<!--- 新添加 --->
<div class="menu">
<router-link to="/vue2-micro-app">Child Home</router-link>
</div>
<div class="menu">
<router-link to="/vue2-micro-app/about">Child About</router-link>
</div>
</div>
<div class="container">
<div class="header" v-if="token">Child Header</div>
<div class="router-view">
<router-view />
<!-- 新添加,微應(yīng)用的容器 -->
<div id="micro-container"></div>
</div>
</div>
相信你也發(fā)現(xiàn)了,to中多了上面app.js的activeRule字段中對(duì)應(yīng)的值(去掉了#號(hào)),因?yàn)?/vue2-micro-app正是觸發(fā)啟動(dòng)微應(yīng)用的條件
這是刷新我們的微應(yīng)用,然后點(diǎn)擊一下Child Home菜單,你會(huì)發(fā)現(xiàn)有兩個(gè)報(bào)錯(cuò)
第一個(gè)是跨域報(bào)錯(cuò),因?yàn)槲覀冎鲬?yīng)用運(yùn)行在8080端口,微應(yīng)用是8081端口,后面用nginx做一下代理就好
第二個(gè)報(bào)錯(cuò)就是源自于我們的微應(yīng)用還未改造,所以還等什么,趕緊改造微應(yīng)用
微應(yīng)用改造
官網(wǎng)寫了,微應(yīng)用入口必須導(dǎo)出 bootstrap、mount、unmount 三個(gè)生命周期鉤子,以供主應(yīng)用在適當(dāng)?shù)臅r(shí)機(jī)調(diào)用
這是微應(yīng)用改造前的main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import '@/assets/main.css'
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
下面我們來(lái)改造一下main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import '@/assets/main.css'
Vue.config.productionTip = false
// 新增:用于保存vue實(shí)例
let instance = null;
// 新增:動(dòng)態(tài)設(shè)置 webpack publicPath,防止資源加載出錯(cuò)
if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
/** * 新增: * 渲染函數(shù) * 兩種情況:主應(yīng)用生命周期鉤子中運(yùn)行 / 微應(yīng)用單獨(dú)啟動(dòng)時(shí)運(yùn)行 */
function render() {
// 掛載應(yīng)用
instance = new Vue({
router,
store,
render: (h) => h(App),
}).$mount("#micro-app");}
/**
* 新增:
* bootstrap 只會(huì)在微應(yīng)用初始化的時(shí)候調(diào)用一次,
下次微應(yīng)用重新進(jìn)入時(shí)會(huì)直接調(diào)用 mount 鉤子,不會(huì)再重復(fù)觸發(fā) bootstrap。
* 通常我們可以在這里做一些全局變量的初始化,比如不會(huì)在 unmount 階段被銷毀的應(yīng)用級(jí)別的緩存等。
*/
export async function bootstrap() {
console.log("VueMicroApp bootstraped");
}
/**
* 新增:
* 應(yīng)用每次進(jìn)入都會(huì)調(diào)用 mount 方法,通常我們?cè)谶@里觸發(fā)應(yīng)用的渲染方法
*/
export async function mount(props) {
console.log("VueMicroApp mount", props);
render(props);
}
/**
* 新增:
* 應(yīng)用每次 切出/卸載 會(huì)調(diào)用的方法,通常在這里我們會(huì)卸載微應(yīng)用的應(yīng)用實(shí)例
*/
export async function unmount() {
console.log("VueMicroApp unmount");
instance.$destroy();
instance = null;
}
// 新增:獨(dú)立運(yùn)行時(shí),直接掛載應(yīng)用
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
// 這是原本啟動(dòng)的代碼
// new Vue({
// router,
// store,
// render: h => h(App)
// }).$mount('#app')
?請(qǐng)注意,render方法中我把$mount后的參數(shù)改為了
?#micro-app,這是為了區(qū)分主應(yīng)用和微應(yīng)用中index.html的根id,所以微應(yīng)用中的public文件夾的index.html也要改為micro-app
然后還要對(duì)webpack配置進(jìn)行改造,微應(yīng)用根目錄添加vue.config.js文件
const path = require("path");
module.exports = {
devServer: {
// 監(jiān)聽端口
port: 8081,
// 關(guān)閉主機(jī)檢查,使微應(yīng)用可以被 fetch
disableHostCheck: true,
// 配置跨域請(qǐng)求頭,解決開發(fā)環(huán)境的跨域問(wèn)題
headers: {
"Access-Control-Allow-Origin": "*",
},
},
configureWebpack: {
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
},
},
output: {
// 微應(yīng)用的包名,這里與主應(yīng)用中注冊(cè)的微應(yīng)用名稱一致
library: "vue-micro-app",
// 將你的 library 暴露為所有的模塊定義下都可運(yùn)行的方式
libraryTarget: "umd",
// 按需加載相關(guān),設(shè)置為 webpackJsonp_VueMicroApp 即可
jsonpFunction: `webpackJsonp_vue-micro-app`,
},
},
};
然后還要改造一下我們的路由
if (window.__POWERED_BY_QIANKUN__) {
microPath = '/vue2-micro-app'
}
const routes = [
{
path: microPath + '/login',
name: 'login',
component: Login
},
{
path: microPath + '/',
redirect: microPath + '/home'
},
{
path: microPath + '/home',
name: 'Home',
component: Home
},
{
path: microPath + '/about',
name: 'About',
component: () => import( /* webpackChunkName: "about" */ '../views/About.vue')
}
]
router.beforeEach((to, from, next) => {
if (to.path !== (microPath + '/login')) {
if (store.state.token) {
next()
} else {
next(microPath + '/login')
}
}
else {
next()
}
})
?路由主要的改動(dòng)就是每個(gè)
?path都添加了一個(gè)microPath變量,用于檢測(cè)是否由微前端改動(dòng),相應(yīng)的路由守衛(wèi)也要添加microPath變量,另外微應(yīng)用的login跳轉(zhuǎn)的時(shí)候也要加上microPath判斷
最后重啟一下我們的微應(yīng)用,再去我們的主應(yīng)用點(diǎn)擊一下Child Home菜單,如無(wú)意外你就會(huì)得到和我下面截圖一樣的界面
沒(méi)錯(cuò),你已經(jīng)成功了!vue2的項(xiàng)目已經(jīng)成功嵌入到vue3中去了
但是,細(xì)心的你也發(fā)現(xiàn)了,我已經(jīng)登錄了一次了,為什么又要登錄一次呀,所以,接下來(lái)我們要利用通信去解決掉這個(gè)問(wèn)題。
主應(yīng)用和微應(yīng)用通信
應(yīng)用間的通信,我們要利用qiankun框架的initGlobalState和MicroAppStateActions api,相關(guān)的api介紹如下:
setGlobalState:設(shè)置 globalState - 設(shè)置新的值時(shí),內(nèi)部將執(zhí)行淺檢查,如果檢查到globalState發(fā)生改變則觸發(fā)通知,通知到所有的觀察者函數(shù)。
onGlobalStateChange:注冊(cè)觀察者函數(shù) - 響應(yīng)globalState變化,在globalState發(fā)生改變時(shí)觸發(fā)該觀察者函數(shù)。
offGlobalStateChange:取消觀察者函數(shù) - 該實(shí)例不再響應(yīng)globalState變化。
所以我們?cè)俅胃脑煲幌聝蓚€(gè)項(xiàng)目,首先是主應(yīng)用的micros/index.js
import {
registerMicroApps,
addGlobalUncaughtErrorHandler,
start,
initGlobalState // 新增
} from "qiankun";
const state = {}
const actions = initGlobalState(state);
export { actions }
以上新增了并導(dǎo)出了actions,然后去到login.vue
import { actions } from "@/micros"; //新增
const login = () => {
if (username.value && password.value) {
store.commit("setToken", "123456");
// 新增
actions.setGlobalState({globalToken: "123456"});
router.push({path: "/"});
}
};
引入actions并新增了actions.setGlobalState方法
然后是子應(yīng)用的main.js
function render(props) {
console.log("子應(yīng)用render的參數(shù)", props)
// 新增
props.onGlobalStateChange((state, prevState) => {
// state: 變更后的狀態(tài); prev 變更前的狀態(tài)
console.log("通信狀態(tài)發(fā)生改變:", state, prevState);
// 這里監(jiān)聽到globalToken變化再更新store
store.commit('setToken', '123456') }, true);
// 掛載應(yīng)用
instance = new Vue({
router,
store,
render: (h) => h(App),
}).$mount("#micro-app");}
在render方法中我們加上onGlobalStateChange,并且第二位參數(shù)置為true,這樣微應(yīng)用一啟動(dòng)的時(shí)候,我們馬上就可以看到剛剛設(shè)置的globalToken:123456
好了已經(jīng)改造完畢,我們刷新重新登錄主應(yīng)用然后點(diǎn)擊微應(yīng)用的菜單,可以看到微應(yīng)用不需要再登錄了,如下圖:
好像還是有點(diǎn)問(wèn)題喔,微應(yīng)用的菜單怎么展示出來(lái)了???
別怕,最后一步,留給親愛的你去解決吧,思路就是在微應(yīng)用中利用window.__POWERED_BY_QIANKUN__去判斷是否通過(guò)qiankun啟動(dòng)的,是的話我們寫個(gè)變量使用v-if將微應(yīng)用的菜單和頭部隱藏,不就完事了?
?以上就是
?qiankun框架實(shí)戰(zhàn)的第一篇本地開發(fā)的全部?jī)?nèi)容,總體結(jié)構(gòu)上跟我做的項(xiàng)目遷移很相似了,其它還有些小細(xì)節(jié)不影響,其實(shí)本章有一個(gè)巨坑,下一篇將帶大家部署打包后的項(xiàng)目,并告訴大家這個(gè)巨坑在哪里。

“分享、點(diǎn)贊、在看” 支持一波 
