微前端框架qiankun項目實戰(zhàn)(二)--踩坑與部署篇
回復1,加入高級 Node 進階交流群

作者:黑化程序員(作者授權轉載)
鏈接:https://juejin.cn/post/6973111766767108103
大家好,我是小黑。
在上一篇《微前端框架qiankun項目實戰(zhàn)(一)--本地開發(fā)篇》發(fā)布后,感謝有網友提出了微應用的緩存問題,的確基于第一篇使用的registerMicroApps方式很難做到緩存,要做到應用緩存的方式使用手動加載管理微應用的方式是最好的,我將再寫一篇補充篇使用loadMicroApp手動管理微應用,本篇我會模擬部署一下主應用和微應用,并將揭開我上一篇所謂的巨坑是什么。
貼上我建好的模板倉庫地址
vue3模板:https://gitee.com/jimpp/vue3-main-app
vue2模板:https://gitee.com/jimpp/vue2-micro-app
在上一篇中,master分支都是未改造前能獨立運行的項目,dev分支是最終改造后的項目,本篇所有代碼會在新建的test分支修改
隱藏微應用菜單和頭部
在上篇的結尾,我們本地運行微前端的時候,發(fā)現微應用的菜單和頭部還是渲染出來了

不知道親愛的你是否有思路如何實現隱藏,下面給出我的思路代碼
// template
<div class="nav" v-if="showMenu">
<div class="menu">
<router-link to="/">Child Home</router-link>
</div>
<div class="menu">
<router-link to="/about">Child About</router-link>
</div>
</div>
<div class="container">
<div class="header" v-if="showHeader">Child Header</div>
<div class="router-view">
<router-view />
</div>
</div>
// js
computed: {
...mapState(["token"]),
// 控制菜單顯示隱藏
showMenu() {
return this.token && !this.isMicroEnc
},
// 控制頭部顯示隱藏
showHeader() {
return this.token && !this.isMicroEnc
},
isMicroEnc() {
return window.__POWERED_BY_QIANKUN__
}
}
利用computed根據token 和 window.POWERED_BY_QIANKUN 去控制顯示隱藏,效果如下

token放進本地緩存
這個過程中我們要不斷地修改項目,一刷新就要重新登錄實在太煩了,下面我們改造一下主應用,把登錄后的token存到localStorage中
在src/store/index.js中
mutations: {
setToken(state, token) {
state.token = token
// 新增,登錄的時候同時把token存到localStorage
localStorage.setItem('token', token)
}
},
// 新增
const storagePlugin = store => {
const token = localStorage.getItem('token')
if(token) {
store.commit('setToken', token)
}
}
plugins: [storagePlugin]
這里在setToken方法中添加了把token存到localStorage的邏輯,并編寫了一個Vuex的storagePlugin插件,該插件主要功能是在應用加載的時候去獲取localStorage中的token,如果有的話直接commit到我們的store中,這樣一來我們只要登錄了,再刷新也不需要重新登錄
接下來,準備開始踩坑了
坑1:樣式沖突問題
首先遇到的樣式沖突,不是什么ui庫的沖突,而是iconfont的沖突,我是在改造兩個線上項目的時候遇到的
首先去iconfont官網為兩個應用添加兩組圖標
主應用的圖標

微應用的圖標

可以看到兩個應用的圖標命名是一致的,不過主應用是空心的,微應用是實心的
下載好的圖標庫是這樣的

