一、核心設(shè)計(jì)理念
- 簡(jiǎn)單
由于主應(yīng)用微應(yīng)用都能做到技術(shù)棧無(wú)關(guān),qiankun 對(duì)于用戶而言只是一個(gè)類似 jQuery 的庫(kù),你需要調(diào)用幾個(gè) qiankun 的 API 即可完成應(yīng)用的微前端改造。同時(shí)由于 qiankun 的 HTML entry 及沙箱的設(shè)計(jì),使得微應(yīng)用的接入像使用 iframe 一樣簡(jiǎn)單。
- 解耦/技術(shù)棧無(wú)關(guān)
微前端的核心目標(biāo)是將巨石應(yīng)用拆解成若干可以自治的松耦合微應(yīng)用,而 qiankun 的諸多設(shè)計(jì)均是秉持這一原則,如 HTML entry、沙箱、應(yīng)用間通信等。這樣才能確保微應(yīng)用真正具備 獨(dú)立開(kāi)發(fā)、獨(dú)立運(yùn)行 的能力。
二、快速上手
- 安裝 qiankun
yarn add qiankun # 或者 npm i qiankun -S
2.在主應(yīng)用中注冊(cè)微應(yīng)用
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'react app', // app name registered
entry: '//localhost:7100',
container: '#yourContainer',
activeRule: '/yourActiveRule',
},
{
name: 'vue app',
entry: { scripts: ['//localhost:7100/main.js'] },
container: '#yourContainer2',
activeRule: '/yourActiveRule2',
},
]);
start();
當(dāng)微應(yīng)用信息注冊(cè)完之后,一旦瀏覽器的 url 發(fā)生變化,便會(huì)自動(dòng)觸發(fā) qiankun 的匹配邏輯,所有 activeRule 規(guī)則匹配上的微應(yīng)用就會(huì)被插入到指定的 container 中,同時(shí)依次調(diào)用微應(yīng)用暴露出的生命周期鉤子。
如果微應(yīng)用不是直接跟路由關(guān)聯(lián)的時(shí)候,你也可以選擇手動(dòng)加載微應(yīng)用的方式:
import { loadMicroApp } from 'qiankun';
loadMicroApp({
name: 'app',
entry: '//localhost:7100',
container: '#yourContainer',
});
3.微應(yīng)用配置
微應(yīng)用分為有 webpack 構(gòu)建和無(wú) webpack 構(gòu)建項(xiàng)目,有 webpack 的微應(yīng)用(主要是指 Vue、React、Angular)需要做的事情有:
新增 public-path.js 文件,用于修改運(yùn)行時(shí)的 publicPath。
注意:運(yùn)行時(shí)的 publicPath 和構(gòu)建時(shí)的 publicPath 是不同的,兩者不能等價(jià)替代。
微應(yīng)用建議使用 history 模式的路由,需要設(shè)置路由 base,值和它的 activeRule 是一樣的。
在入口文件最頂部引入 public-path.js,修改并導(dǎo)出三個(gè)生命周期函數(shù)。
修改 webpack 打包,允許開(kāi)發(fā)環(huán)境跨域和 umd 打包。
主要的修改就是以上四個(gè),可能會(huì)根據(jù)項(xiàng)目的不同情況而改變。例如,你的項(xiàng)目是 index.html 和其他的所有文件分開(kāi)部署的,說(shuō)明你們已經(jīng)將構(gòu)建時(shí)的 publicPath 設(shè)置為了完整路徑,則不用修改運(yùn)行時(shí)的 publicPath (第一步操作可?。?。
無(wú) webpack 構(gòu)建的微應(yīng)用直接將 lifecycles 掛載到 window 上即可。
此處以vue為例:
1.在src目錄新增 public-path.js:
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
2.入口文件 main.js 修改,為了避免根 id #app 與其他的 DOM 沖突,需要限制查找范圍。
let instance = null
function render (props = {}) {
const { container } = props
instance = new Vue({
router,
store,
render: (h) => h(App)
}).$mount(container ? container.querySelector('#app') : '#app')
}
// 獨(dú)立運(yùn)行時(shí)
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)
props.onGlobalStateChange((state, prev) => {
// state: 變更后的狀態(tài); prev 變更前的狀態(tài)
console.log('微應(yīng)用獲取主應(yīng)用狀態(tài)', state, prev)
if (state && state.eicloud) {
store.dispatch('setMicroAppLogin', state.eicloud)
} else {
store.dispatch('clearStorage')
}
}, true)
render(props)
}
export async function unmount () {
store.dispatch('clearStorage')
instance.$destroy()
instance.$el.innerHTML = ''
instance = null
}
三、qiankun微應(yīng)用keep-alive實(shí)現(xiàn)方案
背景:qiankun提供了兩種加載子應(yīng)用的方式 registerMicroApps和loadMicroApp。官方推薦的是registerMicroApps路由匹配激活子應(yīng)用,當(dāng)瀏覽器 url 發(fā)生變化時(shí),會(huì)自動(dòng)檢查每一個(gè)微應(yīng)用注冊(cè)的 activeRule 規(guī)則,符合規(guī)則的應(yīng)用將會(huì)被自動(dòng)激活。但是在微應(yīng)用和主應(yīng)用之間切換時(shí),每次都會(huì)刷新?tīng)顟B(tài),沒(méi)辦法保持上次操作狀態(tài)。此時(shí)就需要用到loadMicroApp來(lái)手動(dòng)加載微應(yīng)用,這種需要手動(dòng)去調(diào)用加載和卸載方法。
實(shí)現(xiàn)方案
- 入口文件改造,主應(yīng)用微應(yīng)用用動(dòng)態(tài)組件加載
<template>
<div id="app-wrapper">
<keep-alive>
<component :is="currentName" />
</keep-alive>
</div>
</template>
components: { microA, microB, mainApp }, // 注冊(cè)微應(yīng)用,主應(yīng)用組件
setup() {
const currentName = ref('mainApp')
const userStore = useUserStore()
const route = useRoute()
checkPath(route.path)
// 監(jiān)聽(tīng)路由,手動(dòng)匹配加載微應(yīng)用
watch(
() => route.path,
val => {
checkPath(val)
}
)
function checkPath(path) {
const apps = userStore.getMicroApps
const app = apps.find(item => path.startsWith(`/${item.name}`))
currentName.value = app ? app.name : 'mainApp'
if (app) {
nextTick(() => {
userStore.registerMicroApps(app) // 手動(dòng)加載微應(yīng)用
})
}
}
return {
currentName
}
}
- user.ts 預(yù)加載微應(yīng)用靜態(tài)資源、手動(dòng)調(diào)用加載
microAppsInstance: {}
// 預(yù)加載微應(yīng)用靜態(tài)資源 在登錄之后調(diào)用
export function prefetchMircoApps() {
const userStore = useUserStore()
const apps = userStore.getMicroApps.map(({ name, entry }) => {
return {
name: name,
entry
}
})
prefetchApps(apps) // 預(yù)加載微應(yīng)用的靜態(tài)資源
}
export function registerApps(app) {
const userStore = useUserStore()
const microAppsInstance = userStore.getMicroAppsInstance
if (!microAppsInstance[app.name]) {
console.log('手動(dòng)加載微應(yīng)用:', app.name)
microAppsInstance[app.name] = loadMicroApp(app)
userStore.setMicroAppsInstance(microAppsInstance)
}
}
// 卸載微應(yīng)用
export function uninstallMicroApp() {
const appsInstance = userStore.getMicroAppsInstance
for (const key in appsInstance) {
appsInstance[key].unmount()
}
userStore.setMicroAppsInstance({})
}
- 卸載微應(yīng)用
到現(xiàn)在加載已經(jīng)完成了,打開(kāi)主應(yīng)用和微應(yīng)用頁(yè)面,來(lái)回切換已經(jīng)實(shí)現(xiàn)了狀態(tài)保持,接下來(lái)就是卸載微應(yīng)用。標(biāo)簽被關(guān)閉的時(shí)候去判斷是否需要卸載。
// 編輯標(biāo)簽
function handleEdit(targetKey: string, action) {
if (action === 'remove') unmountMicroApp(targetKey)
// Added operation to hide, currently only use delete operation
if (unref(unClose)) {
return
}
tabStore.closeTabByKey(targetKey, router)
}
function unmountMicroApp(targetKey) {
const appsInstance = userStore.getMicroAppsInstance
const appKey = targetKey.split('/')[1]
if (!appsInstance[appKey]) return
// 判斷打開(kāi)的標(biāo)簽中,是否還有該微應(yīng)用的頁(yè)面,如果沒(méi)有則卸載該子應(yīng)用
if (
!tabStore.getTabList.some(
tab => tab.path !== targetKey && tab.path.startsWith(`/${appKey}`)
)
) {
appsInstance[appKey].unmount()
delete appsInstance[appKey]
userStore.setMicroAppsInstance(appsInstance)
}
}