微前端使用: qiankun
一、qiankun使用場景
1. 簡介
qiankun是在single-spa的基礎上實現的,可以保證各個項目獨立使用,也可以集成使用。各系統之間不受技術棧的限制,集成使用也能保證各樣式和全局變量的隔離。模塊的插拔式使用,當公司項目集是一個大系統下包含多個子系統或者模塊時,可以采用這種方式動態部署各個系統。亦或者是老項目技術升級和重構,可以通過qiankun按模塊進行改造,避免對整個系統產生較大的影響。功能和iframe類似,但是由于iframe數據通信難度較大,且有安全和SEO的問題,所以iframe使用體驗不佳。
2. 原理邏輯:
a. 需要在各個子應用的基礎上新增一個主應用,通過主應用監聽路由變化。
b. 當有路由切換時就會觸發上述監聽函數從而去匹配在主應用中注冊的各個子應用路徑(activeRule)是否匹配。
c. 匹配到子應用后就會加載子應用的資源到對應的容器當中去。
二、實現樣例
本樣例使用的是Node 16的版本,主應用采用Vue3框架,兩個子應用分別使用Vue2和Vue3框架。qiankun版本是2.10.16。
1. 搭建主應用
利用腳手架創建一個qiankun-main的主應用,同時安裝qiankun組件(qiankun只需要在主應用安裝,子應用不需要),其中代碼中標注重點的內容是配置qiankun的關鍵步驟
1.1 打開vue.config.js文件,添加跨域處理,避免跳轉時出現跨域問題
// vue.config.js const { defineConfig } = require('@vue/cli-service') module.exports = defineConfig({ transpileDependencies: true, devServer: { port: 8085, headers: { // 重點1: 允許跨域訪問子應用頁面 'Access-Control-Allow-Origin': '*', } } })
1.2 主應用中設置子應用接收容器
// App.vue <template> <div class="app"> <p><router-link to="/">點擊跳轉到父頁面</router-link></p> <button @click="login">登陸</button> <p><router-link to="/vue3">跳轉到Vue3子應用</router-link></p> <p><router-link to="/vue2">跳轉到Vue2子應用</router-link></p> <router-view /> <div id="vue3"></div> <!-- 重點2:子應用容器 id --> <div id="vue2"></div> <!-- 重點2:子應用容器 id --> </div> </template> <script> import actions from '@/shared/actions'; export default { name: 'App', components: { }, mounted() { actions.onGlobalStateChange((state, prevState) => { // state: 變更后的狀態; prevState: 變更前的狀態 console.log('主應用觀察者:token值改為', prevState.token); console.log("主應用觀察者:登錄狀態發生改變,改變后的 token 的值為 ", state.token); }); }, methods: { login() { console.log('進入登陸事件'); setTimeout(() => { const token = 'token_' + Math.floor(Math.random() * 100000); //登陸后隨機生成token并設置 actions.setGlobalState({ token }); this.$router.push("/vue3"); }, 300); } } } </script> <style> #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } </style>
1.3 在src根目錄下新增public-path文件;同時改造路由,設置返回的base地址
// public-path.js if (window.__POWERED_BY_QIANKUN__) { // eslint-disable-next-line no-undef __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ } // router/index.js import { createRouter, createWebHashHistory } from 'vue-router' import '../public-path' // 重點3: 引入public-path文件 const router = createRouter({ base: window.__POWERED_BY_QIANKUN__ ? '/vue3' : '/', // 重點4:qiankun進入子應用時,返回true history: createWebHashHistory(), // 重點5 routes: [{ path: '/', redirect: '/child' }, { path: '/child', component: () => import('@/components/child') } ] }) export default router router/index.js
1.4 注冊和引入子應用
// main.js import { createApp } from 'vue' import App from './App.vue' import router from './router' import { registerMicroApps, start, setDefaultMountApp } from 'qiankun' createApp(App).use(router).mount('#app') registerMicroApps([ { name: "vue3 app", entry: "//localhost:8086", // 重點8:對應重點6 container: '#vue3', // 重點9:對應重點2 activeRule: '/#/vue3', // 重點10:對應重點4 props: { appContent: '我是主應用傳給vue的值' } }, { name: "vue2 app", entry: "//localhost:8087", // 重點8:對應重點6 container: '#vue2', // 重點9:對應重點2 activeRule: '/#/vue2', // 重點10:對應重點4 props: { appContent: '我是主應用傳給Vue2的值' } } ]) setDefaultMountApp("/") // 重點11:啟動默認的子模塊 // 啟動 start()
2. 搭建子應用1
同樣利用腳手架創建一個qiankun-vue3-child,項目使用Vue3作為基礎框架
2.1 同樣在src目錄下創建public-path.js文件
// public-path.js if (window.__POWERED_BY_QIANKUN__) { // eslint-disable-next-line no-undef __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ }
2.2 改造router/index.js文件, 確認在qiankun模式下的路由基礎路徑
// router/index.js import { createRouter, createWebHashHistory } from 'vue-router' import '../public-path' // 重點3: 引入public-path文件 const router = createRouter({ base: window.__POWERED_BY_QIANKUN__ ? '/vue3' : '/', // 重點4:qiankun進入子應用時,返回true history: createWebHashHistory(), // 重點5 routes: [ ] }) export default router
2.3 修改入口函數main.js,導入鉤子函數
// main.js import { createApp } from 'vue' import App from './App.vue' import router from './router' import actions from './micros/actions' let instance = null function render(props = {}) { // qiankun模式下實現父子應用之間通信 if (props) { actions.setActions(props); } const { container } = props // 為了避免根id#app與其他DOM沖突,需要限制查找范圍 instance = createApp(App).use(router).mount(container ? container.querySelector('#child-app') : '#child-app') } if (!window.__POWERED_BY_QIANKUN__) { render() } //--------- 生命周期函數------------// export async function bootstrap() { console.log('[vue] vue app bootstraped') } export async function mount(props) { console.log('[vue] props from main framework', props) render(props) } export async function unmount() { if (instance) { console.log(instance, instance.unmount); // instance.unmount(); instance = null } } // createApp(App).use(router).mount('#child-app')
2.4 修改打包配置文件vue.config.js,設置服務端口以及打包模式
// vue.config.js const { defineConfig } = require('@vue/cli-service') const { name } = require('./package'); module.exports = defineConfig({ transpileDependencies: true, devServer: { port: 8086, // 重點6 headers: { // 重點7:同重點1,允許子應用跨域 'Access-Control-Allow-Origin': '*', }, }, // 自定義webpack配置 configureWebpack: { output: { library: `${name}-[name]`, libraryTarget: 'umd', // 把子應用打包成 umd 庫格式 // jsonpFunction: `webpackJsonp_${name}`, }, }, })
3. 搭建子應用2
步驟與第2步類似,只是使用Vue2作為基礎框架
三、功能演示
四、常見問題
1. 子應用部署在同一個服務器同一個端口的不同路徑下如何配置?
基本和部署在不同服務器的類似,只是將注冊子應用的entry的服務器端口號換成某個路徑,同時將打包的publicPath改為該路徑
// 同服務器同端口部署配置 // 主應用入口文件中注冊子應用 registerMicroApps([ { name: "vue3_app", entry: "/entry_vue3", // 對應之前的 //localhost:8086 container: '#vue3', activeRule: '/#/vue3', props: { appContent: '我是主應用傳給vue的值' } } ]) // 子應用的 router/indexedDB.js const router = createRouter({ base: window.__POWERED_BY_QIANKUN__ ? '/vue3' : '/entry_vue3', // 設置路由路徑 history: createWebHashHistory(), routes: [ ] }) // 打包文件vue.config.js中添加默認路徑 module.exports = defineConfig({ publicPath: devFlag ? '/' : '/entry_vue3', transpileDependencies: true, devServer: { port: 8087, // 重點6 headers: { // 重點7:同重點1,允許子應用跨域 'Access-Control-Allow-Origin': '*', }, }, // 自定義webpack配置 重點12 configureWebpack: { output: { library: `${name}-[name]`, libraryTarget: 'umd', // 把子應用打包成 umd 庫格式 // jsonpFunction: `webpackJsonp_${name}`, }, }, })
2. 主子應用之間通信?
2.1 使用qiankun框架提供的 initGlobalState 實現的,主要有下面三個函數:
onGlobalStateChange(callback, Immediately)在當前應用監聽全局狀態變化;
setGlobalState(state)按照一級屬性進行狀態設置,微應用只能修改一級屬性;
offGlobalStateChange()移除當前的狀態監聽,微應用在unmount時默認調用;
2.2 使用方式,效果可見上面的案列圖中對token的打印信息
a. 主應用的src目錄下新增 shared/actions.js 文件。
// actions.js import { initGlobalState } from "qiankun"; const initialState = { token: 'no token' }; const actions = initGlobalState(initialState); export default actions;
b. 比如在主應用的App.vue中使用并且實現登陸后生成token以及跳轉到vue3子應用
// App.vue import actions from '@/shared/actions'; export default { name: 'App', components: { }, mounted() { actions.onGlobalStateChange((state, prevState) => { // state: 變更后的狀態; prevState: 變更前的狀態 console.log('主應用觀察者:token值改為', prevState.token); console.log("主應用觀察者:登錄狀態發生改變,改變后的 token 的值為 ", state.token); }); }, methods: { login() { console.log('進入登陸事件'); setTimeout(() => { const token = 'token_' + Math.floor(Math.random() * 100000); //登陸后隨機生成token并設置 actions.setGlobalState({ token }); this.$router.push("/vue3"); }, 300); } } }
c. 子應用中使用時首先在根目錄下創建一個micros/actions.js文件
// actions.js function emptyAction() { // 確保單獨部署時不會報錯 console.warn('當前無可執行的Action'); } class Actions { // 默認設置空Action actions = { onGlobalStateChange: emptyAction, setGlobalState: emptyAction } // 設置Actions setActions(actions) { this.actions = actions; } // 映射 onGlobalStateChange(...args) { return this.actions.onGlobalStateChange(...args); } // 映射 setGlobalState(...args) { return this.actions.setGlobalState(...args); } } const actions = new Actions(); export default actions;
d. 子應用的APP.vue頁面中監聽主應用中數據的變化以及子應用主動修改數據觀察主應用是否能接收到
// App.vue import actions from '@/micros/actions.js'; export default { name: 'App', components: { }, mounted() { actions.onGlobalStateChange(state => { console.log('子應用Vue的觀察函數:', state); }, true) }, methods: { changeToken() { actions.setGlobalState({ token: 'Vue3_' + Math.floor(Math.random() * 100000) }) } } }
3. 各個應用之間如何提取一些公共的資源或者模塊?
可以將公共模塊提取成一個公共組件發布到npm,然后由各個應用按需安裝。
4. 各個系統如何做到只登陸一次?
可以參考單點登陸實現,簡單邏輯就是比如:
a. 有一個地址sso.com做為控制中心,然后a.com、b.com為子模塊。
b. 當訪問a.com時無權限時路由會攜帶參數“a.com”自動跳轉到登陸頁面,輸入用戶名密碼信息后,經過sso.com驗證通過生成ticket并返回給頁面同時跳轉到a.com并下發ticket。
c. a.com請求獲取到ticket后訪問sso.com的服務器進行驗證是否有效,有效則允許登陸,這樣就完成了一次登陸。
d. 如果在已登錄的狀態下跳轉到b.com,則省略第二步的登陸驗證直接將ticket攜帶到b.com,然后再訪問sso.com進行有消息驗證。
注意:主應用注冊的activeRule為/vue3時跳轉到子應用不生效可能是因為瀏覽器路由跳轉時自動加上/#/,所以在activeRule也需要修改為/#/vue3才可以跳轉