轉(zhuǎn)載請注明出處股囊,點擊此處 查看更多精彩內(nèi)容
micro-app 使用手冊
micro-app 是借鑒了 Web Component
的思想欧募,通過 Custom Element
結(jié)合自定義的 Shadow Dom
想暗,將微前端封裝成一個類 Web Component
組件圆裕,從而實現(xiàn)微前端的組件化渲染取逾。并且由于自定義 Shadow Dom
的隔離特性,micro-app
不需要像 single-spa
和 qiankun
一樣要求子應(yīng)用修改渲染邏輯并暴露出方法箕母,也不需要修改 Webpack
配置储藐,是目前市面上接入微前端成本最低的方案。
概念圖
快速上手
主應(yīng)用
主應(yīng)用不限技術(shù)棧司蔬,只需引入 micro-app
邑茄、配置子應(yīng)用路由并啟動 micro-app
即可。這里以 Vue3
框架為例俊啼。
安裝 micro-app
yarn add @micro-zoe/micro-app
pnpm add @micro-zoe/micro-app
npm i @micro-zoe/micro-app
啟動 micro-app
在應(yīng)用入口引入并啟動 micro-app
。
import microApp from '@micro-zoe/micro-app'
microApp.start()
嵌入子應(yīng)用
創(chuàng)建 Vue 頁面(如 src/views/SubApp.vue
)用于承載子應(yīng)用左医。
<template>
<div>
<micro-app name='sub-app' url='http://localhost:8381/' baseroute='/sub-app'></micro-app>
</div>
</template>
<micro-app>
組件配置說明:
- name: 子應(yīng)用名稱授帕。必須以字母開頭,且不可以帶有除中劃線和下劃線外的特殊符號浮梢,每個 name 都對應(yīng)一個應(yīng)用跛十,當(dāng)多個應(yīng)用同時渲染時,name 不可以重復(fù)秕硝。
- url: 子應(yīng)用地址芥映。會被自動補全,如 http://localhost:3000/index.html。
- baseroute: 主應(yīng)用分配給子應(yīng)用的基礎(chǔ)路由奈偏。
- 查看更多子應(yīng)用配置
配置子應(yīng)用路由
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
...,
{
path: '/sub-app/*',
component: () => import('../views/SubApp.vue')
},
]
})
export default router
path
是子應(yīng)用路由地址坞嘀。非嚴(yán)格匹配,/sub-app/*
都指向 SubApp
頁面惊来。使用 vue-router@4.x
時寫法為:'/sub-app/:page*'
丽涩。
Vue2 + Webpack 子應(yīng)用
設(shè)置基礎(chǔ)路由
const router = new VueRouter({
mode: "history",
routes,
base: window.__MICRO_APP_BASE_ROUTE__ || process.env.BASE_URL,
});
配置跨域支持
修改 vue.config.js
配置跨域支持。
const { defineConfig } = require("@vue/cli-service");
module.exports = defineConfig({
...,
devServer: {
headers: {
"Access-Control-Allow-Origin": "*",
},
...,
},
});
Vue3 + Vite 子應(yīng)用
在嵌入 Vite
子應(yīng)用時裁蚁,micro-app
的功能只負(fù)責(zé)渲染矢渊,其它的行為由應(yīng)用自行決定,這包括如何防止樣式枉证、JavaScript 變量矮男、元素的沖突。
子應(yīng)用的修改
- 添加自定義插件
- 新建插件
vite-plugin-micro-app.js
import fs from 'fs'
import path from 'path'
function VitePluginMicroApp() {
let basePath = ''
return {
name: 'vite:micro-app',
apply: 'build',
configResolved(config) {
basePath = `${config.base}${config.build.assetsDir}/`
},
writeBundle(options, bundle) {
for (const chunkName in bundle) {
if (!Object.prototype.hasOwnProperty.call(bundle, chunkName)) {
continue
}
const chunk = bundle[chunkName]
if (!chunk.fileName?.endsWith('.js') && !chunk.fileName?.endsWith('.ts')) {
continue
}
chunk.code = chunk.code.replace(/(from|import\()(\s*['"])(\.\.?\/)/g, (all, $1, $2, $3) =>
all.replace($3, new URL($3, basePath))
)
const fullPath = path.join(options.dir, chunk.fileName)
fs.writeFileSync(fullPath, chunk.code)
}
}
}
}
export default VitePluginMicroApp
- 導(dǎo)入插件并配置公共資源基礎(chǔ)路徑
import microAppPlugin from './vite-plugin-micro-app'
export default defineConfig({
...,
base: '/vue3-app/',
plugins: [vue(), microAppPlugin()],
})
- 修改容器元素 id
- 修改
index.html
中容器元素的 id 值
<body>
<div id="my-vite-app"></div>
</body>
- 使用新的 id 渲染
createApp(App).mount('#my-vite-app')
當(dāng)多個vite子應(yīng)用同時渲染時室谚,必須修改容器元素的id值昂灵,確保每個子應(yīng)用容器元素id的唯一性,否則無法正常渲染舞萄。
- 路由
推薦基座使用 history
路由眨补,Vite
子應(yīng)用使用 hash
路由,避免一些可能出現(xiàn)的問題倒脓。
子應(yīng)用如果是 Vue3
撑螺,在初始化時路由時,createWebHashHistory
不要傳入?yún)?shù)崎弃,如下:
const router = createRouter({
...,
history: createWebHashHistory(),
})
- 靜態(tài)資源
圖片等靜態(tài)資源需要使用絕對地址甘晤,可以使用 new URL('../assets/logo.png', import.meta.url).href
等方式獲取資源的全鏈接地址。
主應(yīng)用的修改
- 關(guān)閉沙箱并使用內(nèi)聯(lián) script 模式
<micro-app
name='child-name'
url='http://localhost:3001/basename/'
disableSandbox // 關(guān)閉沙箱
inline // 使用內(nèi)聯(lián)script模式
>
- 處理子應(yīng)用靜態(tài)資源
寫一個簡易的插件饲做,對開發(fā)環(huán)境的子應(yīng)用進行處理线婚,補全靜態(tài)資源路徑。
microApp.start({
plugins: {
modules: {
// appName 即子應(yīng)用的 name
appName: [{
loader(code) {
if (import.meta.env.MODE !== 'development') {
return code
}
// 這里 basename 需要和子應(yīng)用vite.config.js中base的配置保持一致
code = code.replace(/(from|import)(\s*['"])(\/basename\/)/g, all => {
return all.replace('/basename/', '子應(yīng)用域名/basename/')
})
return code
}
}]
}
}
})
React 子應(yīng)用
設(shè)置基礎(chǔ)路由
ReactDOM.render(
<React.StrictMode>
<BrowserRouter basename={window.__MICRO_APP_BASE_ROUTE__ || "/"}>
<App />
</BrowserRouter>
</React.StrictMode>,
document.getElementById("root")
);
配置跨域支持
- 安裝
react-app-rewired
customize-cra
依賴
yarn add -D react-app-rewired customize-cra
pnpm add -D react-app-rewired customize-cra
npm i -D react-app-rewired customize-cra
- 應(yīng)用根目錄添加
config-overrides.js
文件
const { overrideDevServer } = require("customize-cra");
module.exports = {
devServer: overrideDevServer((config) => ({
...config,
headers: {
"Access-Control-Allow-Origin": "*",
},
})),
};
- 修改
package.json
- "start": "react-scripts start",
+ "start": "react-app-rewired start",
- "build": "react-scripts build",
+ "build": "react-app-rewired build",
應(yīng)用間通信
micro-app
提供了一套靈活的數(shù)據(jù)通信機制盆均,方便主應(yīng)用和子應(yīng)用之間的數(shù)據(jù)傳輸塞弊。
正常情況下,主應(yīng)用和子應(yīng)用之間的通信是綁定的泪姨,主應(yīng)用只能向指定的子應(yīng)用發(fā)送數(shù)據(jù)游沿,子應(yīng)用只能向基座發(fā)送數(shù)據(jù),這種方式可以有效的避免數(shù)據(jù)污染肮砾,防止多個子應(yīng)用之間相互影響诀黍。
同時 micro-app
也提供了全局通信,方便跨應(yīng)用之間的數(shù)據(jù)通信仗处。
主應(yīng)用向子應(yīng)用發(fā)送數(shù)據(jù)
主應(yīng)用向子應(yīng)用發(fā)送數(shù)據(jù)有兩種方式眯勾。
- 通過
data
屬性發(fā)送數(shù)據(jù)
使用 <micro-app>
組件的 data
給子應(yīng)用發(fā)送數(shù)據(jù)枣宫,此時只接受對象類型,數(shù)據(jù)變化時會自動重新發(fā)送吃环。
<template>
<div>
<micro-app name="my-app" url="http://localhost:8381/" baseroute="/my-app" :data="data" />
</div>
</template>
<script setup lang="ts">
const data = { msg: '通過 data 發(fā)送給子應(yīng)用的數(shù)據(jù)' }
</script>
- 手動發(fā)送數(shù)據(jù)
手動發(fā)送數(shù)據(jù)需要通過 name
指定接受數(shù)據(jù)的子應(yīng)用也颤,此值和 <micro-app>
元素中的 name
一致。
// 發(fā)送數(shù)據(jù)給子應(yīng)用 my-app模叙,setData第二個參數(shù)只接受對象類型
microApp.setData('my-app', { msg: '手動發(fā)送給子應(yīng)用的數(shù)據(jù)' })
子應(yīng)用接收主應(yīng)用發(fā)送的數(shù)據(jù)
micro-app
會向子應(yīng)用注入名稱為 microApp
的全局對象歇拆,子應(yīng)用通過這個對象有兩種方式獲取來自主應(yīng)用的數(shù)據(jù)。
- 直接獲取
// 獲取主應(yīng)用下發(fā)的 data 數(shù)據(jù)
const data = window.microApp.getData()
- 綁定監(jiān)聽函數(shù)
function dataListener(data) {
console.log('來自主應(yīng)用的數(shù)據(jù)', data)
}
/**
* 綁定監(jiān)聽函數(shù)范咨,監(jiān)聽函數(shù)只有在數(shù)據(jù)變化時才會觸發(fā)
* dataListener: 綁定函數(shù)
* autoTrigger: 在初次綁定監(jiān)聽函數(shù)時如果有緩存數(shù)據(jù)故觅,是否需要主動觸發(fā)一次,默認(rèn)為 false
* !!!重要說明: 因為子應(yīng)用是異步渲染的渠啊,而基座發(fā)送數(shù)據(jù)是同步的输吏,
* 如果在子應(yīng)用渲染結(jié)束前主應(yīng)用發(fā)送數(shù)據(jù),則在綁定監(jiān)聽函數(shù)前數(shù)據(jù)已經(jīng)發(fā)送替蛉,在初始化后不會觸發(fā)綁定函數(shù)贯溅,
* 但這個數(shù)據(jù)會放入緩存中,此時可以設(shè)置 autoTrigger 為 true 主動觸發(fā)一次監(jiān)聽函數(shù)來獲取數(shù)據(jù)躲查。
*/
window.microApp.addDataListener(dataListener: Function, autoTrigger?: boolean)
// 解綁監(jiān)聽函數(shù)
window.microApp.removeDataListener(dataListener: Function)
// 清空當(dāng)前子應(yīng)用的所有綁定函數(shù)(全局?jǐn)?shù)據(jù)函數(shù)除外)
window.microApp.clearDataListener()
子應(yīng)用向主應(yīng)用發(fā)送數(shù)據(jù)
// dispatch只接受對象作為參數(shù)
window.microApp.dispatch({ msg: '子應(yīng)用發(fā)送的數(shù)據(jù)' })
主應(yīng)用接收子應(yīng)用發(fā)送的數(shù)據(jù)
主應(yīng)用獲取來自子應(yīng)用的數(shù)據(jù)有三種方式它浅。
- 直接獲取數(shù)據(jù)
// 獲取指定子應(yīng)用發(fā)送的數(shù)據(jù)
const childData = microApp.getData(appName)
- 監(jiān)聽
datachange
事件
<template>
<div>
<micro-app name="my-app" url="http://localhost:8381/" baseroute="/my-app" @datachange="handleDataChange" />
</div>
</template>
<script setup lang="ts">
function handleDataChange(data: any) {
console.log(data);
}
</script>
- 綁定監(jiān)聽函數(shù)
function dataListener(data) {
console.log('來自子應(yīng)用的數(shù)據(jù)', data)
}
/**
* 綁定監(jiān)聽函數(shù)
* appName: 應(yīng)用名稱
* dataListener: 綁定函數(shù)
* autoTrigger: 在初次綁定監(jiān)聽函數(shù)時如果有緩存數(shù)據(jù),是否需要主動觸發(fā)一次镣煮,默認(rèn)為false
*/
microApp.addDataListener(appName: string, dataListener: Function, autoTrigger?: boolean)
// 解綁監(jiān)聽my-app子應(yīng)用的函數(shù)
microApp.removeDataListener(appName: string, dataListener: Function)
// 清空所有監(jiān)聽appName子應(yīng)用的函數(shù)
microApp.clearDataListener(appName: string)
全局?jǐn)?shù)據(jù)通信
全局?jǐn)?shù)據(jù)通信會向主應(yīng)用和所有子應(yīng)用發(fā)送數(shù)據(jù)姐霍,在跨應(yīng)用通信的場景中適用。
發(fā)送全局?jǐn)?shù)據(jù)
// setGlobalData 只接受對象作為參數(shù)
microApp.setGlobalData({ msg: '全局?jǐn)?shù)據(jù)' })
獲取全局?jǐn)?shù)據(jù)
- 直接獲取數(shù)據(jù)
const globalData = window.microApp.getGlobalData()
- 綁定監(jiān)聽函數(shù)
const dataListener = data => {
console.log(data)
}
microApp.addGlobalDataListener(dataListener, true)
關(guān)閉沙箱后的通信方式
沙箱關(guān)閉后典唇,子應(yīng)用默認(rèn)的通信功能失效镊折,此時可以通過手動注冊通信對象實現(xiàn)一致的功能。
注冊方式:在主應(yīng)用中為子應(yīng)用初始化通信對象
import { EventCenterForMicroApp } from '@micro-zoe/micro-app'
// 注意:每個子應(yīng)用根據(jù) appName 單獨分配一個通信對象
window.eventCenterForAppxx = new EventCenterForMicroApp(appName)
子應(yīng)用通信方式:
// 直接獲取數(shù)據(jù)
const data = window.eventCenterForAppxx.getData()
function dataListener(data) {
console.log('來自主應(yīng)用的數(shù)據(jù)', data)
}
/**
* 綁定監(jiān)聽函數(shù)
* dataListener: 綁定函數(shù)
* autoTrigger: 在初次綁定監(jiān)聽函數(shù)時如果有緩存數(shù)據(jù)介衔,是否需要主動觸發(fā)一次恨胚,默認(rèn)為 false
*/
window.eventCenterForAppxx.addDataListener(dataListener: Function, autoTrigger?: boolean)
// 解綁監(jiān)聽函數(shù)
window.eventCenterForAppxx.removeDataListener(dataListener: Function)
// 清空當(dāng)前子應(yīng)用的所有綁定函數(shù)(全局?jǐn)?shù)據(jù)函數(shù)除外)
window.eventCenterForAppxx.clearDataListener()
// 子應(yīng)用向主應(yīng)用發(fā)送數(shù)據(jù),只接受對象作為參數(shù)
window.eventCenterForAppxx.dispatch({ msg: '子應(yīng)用發(fā)送的數(shù)據(jù)' })
常見問題
__webpack_public_path__
無效炎咖,靜態(tài)資源路徑錯誤赃泡。
將 public-path.js
的導(dǎo)入語句放在應(yīng)用入口文件的第一行。
TypeScript cannot find name __webpack_public_path__
塘装。
在 src
目錄新增 global.d.ts
文件:
declare let __webpack_public_path__: string;
interface Window {
__MICRO_APP_BASE_ROUTE__: string;
__MICRO_APP_PUBLIC_PATH__: string;
__MICRO_APP_ENVIRONMENT__: boolean;
}
React18 子應(yīng)用首次進入展示空白急迂,再次進入正常。
在 React18
項目中使用 @rescripts/cli
修改 Webpack
配置可能會導(dǎo)致 micro-app
首次進入 React18
子應(yīng)用時展示空白蹦肴。
將 @rescripts/cli
替換為 react-app-rewired
customize-cra
即可。
configuration.output has an unknown property 'jsonpFunction'.
將 output.jsonpFunction
更名為 output.chunkLoadingGlobal
猴娩。
// jsonpFunction: `webpackJsonp_${name}`,
chunkLoadingGlobal: `webpackJsonp_${name}`,
子應(yīng)用內(nèi)部路由跳轉(zhuǎn)后無法切換到主應(yīng)用或其他子應(yīng)用且路由棧異常阴幌。
-
Vue
通過路由守衛(wèi)更新state
router.afterEach(() => {
// Vue2
Object.assign(history.state, { current: location.pathname });
// Vue3
Object.assign(history.state, {
current: history.state.current === '/' ? '/' : `${location.pathname}${location.hash}`
});
});
-
React
通過輪詢監(jiān)聽路由變化更新state
function listenRouterChange(callback: () => void) {
let oldHref = window.location.href;
const loop = () => {
window.requestAnimationFrame(() => {
if (window.location.href !== oldHref) {
oldHref = window.location.href;
callback();
} else {
loop();
}
});
};
loop();
}
listenRouterChange(() => {
Object.assign(window.history.state, { current: window.location.pathname });
});