微前端(micro-app)使用手冊

轉(zhuǎn)載請注明出處股囊,點擊此處 查看更多精彩內(nèi)容

micro-app 使用手冊

micro-app 是借鑒了 Web Component 的思想欧募,通過 Custom Element 結(jié)合自定義的 Shadow Dom想暗,將微前端封裝成一個類 Web Component 組件圆裕,從而實現(xiàn)微前端的組件化渲染取逾。并且由于自定義 Shadow Dom 的隔離特性,micro-app 不需要像 single-spaqiankun 一樣要求子應(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)用的修改

  1. 添加自定義插件
  • 新建插件 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()],
})
  1. 修改容器元素 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的唯一性,否則無法正常渲染舞萄。

  1. 路由

推薦基座使用 history 路由眨补,Vite 子應(yīng)用使用 hash 路由,避免一些可能出現(xiàn)的問題倒脓。

子應(yīng)用如果是 Vue3撑螺,在初始化時路由時,createWebHashHistory 不要傳入?yún)?shù)崎弃,如下:

const router = createRouter({
  ...,
  history: createWebHashHistory(),
})
  1. 靜態(tài)資源

圖片等靜態(tài)資源需要使用絕對地址甘晤,可以使用 new URL('../assets/logo.png', import.meta.url).href 等方式獲取資源的全鏈接地址。

主應(yīng)用的修改

  1. 關(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模式
>
  1. 處理子應(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")
);

配置跨域支持

  1. 安裝 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
  1. 應(yīng)用根目錄添加 config-overrides.js 文件
const { overrideDevServer } = require("customize-cra");

module.exports = {
  devServer: overrideDevServer((config) => ({
    ...config,
    headers: {
      "Access-Control-Allow-Origin": "*",
    },
  })),
};
  1. 修改 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ù)有兩種方式眯勾。

  1. 通過 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>
  1. 手動發(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ù)。

  1. 直接獲取
// 獲取主應(yīng)用下發(fā)的 data 數(shù)據(jù)
const data = window.microApp.getData()
  1. 綁定監(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ù)有三種方式它浅。

  1. 直接獲取數(shù)據(jù)
// 獲取指定子應(yīng)用發(fā)送的數(shù)據(jù)
const childData = microApp.getData(appName)
  1. 監(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>
  1. 綁定監(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ù)

  1. 直接獲取數(shù)據(jù)
const globalData = window.microApp.getGlobalData()
  1. 綁定監(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 });
});
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末勺阐,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子矛双,更是在濱河造成了極大的恐慌渊抽,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件议忽,死亡現(xiàn)場離奇詭異懒闷,居然都是意外死亡,警方通過查閱死者的電腦和手機栈幸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進店門愤估,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人速址,你說我怎么就攤上這事玩焰。” “怎么了芍锚?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵昔园,是天一觀的道長。 經(jīng)常有香客問我并炮,道長默刚,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任逃魄,我火速辦了婚禮荤西,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘嗅钻。我一直安慰自己皂冰,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布养篓。 她就那樣靜靜地躺著秃流,像睡著了一般。 火紅的嫁衣襯著肌膚如雪柳弄。 梳的紋絲不亂的頭發(fā)上舶胀,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天,我揣著相機與錄音碧注,去河邊找鬼嚣伐。 笑死,一個胖子當(dāng)著我的面吹牛萍丐,可吹牛的內(nèi)容都是我干的轩端。 我是一名探鬼主播,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼逝变,長吁一口氣:“原來是場噩夢啊……” “哼基茵!你這毒婦竟也來了奋构?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤拱层,失蹤者是張志新(化名)和其女友劉穎弥臼,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體根灯,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡径缅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了烙肺。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片纳猪。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖茬高,靈堂內(nèi)的尸體忽然破棺而出兆旬,到底是詐尸還是另有隱情,我是刑警寧澤怎栽,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布丽猬,位于F島的核電站,受9級特大地震影響熏瞄,放射性物質(zhì)發(fā)生泄漏脚祟。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一强饮、第九天 我趴在偏房一處隱蔽的房頂上張望由桌。 院中可真熱鬧,春花似錦邮丰、人聲如沸行您。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽娃循。三九已至,卻和暖如春斗蒋,著一層夾襖步出監(jiān)牢的瞬間捌斧,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工泉沾, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留捞蚂,地道東北人。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓跷究,卻偏偏與公主長得像姓迅,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,675評論 2 359

推薦閱讀更多精彩內(nèi)容