學習視頻🔊
基礎: 黑馬前端基于qiankun搭建微前端項目實戰教程_嗶哩嗶哩_bilibili
路由、部署配置注意:qiankun+vite微前端上線注意事項,base公共路徑設置_嗶哩嗶哩_bilibili
微前端
什么是微前端?
微前端是將前端應用分解成一系列更小、更易管理的獨立部分的架構方案。類似于微服務,每個微應用可以由不同團隊獨立開發、測試、部署。
微前端的好處?
-
技術棧無關
主應用和子應用可以使用不同的技術棧
允許漸進式技術棧升級
降低全局技術升級的風險
-
獨立開發部署
各團隊可以獨立開發、測試、部署
有哪些微前端方案?
下面介紹有哪些主流的微前端方案。
1、qiankun(螞蟻金服,后續也采用這種)
優點:
-
基于 single-spa 封裝
-
完善的沙箱機制
-
開箱即用的 API
-
中文社區活躍
適用場景:
-
大型中臺系統
-
需要多團隊協作的項目
2、single-spa
優點:
-
最早的微前端框架
-
靈活性高
-
社區成熟
缺點:
-
配置較復雜
-
需要自己實現樣式隔離
3、Module Federation(來自Webpack 5,中文:模塊聯邦,可以用來做遠程組件)
優點:
-
Webpack 原生支持
-
真正的運行時模塊共享
-
構建時優化
適用場景:
-
新項目
-
需要精細化控制模塊共享
4、micro-app(京東)
優點:
-
使用簡單
-
基于 Web Components
-
性能好
適用場景:
-
對性能要求高的項目
-
喜歡簡單配置的團隊
5、Iframe
優點:
-
具有天然的隔離屬性,js沙箱、樣式隔離等都很好。
缺點:
-
UI不同步,比如在iframe中添加蒙層彈框,只會在iframe中顯示,不是全屏的。
-
慢,每次進入會重新加載,多個iframe時瀏覽器容易卡死。
基礎改造
🌈🌈🌈
基座改造🚩
一、Antd Pro基座改造(umi系)🤠
接下來對中臺(antd Pro 創建的項目,作為基座)進行改造。
1、安裝@umijs/plugin-qiankun
【umimax已內置】
pnpm i @umijs/plugin-qiankun
2、注冊子應用
config/config.ts配置:
在config的qiankun.master.apps數組中 注冊子應用
export default defineConfig({qiankun: {master: {apps: [{name: 'sub-umi',//子應用的名稱entry: '//localhost:5175',//子應用的入口地址activeRule: '/qiankun/umi',//子應用的激活規則,指路由sandbox: {strictStyleIsolation: true,//嚴格樣式隔離},},],},},//其他配置
})
3、配置訪問子應用的路由
config/router.tsx :在基座的路由中添加能訪問子應用的路由。
方式1:用microApp屬性指定要渲染的子應用的name。(本次改造采用這種)
{path: '/qiankun',name: 'qiankun',routes: [{path: '/qiankun/umi',name: 'sub-umi',microApp: 'sub-umi', //和注冊時的name一致microAppProps: {// 子應用自動設置loading// autoSetLoading: true, //可以用autoSetLoading,需要子應用引入antdloader: (loading: boolean) => <Spin spinning={loading} />,},},],},
方式2:使用component屬性指定組件,在組件中使用qiankun提供的 MicoApp組件
// 1、路由{path: '/app1',name: 'sub-app',element:<SubApp/>}// 2、 SubApp組件,用MicroApp組件占位,需要指定name(和注冊時同名)
import React from 'react';
import { MicroApp } from '@umijs/max';
type Props = {};const MicroApp1 = (props: Props) => {return <MicroApp name="sub-app" />;
};export default MicroApp1;
二、非umi系基座改造
1、安裝qiankun
pnpm i qiankun // 或者 yarn add qiankun
2、修改入口文件
在src/index.tsx中增加如下代碼:從qiankun中引入注冊和啟動的函數,注冊子應用并調用start啟動。
import { start, registerMicroApps } from 'qiankun';// 1. 要加載的子應用列表
const apps = [{name: "sub-react", // 子應用的名稱entry: '//localhost:8080', // 默認會加載這個路徑下的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)],
})start() // 3. 啟動微服務
/*
// 配置qiankun啟動參數
start({// prefetch: true, // 預加載 默認是true,即在主應用加載的時候,加載子應用sandbox: {//沙箱// experimentalStyleIsolation: true, // 實驗性樣式隔離,好像沒用哦strictStyleIsolation: true, // 嚴格樣式隔離},
})
*/
3、注冊子應用路由,提供容器dom
router注冊一下子應用的路由,element設置為null,在跳轉到子應用的路由時,展示id為subApp的div。
//router/index.jsx
const router = [{path: "/",element: <Home />,},{path: "/app1/*",element: null,},
]//App.jsx
function App() {const element = useRoutes(router)return (<div className="main-layout"><nav><Link to="/">首頁</Link><Link to="/app1">子應用1</Link></nav><div className="test-aa">123</div><div className="main-content"><Suspense fallback={<div>Loading...</div>}>{element}{/* 需要子應用的容器 */}<div id="subApp"></div></Suspense></div></div>)
}
一旦瀏覽器的 url 發生變化,便會自動觸發 qiankun 的匹配邏輯。 所有 activeRule 規則匹配上的微應用就會被插入到指定的 container 中,同時依次調用微應用暴露出的生命周期鉤子。
-
registerMicroApps(apps, lifeCycles?)
注冊所有子應用,qiankun會根據activeRule去匹配對應的子應用并加載
-
start(options?)
啟動 qiankun,可以進行預加載和沙箱設置,更多options : API 說明 - qiankun
至此基座基本就改造完成,接下來改造子應用。
子應用改造🤩
子應用改造主要需要注意:
-
用umd格式打包,當然,像umi,vite這些插件已經設置了。
一、umi子應用改造
中臺項目的子應用是采用umi進行構建。
-
安裝插件
在子應用目錄安裝@umijs/plugins插件,才能在umirc中用qiankun字段。
pnpm i @umijs/plugins
-
使用插件
在 .umirc.ts 中使用上面的插件,這樣就在基座中通過子應用的地址來訪問這個子應用了。
export default defineConfig({base: '/', // 用qiankun插件后默認base為包名,所以這里重置一下qiankun: { //告訴umi這個項目需要用到qiankunslave: {},},plugins: ['@umijs/plugins/dist/qiankun', '@umijs/plugins/dist/model', '@umijs/plugins/dist/mf'], //plugins使用@umijs/plugins插件,功能分別是支持qiankun、允許useModel、模塊聯邦mf})
-
生命周期
如果需要在生命周期中做一些事情,可以在入口文件app.tsx中導出qiankun對象,在對象中的方法寫代碼,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);},
};
二、vue3+vite改造
創建子應用
創建子應用,選擇vue3+vite
npm create vite@latest
改造子應用
-
安裝
vite-plugin-qiankun
包,因為qiankun和vite有些問題,需要這個包解決。
pnpm i vite-plugin-qiankun
-
修改vite.config.js,使用上面的插件
import qiankun from 'vite-plugin-qiankun';defineConfig({base: '/sub-vue', // 和基座中配置的activeRule一致server: {port: 3002,cors: true,origin: 'http://localhost:3002'},plugins: [vue(),qiankun('sub-vue', { // 配置qiankun插件useDevMode: true})]
})
-
修改main.ts
我們需要提供三個必須的生命周期函數,即:bootstrap(只在第一次進入的時候執行)、mount(掛載)、onmount(卸載)
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import { renderWithQiankun, qiankunWindow } from 'vite-plugin-qiankun/dist/helper';let app: any;
if (!qiankunWindow.__POWERED_BY_QIANKUN__) {createApp(App).mount('#app');
} else {renderWithQiankun({// 子應用掛載mount(props) {app = createApp(App);app.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();}});
}
三、create-react-app改造
1、改造入口文件
代碼如下
-
導出三個必須的生命周期函數,供qiankun使用。
-
根據window.__POWERED_BY_QIANKUN__來決定render邏輯
let root: Root// 將render方法用函數包裹,供后續主應用與獨立運行調用
function render(props: any) {const { container } = propsconst dom = container ? container.querySelector('#root') : document.getElementById('root')root = createRoot(dom)root.render(// 可以根據需要指定basename//<BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? "/sub-react" : ""}><BrowserRouter><App/></BrowserRouter>)
}// 判斷是否在qiankun環境下,非qiankun環境下獨立運行
if (!(window as any).__POWERED_BY_QIANKUN__) {render({});
}// 各個生命周期
// bootstrap 只會在微應用初始化的時候調用一次,下次微應用重新進入時會直接調用 mount 鉤子,不會再重復觸發 bootstrap。
export async function bootstrap() {console.log('react app bootstraped');
}// 應用每次進入都會調用 mount 方法,通常我們在這里觸發應用的渲染方法
export async function mount(props: any) {render(props);
}// 應用每次 切出/卸載 會調用的方法,通常在這里我們會卸載微應用的應用實例
export async function unmount(props: any) {root.unmount();
}
2、新增public-path.js
動態設置 webpack publicPath,防止資源加載出錯
if (window.__POWERED_BY_QIANKUN__) {// 動態設置 webpack publicPath,防止資源加載出錯// eslint-disable-next-line no-undefwebpack_public_path = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}
3、修改webpack配置文件,用umd格式打包
這里用craco修改webpack的配置,所以應該先下載craco。在根目錄下新增craco.config.js文件并新增如下配置
const { name } = require("./package");module.exports = {webpack: (webpackConfig) => {webpackConfig.output = {...webpackConfig.output,library: ${packageName}-[name],libraryTarget: "umd", //主要是這個配置,用umd打包這個項目chunkLoadingGlobal: webpackJsonp_${packageName},filename: "static/js/[name].umd.js",chunkFilename: "static/js/[name].umd.chunk.js",}return webpackConfig}
};
什么是umd?
umd格式_umd 庫格式-CSDN博客
在基座能訪問子應用,即說明配置成功。
說明
一、樣式隔離
使用
子應用之間的樣式隔離qiankun已經實現了,但是基座和子應用之間的樣式隔離沒有實現。
我們可以在基座注冊子應用時 設置 strictStyleIsolation為true ,這樣設置主要是對直接設置class時進行隔離(className='test'),在項目中我們一般是 css module 和strictStyleIsolation 一起使用,子應用能加自己的前綴是更好的。
export default defineConfig({qiankun: {master: {apps: [{name: 'sub-umi',entry: '//localhost:5175',activeRule: '/qiankun/umi',sandbox: {strictStyleIsolation: true,//嚴格樣式隔離},},],},},//其他配置
})
strictStyleIsolation原理??
【基于shadow dom來做的樣式隔離】
shadowDOM 的MDN地址如下:
使用影子 DOM - Web API | MDN
strictStyleIsolation
的原理是基于 Web Components 中的 Shadow DOM 技術。讓我詳細解釋一下:
-
基本實現原理:
function createShadowContainer(container, appName) {// 創建 Shadow DOMconst shadow = container.attachShadow({ mode: 'open' });// 子應用的所有內容都會被放入這個 Shadow DOM 中return shadow;
}
實際效果:
<!-- 普通 DOM 結構 -->
<div id="main"><div id="app1">#shadow-root (open)<!-- 子應用的所有內容都在 Shadow DOM 中 --><style>.title { color: red; }</style><div class="title">我是app1的標題</div></div>
</div>
-
Shadow DOM 的特性:
-
獨立的 DOM 樹
-
樣式完全隔離
-
JavaScript 訪問限制
-
事件局部化
-
樣式隔離效果:
/* Shadow DOM 內部的樣式 */
.title { color: red; }/* 外部的樣式無法影響到 Shadow DOM 內部 */
#main .title { color: blue; } /* 這個樣式不會影響 Shadow DOM 內的 .title */
主要優點:
-
完全的樣式隔離
-
不需要額外的樣式轉換
-
原生的隔離方案
主要缺點:
-
一些第三方庫可能無法正常工作
-
彈窗類組件可能會被限制在 Shadow DOM 內
-
瀏覽器兼容性問題
二、js沙箱
-
在基座中修改window會共享到各個子應用。
-
在子應用A修改window不會影響到子應用B。
qiankun中js沙箱的原理
qiankun中的js沙箱是對window進行隔離,主要解決全局變量沖突和全局狀態相互影響的問題。
qiankun提供了三種沙箱模式:
1、在不支持proxy的瀏覽器,提供【快照沙箱】,在進入子應用之前
2、在支持proxy的瀏覽器,用proxy代理window,子應用修改代理后的window。單例就采用legacySandbox沙箱,多例就采用proxySandbox沙箱。
類似代碼:
const proxy = new Proxy(window)
(function(window){//子應用的代碼
})(proxy)
倔金沙箱:?
jhttps://juejin.cn/post/6920110573418086413#heading-12
三、剔除重復依賴
如果基座和子應用使用了項目的庫,可以考慮子應用使用基座的包,從而減少重復加載
有兩種方式:
-
externals,基座用cdn引入包,子應用相同的cdn設置為ignore。(更推薦用externals)
-
模塊聯邦,基座將重復包打包至remote.js,子應用不打包重復的包,而是在運行時請求基座的remote.js
externals
流程:
-
基座:將所有公共依賴配置
webpack
的externals
,并且在index.html
使用外鏈引入這些公共依賴 -
子應用:和主應用一樣配置
webpack
的externals
,并且在index.html
使用外鏈引入這些公共依賴,注意,還需要給子應用的公共依賴的加上ignore
屬性(這是自定義的屬性,非標準屬性),qiankun在解析時如果發現igonre
屬性就會自動忽略
以lodahs為例:
基座:
修改config/config.ts文件,在externals中添加lodash,之后在headScripts數組中添加lodash的cdn地址。
// 修改config/config.js
export default defineConfig({/*** @name <head> 中額外的 script* @description 配置 <head> 中額外的 script*/headScripts: [// 解決首次加載時白屏的問題//{ src: '/scripts/loading.js', async: true },//lodash 的cdn{ src: 'https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js', async: false },],externals: {lodash: '_',//externals 指定lodash和他的全局變量名},
})
umi子應用:
子應用同樣需要在自己的配置文件中添加cdn的lodash,并且需要添加ignore忽略lodash。
<!-- 注意:這里的公共依賴的版本,基座和子應用需要一致 -->
export default defineConfig({// 剔除重復包externals: {lodash: '_',},headScripts: [{src: 'https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js',async: false,ignore: true, // 子應用需要添加ignore,忽略lodash,使用基座掛載window上的lodash,這樣只需要請求一次},],
})
四、公共組件
我們后續會用pnpm的workspace來做monorepo,這樣在單倉下基座和子應用就能共享組件了,
但是這樣還有一個問題,當公共組件變化,子應用就需要重新打包部署才能得到公共組件的變化,所以可以采用模塊聯邦的方式,讓子應用使用基座的遠程組件,這樣就只需要對基座進行打包部署。
接下來先介紹monorepo改造基座,并舉例在子應用中如何使用workspce的公共組件,最后介紹將公共組件轉為遠程組件使用。
1、monorepo改造
-
在基座的根目錄新建
pnpm-workspace.yaml
文件,文件內容:
意思是:
-
會將這三個文件夾下的目錄添加到workspace工作空間中,他們可以相互通過workspce訪問到。
-
其中app文件夾存放子應用的項目代碼,src/expoese/components文件下存放各種公共組件的代碼。
-
在umi中要導出遠程組件,需要將組件寫到src/exposes文件夾下,umi自動處理exposes文件夾下的組件。
packages:- 'apps/*'- 'src/exposes/components/*'- 'src/exposes/*'
-
新建yaml中涉及的文件夾,將子應用放入apps文件夾中。
至此簡單的monorepo改造完畢
2、添加公共組件
下面演示PureButton
這個公共組件的創建和使用。
-
前置步驟
在src/exposes/components (看上面,這是一個workspace目錄) 目錄下執行npm init -y
,我們會將components目錄作為組件庫的目錄,后續的公共組件都寫在這個目錄下。其package.json類似步驟2(ps:也可以不這么做,也可以直接所有公共組件寫在exposes下,只要組件是個npm包就行。)
{"name": "components","version": "1.0.1","description": "","main": "index.tsx","keywords": [],"author": "","license": "ISC"
}
-
在workspce中創建組件包
在src/exposes/components下新建一個PureButton
文件夾,在這個文件夾中執行npm init -y
生成package.json 文件,主要修改文件中以下字段:
-
name(我們可以用@loctek這個前綴,包名用橫線分割,不要用駝峰。)
-
version
-
main(main是這個包的入口)
{"name": "@loctek/pure-button","version": "1.0.0","description": "","main": "index.tsx","types": "index.d.ts","keywords": [],"author": "","license": "ISC","private": true
}
-
書寫組件代碼
新建index.tsx,書寫組件的代碼
import styles from './style.module.less';
import type { Props } from './index.d';const PureButton = ({ btnStr = '' }: Props) => {return (<><div className={styles['btn-container']}>{btnStr}</div></>);
};export default PureButton;
-
測驗組件是否生效
(這里單純測試workspace是否生效,實際項目我們采用模塊聯邦的方式。)
在本地開發中,我們可以在apps的子應用中,在其package.json中添加上面的包,然后在子項目的目錄執行pnpm i 或者 pnpm i @loctek/pure-button
命令,這樣就會將@loctek/pure-button
組件的軟鏈接添加到子項目的node_modules中供子應用使用。
"dependencies": {//其他包..."@loctek/pure-button": "workspace:*",},
3、利用模塊聯邦使用公共組件
官網地址:
Module Federation 插件
-
改造基座,導出遠程組件
在config/config.ts中使用 mf 字段,這樣最終會在打包文件中多出一個remote.js文件。用name起一個名字,remoteHash用于取消打包的hash,library指定打包的文件的模塊
export default defineConfig({// 模塊聯邦mf: {name: 'master',//關閉remote.js的hashremoteHash: false,// qiankun時必須要,window是掛載到window上,默認是varlibrary: { type: 'window', name: 'master' },},//其他配置})
-
子應用注冊遠程組件
子應用的.umirc.ts中,1、添加mf插件、2、添加mf字段
mf中的remotes指定訪問的遠程組件地址和他的name,基座最終會被部署到80端口,所以我們的entry就是//
localhost:80/remote.js'
,,remote.js就是會將基座中src/exposes下的文件打包進去。(ps:在本地開發階段可以寫基座項目啟動的地址,比如 //localhost:8080/remote.js
)
shared字段填這個子應用用到的遠程包,因為我們會將所有的組件寫入components,所以直接這樣寫好就行了。
plugins: ['@umijs/plugins/dist/qiankun', '@umijs/plugins/dist/model', '@umijs/plugins/dist/mf'],//添加 @umijs/plugins/dist/mf'mf: {remotes: [{aliasName: 'masterAppXXX',//一個別名name: 'master', // 對應基座應用的 nameentry: '//localhost:80/remote.js', // 基座應用中導出的共享包的入口},],// 聲明共享依賴shared: {'components': {singleton: true,//單例,整個應用只存在一個,防止一個庫加載多個版本。eager: false,//控制共享模塊的加載時機,默認為false:異步加載,實際用的時候加載;為true時指:同步加載,應用啟動就加載,使用于本地開發的時候。requiredVersion: '^1.0.1',},},},
-
使用遠程組件
隨便在子應用中找個文件用遠程的PureButton組件
//impoer MasterApp from '別名/包名'
import MasterApp from 'masterAppXXX/components';
//因為是默認導出,所以需要拿到默認導出的東西后解構
const { PureButton} = MasterApp;
export default function Foo(){return (<><PureButton btnStr="555" /></>)
}
五、應用之間的通信
官網地址:
微前端
useModel
基座
在基座中定義了一些model,并且在app.jsx這個入口文件中導出子應用中需要使用的model
入口文件:app.tsx中導出useQiankunStateForSlave函數。
// 子應用(需要子應用是umi項目)獲取主應用的全局狀態,需要在app.tsx導出useQiankunStateForSlave供umi使用
interface QiankunState {site: string;
}
export function useQiankunStateForSlave(): QiankunState {const { site } = useModel('site');return {site,//導出site供子應用使用};
}
子應用
在子應用中用useModel('@@qiankunStateFromMaster')
即可拿到導出的數據
import { useModel } from 'umi';export default function HomePage() {// 獲取主應用的actionsconst model = useModel('@@qiankunStateFromMaster');console.log(model?.site)return <>...<>}
六、keep-alive
菜單切換時會重新加載子應用,我們能手動來加載子應用的顯示隱藏來實現keep-alive的效果,umimax已經為我們提供好了【MicroAppWithMemoHistory】組件,直接用就行。
路由注冊時使用這個組件,routes.ts:
export default [ ...['accountService', 'recordCenter'].map((base) => ({path: `/${base}/*`,component: './MicroAppWrapper',})),
]
組件代碼:
import { useMemo } from 'react';
import { MicroAppWithMemoHistory } from '@umijs/max';const map = {'accountService': 'qijing','recordCenter': 'haitu',
};function MicroAppWrapper() {const info = useMemo(() => {const pathname = window.location.pathname;const base = pathname.split('/')[1];return {name: map[base],pathname,};}, []);return <MicroAppWithMemoHistory name={info.name} url={info.pathname} />;
}export default MicroAppWrapper;
?
nginx部署
官網地址:
入門教程 - qiankun
-
可以將子應用和基座部署在同一個server,也可以部署在不同的server下。
一、部署在同一個server
部署在同一個server下,
-
子應用的路由base需要和基座的activeRule保持一致。
-
子應用的entery則是需要與其nginx路徑一致
基座注冊:
qiankun: {master: {apps: [{name: 'sub-umi-1', //子應用的名稱entry: '/sub/umi1/', //子應用的入口地址,nginx配置的目錄activeRule: '/app-umi1-history', //子應用的激活規則,指路由sandbox: {strictStyleIsolation: true, //嚴格樣式隔離},},],},},
子應用.umirc.ts
base: '/app-umi2-history', //供基座訪問的路由前綴,會和activeRule一樣// 用qiankun插件后默認base為包名publicPath: '/sub/umi2/', //資源的前綴,會和nginx中存放的目錄保持一次
二、部署在不同的server
部署在不同的server需要為子應用的server添加跨域
nginx.conf內容:
# main主應用server {listen 80;server_name localhost;# CORS 配置add_header Access-Control-Allow-Origin '*' always;add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS' always;add_header Access-Control-Allow-Headers '*' always;add_header Access-Control-Allow-Credentials 'true' always;if ($request_method = 'OPTIONS') {return 204;}location / {root html/main;index index.html index.htm;try_files $uri $uri/ /index.html;}# API 代理配置location ^~ /auth {proxy_pass http://mall-center.dev.springbeetle.top;}location ^~ /perm {proxy_pass http://mall-center.dev.springbeetle.top;}# 處理子應用的代理# location ^~ /qiankun/react {# proxy_pass http://localhost:5173;# }error_page 500 502 503 504 /50x.html;location = /50x.html {root html;}}# umi子應用server {listen 5175;server_name localhost;# # 添加5174全局 CORS 配置add_header Access-Control-Allow-Origin '*' always;add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS' always;add_header Access-Control-Allow-Headers '*' always;add_header Access-Control-Allow-Credentials 'true' always;if ($request_method = 'OPTIONS') {return 204;}location / {root html/qiankun/umi;index index.html index.htm;try_files $uri $uri/ /index.html;}# API 代理配置# location ^~ /auth {# proxy_pass http://mall-center.dev.springbeetle.top;# }error_page 500 502 503 504 /50x.html;location = /50x.html {root html;}}