什么是微前端
微前端是指存在于瀏覽器中的微服務,其借鑒了微服務的架構理念,將微服務的概念擴展到了前端。
如果對微服務的概念比較陌生的話,可以簡單的理解為微前端就是將一個大型的前端應用拆分成多個模塊,每個微前端模塊可以由不同的團隊進行管理,并可以自主選擇框架,并且有自己的倉庫,可以獨立部署上線。
微前端的好處
1.團隊自治
2.兼容老項目
3.跨技術棧
現有的微前端方案
1.iframe
通過iframe標簽來嵌入到父應用中,iframe具有天然的隔離屬性,各個子應用之間以及子應用和父應用之間都可以做到互不影響。
iframe的缺點:
- url不同步,如果刷新頁面,iframe中的頁面的路由會丟失。
- 全局上下文完全隔離,內存變量不共享。
- UI不同步,比如iframe中的頁面如果有帶遮罩層的彈窗組件,則遮罩就不能覆蓋整個瀏覽器,只能在iframe中生效。
- 慢。每次子應用進入都是一次瀏覽器上下文重建、資源重新加載的過程。
2.single-spa
官網:https://zh-hans.single-spa.js.org/docs/getting-started-overviewsingle-spa是最早的微前端框架,可以兼容很多技術棧。
single-spa的缺點:
- 沒有實現js隔離和css隔離
- 需要修改大量的配置,包括基座和子應用的,不能開箱即用
3.qiankun
qiankun是阿里開源的一個微前端的框架qiankun的優點:
- 基于single-spa封裝的,提供了更加開箱即用的API
- 技術棧無關,任意技術棧的應用均可使用/接入,不論是 React/Vue/Angular/JQuery 還是其他等框架。
- HTML Entry的方式接入,像使用iframe一樣簡單
- 實現了single-spa不具備的樣式隔離和js隔離
- 資源預加載,在瀏覽器空閑時間預加載未打開的微應用資源,加速微應用打開速度。
- 基座(主應用):主要負責集成所有的子應用,提供一個入口能夠訪問你所需要的子應用的展示,盡量不寫復雜的業務邏輯
- 子應用:根據不同業務劃分的模塊,每個子應用都打包成
umd
模塊的形式供基座(主應用)來加載
基座改造
基座用的是create-react-app
腳手架加上antd
組件庫搭建的項目,也可以選擇vue或者其他框架,一般來說,基座只提供加載子應用的容器,盡量不寫復雜的業務邏輯。
- 安裝qiankun
// 安裝qiankun
npm i qiankun // 或者 yarn add qiankun
- 修改入口文件
// 在src/index.tsx中增加如下代碼
import { start, registerMicroApps} from 'qiankun'
// 1.要加載的子應用列表
const apps = [{name: 'sub-react', // 子應用的名稱entry: '//localhost:3001', // 默認會加載這個路徑下的html,解析里面的jsactiveRule: '/sub-react',// 匹配的路由container: '#sub-app' // 子應用加載的容器},
] // 2. 注冊子應用
registerMicroApps(apps, {beforeLoad: [async app => console.log('before load', app.name)],beforeMount: [async app => console.log('before mount', app.name)],afterMount: [async app => console.log('after mount', app.name)],afterUnmount: [async app => console.log('after unmount', app.name)]
})// 3. 啟動微服務
start()
react子應用
使用create-react-app
腳手架創建,webpack
進行配置,為了不eject所有的webpack配置,我們選擇用react-app-rewired
工具來改造webpack配置。
2. 修改入口文件
// 在src/index.tsx中增加如下代碼
import React from 'react';
import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App';
import { BrowserRouter } from 'react-router-dom'
import './public-path'let root: any;
// 將render方法用函數包裹,供后續主應用與獨立運行調用
function render(props: any) {const { container } = props// 1.拿到root的divconst dom = container ? container.querySelector('#root') : document.getElementById('root')root = createRoot(dom)// 2.把app渲染到root上面// basename對應的是基座里面子應用列表的路由// 因為基座加載子應用的時候是匹配路由的root.render(<BrowserRouter basename='/sub-react'><App/></BrowserRouter>)
}// 判斷是否在qiankun環境下,非qiankun環境下獨立運行
if(!(window as any).__POWERED_BY_QIANKUN__) {render({})
}// 各個生命周期
// bootstrap 置灰在微應用初始化的時候調用一次,下次微應用重新進入時會直接調用mount鉤子,不會再重復觸發 bootstrap
export async function bootstrap() {console.log('react app bootstrap');
}// 應用每次進入都會調用mount方法,通常我們在這里觸發應用的渲染方法
export async function mount(props: any) {console.log('props==', props);render(props)
} // 應用每次 切出/卸載 會調用的方法,通常在這里我們會卸載微應用的應用實例
export async function unmount(props: any) {root.unmount()
}
- 新增public-path.js
if (window.__POWERED_BY_QIANKUN__) {// 動態設置 webpack publicPath,防止資源加載出錯// eslint-disable-next-line no-undef__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}
- 修改webpack配置文件
// 在根目錄下新增config-overrides.js文件并新增如下配置
const { name } = require("./package");module.exports = {webpack: (config) => {config.output.library = `${name}-[name]`;// 把項目打包成umd模塊,方便qiankun去讀取我們暴露出來的生命周期(bootstrap、mount、unmount)config.output.libraryTarget = "umd";config.output.chunkLoadingGlobal = `webpackJsonp_${name}`;return config;}
};
vue子應用
# 創建子應用,選擇vue3+vite
npm create vite@latest
改造子應用
- 安裝
vite-plugin-qiankun
依賴包
npm i vite-plugin-qiankun # yarn add vite-plugin-qiankun
- 修改vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import qiankun from 'vite-plugin-qiankun';export default defineConfig({base: '/sub-vue', // 和基座中配置的activeRule一致server: {port: 3002, // 端口cors: true, // 允許跨域origin: 'http://localhost:3002' // 指定允許跨域請求的來源地址},plugins: [vue(),// 加一個qiankun,寫子應用的名稱,需要開發模式的需要配置useDevModeqiankun('sub-vue', { // 配置qiankun插件// 是否運行在開發模式下useDevMode: true})]
})
- 修改main.ts
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router'import { renderWithQiankun, qiankunWindow } from 'vite-plugin-qiankun/dist/helper'let app: any;
// 判斷是不是在qiankun環境下
if (!qiankunWindow.__POWERED_BY_QIANKUN__) {createApp(App).use(router).mount('#app');
} else {renderWithQiankun({// 子應用掛載(來回切換的時候)mount (props){ // 掛載的時候app = createApp(App);app.use(router).mount(props.container.querySelector('#app'))},// 只有子應用第一次加載會觸發bootstrap () { // 應用剛加載的時候console.log('vue app bootstrap');},// 更新update () { // 更新console.log('vue app update');},// 卸載unmount () { // 卸載console.log('vue app unmount');app?.unmount();}})
}
umi子應用
使用umi4去創建子應用,創建好后只需要簡單的配置就可以跑起來
- 安裝插件
npm i @umijs/plugins
- 配置.umirc.ts
export default {base: '/sub-umi',npmClient: 'npm',plugins: ['@umijs/plugins/dist/qiankun'],qiankun: {slave: {},}
};
// 在入口文件導出qiankun的生命周期
export const qiankun = {async mount(props:any) {console.log(props);},async bootstrap () {console.log('umi app bootstraped');},async afterMount(props: any) {console.log('umi app afterMount', props);},
}
補充
1.樣式隔離
qiankun內部實現的是子應用之間的樣式隔離,基座和子應用之間的樣式么沒有進行隔離
應用間的樣式隔離原因:子應用之間的樣式隔離很簡單,加載子應用的樣式,卸載的時候把子應用的樣式進行卸載,加載下一個子應用的時候加載下一個子應用的樣式
1.樣式隔離1.1 每個應用的樣式使用固定的格式1.2 通過css-module的方式給每個應用自動添加上前綴修改基座應用公共樣式的時候還是會影響子應用的樣式,這時候可以把子應用的樣式優先級提高一點 (樣式隔離)
2.子應用間的跳轉
2.1 主應用和微應用都是hash模式,主應用根據hash來判斷微應用,則不用考慮這個問題
通過location.hash直接修改hash值
http://localhost:3001/#/react-app/list
修改為
http://localhost:3001/#/vue-app/list
2.2 history模式下應用之間的跳轉或者微應用跳主應用頁面,直接使用微應用的路由實例是不行的,原因是微應用的路由實例跳轉都基于路由的base。
2.2.1 history.pushState()
2.2.2 將主應用的路由實例通過props傳給微應用,微應用這個路由實例跳轉
// 基座和子應用用的都是一個windows對象,可以在基座中復寫并監聽history.pushState()方法并做相應的跳轉邏輯// 在app.tsx重寫pushState
// 重寫函數// 重寫函數const _wr = function (type: string) {// 拿到windos 的history對象傳過來的參數const org = (window as any).history[type]return function() {// 拿到org后對他重新調用下const rv = org.apply(this, arguments);const e: any = new Event(type)// 返回發布一個自定義事件e.arguments = argumentswindow.dispatchEvent(e)return rv}}// 把重寫的函數賦值給window上pushStatewindow.history.pushState = _wr('pushState')// 在這個函數中做跳轉后的邏輯const bindHistory = () => {const currentPath = window.location.pathname;setSelectedPath(routes.find(item => currentPath.includes(item.key))?.key || '')}// 綁定事件window.addEventListener('pushState', bindHistory)
公共依賴加載
3.1 場景:如果主應用和子應用都使用了相同的庫或者包(antd, axios等),就可以用externals(外部擴展)的方式來引入,減少加載重復報導致資源浪費,就是一個 項目使用后另一個項目不必再重復加載。
3.2.1 主應用:將所有公共依賴配置webpack的externals,并且在index.html使用外鏈引入這些公共依賴
3.2.2 子應用:和主應用一樣配置webpack的externals,并且在index.html使用外鏈引入這些公共依賴,注意,還需要給子應用的公共依賴加上ignore屬性(這是自定義的屬性, 非標準屬性),
qiankun在解析時如果發現ignore屬性就會自動忽略
以axios為例:
基座的配置
// 修改config-overrides.js
const { override, addWebpackExternals } = require('customize-cra')// 這個配置就是通過外鏈的方式去引入這個包
module.exports = override(addWebpackExternals({axios: "axios"})
)// 在publi目錄下的index.html添加外鏈
<!-- 注意:這里的公共依賴的版本必須和子應用一致 -->
<script src="https://unpkg.com/axios@1.1.2/dist/axios.min.js"></script>
子應用的配置
// 在umi-app子應用中
// 修改.umirc.ts配置文件
export default {base: '/sub-umi',npmClient: 'npm',plugins: ['@umijs/plugins/dist/qiankun'],qiankun: {slave: {},},headScripts: [{ // 配置外鏈地址,和設置忽略,qiankun在解析時如果發現ignore屬性就會自動忽略src: 'https://unpkg.com/axios@1.1.2/dist/axios.min.js', ignore: true}]
};
全局狀態管理
一般來說,各個子應用是通過業務來劃分的,不同業務線應該降低耦合度,盡量去避免通信,但是如果涉及到一些公共的狀態或者操作,qiankun也是支持的。
qiankun提供了一個全局的GLobalState來共享數據,基座初始化之后,子應用可以監聽到這個數據的變化,也能提交這個數據
// 基座的配置
// 在src/index.tsx中增加如下代碼
import { initGlobalState } from 'qiankun'
// 基座初始化
const state = { count: 1 }
const actions = initGlobalState(state);
// 基座項目監聽和修改
actions.onGlobalStateChange((state, prev) => {// state: 變更后的狀態; prev 變更前的狀態console.log(state, prev);
})
actions.setGlobalState(state)
// 子應用的配置
// 在src/index.tsx中增加如下代碼
// 在子應用的mount生命周期監聽
export async function mount(props: any) {console.log('props==', props);// 子項目監聽和修改// 然后在子應用中拿到這兩個函數,然后給他設置一個count:2props.onGlobalStateChange((state,prev) => {// state: 變更后的狀態, prev 變更前的狀態console.log('子應用state===',state,prev)// 一般是將這個state存儲到我們子應用的store,然后在其他組件中去用// 這樣就是實現了一個簡單基座和子應用之間的通信// 同樣在其他子應用中想要用到基座傳過來的狀態也是這樣用的,// 修改的話也是調用這個setGlobalState// 監聽變化也是調用onGlobalStateChange})props.setGlobalState({ count: 2 })render(props)
}