我們只需要拷貝iconfont.css、iconfont.ttf、iconfont.woff、iconfont.woff2這幾個文件到src/assets目錄下,然后在main.css引入就可以了
iconfont.css的代碼如下
@font-face {
font-family: "iconfont"; /* Project id 2608947 */
src: url('iconfont.woff2?t=1623503003854') format('woff2'),
url('iconfont.woff?t=1623503003854') format('woff'),
url('iconfont.ttf?t=1623503003854') format('truetype');
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-password:before {
content: "\ea41";
}
.icon-username:before {
content: "\e600";
}
main.css中引入
@import url(./iconfont.css);
兩個項目的引入方式是一樣的,最后的目錄結構如下:

然后再分別去到兩個應用的views/Home.vue中添加兩個圖標
<i class="iconfont icon-username"></i>
<i class="iconfont icon-password"></i>
刷新我們的瀏覽器

可以看到,當點擊菜單切換時,都是空心圖標,這明顯有問題啊!我們明明一個有心有個無心!
如何解決?
當時在改造項目的過程中發(fā)現這個情況真的有點炸毛(fxxx = fine),不知道你是否有疑問,我為什么要把iconfont.css的代碼貼出來,因為我們解決這個問題的關鍵就在于
font-family: "iconfont";
大家可以看到兩個項目的iconfont.css都有這么一句話,然后引入的方式都是class="iconfont icon-xxx"的方式,我改造的項目也是如此,我猜測上面的問題跟這個有很大的關系,事實證明了我猜想是對的,下面我們來改造一下
首先回到iconfont的官網,去到我們剛剛添加的圖標庫頁面,有個項目設置選項,點擊后會看到如下兩個選項

沒錯,解決沖突的關鍵就是為兩個項目添加不同的引用前綴和font-family,主應用前綴改為main-app-icon-,font-family改為main-app-iconfont,微應用相應改為micro-app-icon-和micro-app-iconfont
然后重新下載兩個圖標庫并重新引入,目前兩個iconfont.css的關鍵代碼如下
// 主應用的iconfont.css
@font-face {
font-family: "main-app-iconfont"; /* Project id 2608947 */
src: url('iconfont.woff2?t=1623508357834') format('woff2'),
url('iconfont.woff?t=1623508357834') format('woff'),
url('iconfont.ttf?t=1623508357834') format('truetype');
}
.main-app-iconfont {
font-family: "main-app-iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
// 微應用的iconfont.css
@font-face {
font-family: "micro-app-iconfont"; /* Project id 2608945 */
src: url('iconfont.woff2?t=1623508587683') format('woff2'),
url('iconfont.woff?t=1623508587683') format('woff'),
url('iconfont.ttf?t=1623508587683') format('truetype');
}
.micro-app-iconfont {
font-family: "micro-app-iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
相應的我們引入圖標的方式也要改
// 主應用中
<i class="main-app-iconfont main-app-icon-username"></i>
<i class="main-app-iconfont main-app-icon-password"></i>
// 微應用中
<i class="micro-app-iconfont micro-app-icon-username"></i>
<i class="micro-app-iconfont micro-app-icon-password"></i>
改造完畢后刷新瀏覽器

可以看到,樣式沖突的問題已經解決了
為什么會出現這個這個問題?
官方提供了基于shadowDom的樣式隔離方案,不過似乎還是未做到完全的隔離,同類名的情況下可能還是會出現沖突,所以我們盡量通過不同類名,添加前綴的方式去避免樣式沖突,或者是把類名降級放到一個父類中去避免樣式沖突
什么意思呢?例如主微應用都有類名aaa,那么就可能會出現沖突 但是如果我們主應用改成這樣 .main-app > .aaa,微應用改成這樣.micro-app > .aaa,把原本處于根的aaa樣式用容器包裝起來,就可以避免樣式沖突,解決ui庫樣式沖突的方式也是這種思路,可以參考一下這篇文章
部署微前端
處理完樣式問題啦,貌似沒什么問題了,來打包部署一下吧
部署前的改造
還記得主應用micros/app.js如下:
const apps = [
/**
* name: 微應用名稱 - 具有唯一性
* entry: 微應用入口 - 通過該地址加載微應用
* container: 微應用掛載節(jié)點 - 微應用加載完成后將掛載在該節(jié)點上
* activeRule: 微應用觸發(fā)的路由規(guī)則 - 觸發(fā)路由規(guī)則后將加載該微應用
*/
{
name: "vue_micro_app",
entry: "http://localhost:8081",
container: "#micro-container",
activeRule: "#/vue2-micro-app",
},
];
export default apps;
目前entry是寫死的,我們可以部署的時候改,但是改來改去太麻煩啦,有沒有更好的方法
if(process.env.NODE_ENV === 'development') {
}else {
}
還記得這種判斷環(huán)境的代碼嗎,這里我們不用那么麻煩,vue-cli幫我們做好了,我們在根目錄添加.env.production、.env.development文件,這兩個文件就是用來導出一些變量,顧名思義這些變量分別用在dev和pro環(huán)境下的,具體可以點擊這里了解
在.env.development中添加
VUE_APP_MICRO_ENTRY="http://localhost:8081"
至于.env.production中就添加服務器的域名就可以啦
VUE_APP_MICRO_ENTRY="你的服務器域名"
這里我正式環(huán)境用的是localhost:3001,稍后我會建本地服務器在3001端口部署微應用,3000端口部署主應用
這里文件中的變量一定要以VUE_APP_ 開頭,否則是無效的
相應的app.js要改成如下格式:
// 新增
const { VUE_APP_MICRO_ENTRY } = process.env
const apps = [
/**
* name: 微應用名稱 - 具有唯一性
* entry: 微應用入口 - 通過該地址加載微應用
* container: 微應用掛載節(jié)點 - 微應用加載完成后將掛載在該節(jié)點上
* activeRule: 微應用觸發(fā)的路由規(guī)則 - 觸發(fā)路由規(guī)則后將加載該微應用
*/
{
name: "vue_micro_app",
entry: VUE_APP_MICRO_ENTRY, // 修改
container: "#micro-container",
activeRule: "#/vue2-micro-app",
},
];
export default apps;
然后重啟一下主應用,以后打包或者本地開發(fā)都不用再修改app.js啦
開始部署
接下來執(zhí)行npm run build 或者 yarn run build分別打包兩個項目
然后可以新建一個項目名為mock-server,npm init 初始化一下后執(zhí)行npm install koa 和 npm install koa-static,并添加兩個文件夾mian-app和 micro-app,分別把打包后的主應用和微應用放進這兩個文件夾,再新建main-server.js和micro-server.js
這時mock-server的目錄結構如下

然后為main-server.js和micro-server.js添加如下代碼
// main-server.js
const Koa = require('koa')
const path = require('path')
const app = new Koa()
const staticFiles = require('koa-static')
const staticPath = path.join(__dirname + '/main-app')
app.use(staticFiles(staticPath))
app.listen(3000, () => {
console.log('main server running at 3000')
})
--------------
// micro-server.js
const Koa = require('koa')
const path = require('path')
const app = new Koa()
const staticFiles = require('koa-static')
const staticPath = path.join(__dirname + '/micro-app')
app.use(staticFiles(staticPath))
app.listen(3001, () => {
console.log('main server running at 3001')
})
代碼主要就是把打包出來的文件夾用koa分別在3000和3001端口跑起來,沒什么特別的
然后訪問一下,主應用正常運行,微應用報錯了

上篇在微應用render函數中有這么一段代碼:
function render(props) {
console.log("子應用render的參數", props)
// ----看這里----
props.onGlobalStateChange((state, prevState) => {
// state: 變更后的狀態(tài); prev 變更前的狀態(tài)
console.log("通信狀態(tài)發(fā)生改變:", state, prevState);
store.commit('setToken', '123456')
}, true);
// 掛載應用
instance = new Vue({
router,
store,
render: (h) => h(App),
}).$mount("#micro-app");
}
沒錯,當子應用獨立運行時,props是沒有onGlobalStateChange參數的,所以這里要添加判斷(添加的判斷還真不少的說),改成下面這個樣子:
function render(props) {
console.log("子應用render的參數", props)
// 新增判斷,如果是獨立運行不執(zhí)行onGlobalStateChange
if(window.__POWERED_BY_QIANKUN__) {
props.onGlobalStateChange((state, prevState) => {
// state: 變更后的狀態(tài); prev 變更前的狀態(tài)
console.log("通信狀態(tài)發(fā)生改變:", state, prevState);
store.commit('setToken', '123456')
}, true);
}
// 掛載應用
instance = new Vue({
router,
store,
render: (h) => h(App),
}).$mount("#micro-app");
}
重新build并放到mock-server中重新運行3001端口,刷新后可以看到微應用運行成功
跨域問題
當從主應用切換到微應用時


沒錯,經典的跨域問題,因為部署的是本地,有兩個解決辦法
第一個(不推薦)是作弊的方法
新建一個chrome瀏覽器的快捷方式,然后右鍵,屬性

在目標這一欄, --user-data-dir=E:\MyChromeDevUserData到末尾,注意--user前有空格,然后用這個新建的快捷方式可以訪問部署后的應用
第二種,使用koa2-cors
在mock-server中執(zhí)行npm install koa2-cors,然后修改一下micro-server.js
const Koa = require('koa')
const path = require('path')
const app = new Koa()
const staticFiles = require('koa-static')
const cors = require('koa2-cors'); // 新增
const staticPath = path.join(__dirname + '/micro-app')
app.use(cors());// 新增
app.use(staticFiles(staticPath))
app.listen(3001, () => {
console.log('main server running at 3001')
})
重啟micro-server.js并刷新瀏覽器,可以看到切換菜單已經正常啦
第三種,利用nginx做代理(建議)
貼上nginx.conf
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
# 監(jiān)聽的端口
listen 3001;
server_name localhost;
location / {
#允許跨域訪問
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
if ($request_method = 'OPTIONS') {
return 204;
}
# 代理的文件夾
root E:\project\vue-project\vue2-micro-app\dist;
autoindex on;
}
}
}
使用nginx后,我們的micro-app和micro-server.js已經不需要了,因為nginx已經做了代理,允許nginx,刷新瀏覽器,可以看到切換菜單已經正常啦
坑2:頁面無法跳轉問題
這個問題就是我上一節(jié)所說的巨坑,因為這個頁面無法跳轉,在本地是沒有任何問題的!然而部署到測試環(huán)境后,100%復現,本地環(huán)境100%沒問題,你看一步步走到現在也沒發(fā)現這個問題,這就是程序員經典場景----我本機是好的呀o(╥﹏╥)o
注意,即使是使用nginx代理后在本地部署依然無法在本地復現這個問題,我會配合gif圖來還原這個問題
場景還原(以下全部假設運行在測試服務器)
本地也部署跑過感覺沒問題了,開開心心部署到測試服務器,然后一訪問,瞬間傻眼了

為什么會這樣呀??可以看到無論是本地還是測試服務器都是沒有任何報錯的,然后這個問題我搞了幾乎3天
如何解決?
到了第三天的時候,我差不多想放棄微前端改造方案了,突然我發(fā)現,我們點擊菜單的時候,url是有變化的,但是頁面沒有跳轉,所以我又大膽猜測,是不是路由的問題,而且可以看到,每次我們在主微應用之間切換的時候,都會執(zhí)行微應用main.js中導出的mount和unmount函數,然后注意到unmount有這么一段代碼
export async function unmount() {
console.log("VueMicroApp unmount");
// 注意這里
instance.$destroy();
instance = null;
}
而微應用的router的index.js是這樣的

微應用main.js中的render函數是這樣的

可以看到,由始至終,router都是同一個實例!然后每次unmount都會執(zhí)行應用卸載,會不會就是這個問題導致的呢
接下來改造微應用的router.js,不再導出router而是導出routes數組

然后改造main.js
import VueRouter from 'vue-router'
import routes from './router'
Vue.use(VueRouter)
// 新增:用于保存router實例
let router = null;
let microPath = ''
// 新增:動態(tài)設置 webpack publicPath,防止資源加載出錯
if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
microPath = '/vue2-micro-app'
}
function render(props) {
console.log("子應用render的參數", props)
if(window.__POWERED_BY_QIANKUN__) {
props.onGlobalStateChange((state, prevState) => {
// state: 變更后的狀態(tài); prev 變更前的狀態(tài)
console.log("通信狀態(tài)發(fā)生改變:", state, prevState);
store.commit('setToken', '123456')
}, true);
}
// 新增
router = new VueRouter({
routes
})
// 新增
router.beforeEach((to, from, next) => {
if (to.path !== (microPath + '/login')) {
if (store.state.token) {
next()
} else {
next(microPath + '/login')
}
} else {
next()
}
})
// 掛載應用
instance = new Vue({
router,
store,
render: (h) => h(App),
}).$mount("#micro-app");
}
export async function unmount() {
console.log("VueMicroApp unmount");
instance.$destroy();
instance = null;
// 新增
router = null;
}
修改后的main.js,router不再是同一個實例,而是每次mount的時候都會新獲取一個實例,相應的路由守衛(wèi)也要搬遷出來,然后npm run serve看到本地運行微應用沒問題,好npm run build重新打包并重新運行nginx

可以看到,這次部署是真的成功了
PS:在vue3中如果直接監(jiān)聽整個route對象,也會出現頁面無法跳轉的情況
歡迎指出不足和交流,踩坑不易,如果對你有幫助的話,點個贊吧~(#^.^#)
參考文獻
明源云的qiankun教程:https://github.com/a1029563229/blogs/blob/master/BestPractices/qiankun/Communication.md
qinkun官網:https://qiankun.umijs.org/zh/api#initglobalstatestate


“分享、點贊、在看” 支持一波 
