基于 qiankun 的微前端应用实践
業務背景
云音樂廣告 Dsp(需求方平臺)平臺分為合約平臺(Vue 框架)和競價平臺(React 框架),因歷史原因框架選型未能統一,最近來了新需求,需要同時在兩個平臺增加一樣的模塊,因為都是 Dsp 平臺,后期這樣的需求可能會很多,所以考慮到組件復用以及降低維護成本,在想怎么統一技術棧,把 React 系統塞到 Vue 項目中進行呈現。
系統是傳統的左右布局,左側側邊欄展示菜單欄,頭部導航展示基礎信息,應用內容全部填充到藍色的內容區。說實話,第一反應我直接想嵌套 iframe ,但是應用過 iframe 技術的,大家都知道它的痛:
- 瀏覽器歷史棧問題前進 / 后退
 無論你在 iframe 里潛行了多深,你退一步就是一萬步,這個體驗真的很難受
- 應用通信
 有時候主應用可能只想知道子系統的 URL 參數,但是 iframe 應用跟它不同源,你就得想點其他辦法去獲取參數了,我們最常用的就是?postMessage?了
- 緩存
 iframe?應用更新上線后,打開系統會發現系統命中緩存顯示舊內容,需要用時間戳方案解決或強制刷新
 另外就是使用 MPA + 路由分發,當用戶訪問頁面時,由 Nginx 等負責根據路由分發到不同的業務應用,由各個業務應用完成資源的組裝后返回給瀏覽器,這種方式就需要把界面、導航都做成類似的樣子。
 ?
?
?
- 優點: - 多框架開發;
- 獨立部署運行;
- 應用之間完全隔離。
 
?
- 缺點: - 體驗差,每個獨立應用加載時間較長;
- 因為完全隔離,導致在導航、頂部這些通用的地方改動大,復用性變的很差。
 
?
 還有就是目前比較主流的幾種微前端方案:
?
?
?
- 基座模式:主要基于路由分發,由一個基座應用監聽路由,按照路由規則去加載不同的應用,以實現應用間解耦
- EMP:Webpack5 Module Federation,去中心化的微前端方案,可以在實現應用隔離的基礎上,輕松實現應用間的資源共享和通信;
乾坤(qiankun)
qiankun(乾坤)是由螞蟻金服推出的基于Single-Spa實現的前端微服務框架,本質上還是路由分發式的服務框架,不同于原本 Single-Spa 采用 JS Entry 加載子應用的方案,qiankun 采用 HTML Entry 方式進行了替代優化。
JS Entry的使用限制要求:
- 限制一個 JS 入口文件
- 圖片、CSS 等靜態資源需要打包到 JS 里
- Code Splitting 無法應用
 對比 JS Entry, HTML Entry 使用就方便太多了,項目配置給定入口文件后,qiankun 會自行 Fetch 請求資源,解析出 JS 和 CSS 文件資源后,插入到給定的容器中,完美~
HTML Entry 則是使用 HTML 格式進行子應用資源的組織,主應用通過 Fetch html 的方式獲取子應用的靜態資源,同時將 HTML Document 作為子節點塞到主應用的容器中??勺x性和維護性更高,更接近最后頁面掛載后的效果,也不存在需要雙向轉義的問題。
方案實踐
由于 Vue 項目已經開發完成,我們需要在原始項目中進行改造,很明顯選定 Vue 項目作為基座應用,新需求開發采用 Create React App 搭建 React 子應用,接下來我們看一下具體實現
基座應用改造
基座(main)采用是的 vue-cli 搭建的,我們保持其原本的代碼結構和邏輯不變,在此基礎上單獨為子應用提供一個掛載的容器 DIV,同樣填充在相同的內容展示區域。
qiankun 只需要在基座應用中引入,為了方便管理,我們新增目錄,命名為 micro ,標識目錄里面是微前端改造代碼,進行全局配置初始化,改造如下:
路由配置文件 app.js
// 路由配置 const apps = [{name: 'ReactMicroApp',entry: '//localhost:10100',container: '#frame',activeRule: '/react'} ];應用配置注冊函數
import { registerMicroApps, start } from "qiankun"; import apps from "./apps";// 注冊子應用函數,包裝成高階函數,方便后期如果有參數注入修改app配置 export const registerApp = () => registerMicroApps(apps);// 導出 qiankun 的啟動函數 export default start;Layout 組件
<section class="app-main"><transition v-show="$route.name" name="fade-transform" mode="out-in"><!-- 主應用渲染區,用于掛載主應用路由觸發的組件 --><router-view /></transition><!-- 子應用渲染區,用于掛載子應用節點 --><div id="frame" /> </section>import startQiankun, { registerApp } from "../../../micro"; export default {name: "AppMain",mounted() {// 初始化配置registerApp();startQiankun();}, };這里會用到 qiankun 的兩個重要的 API :
- registerMicroApps
- start
我們來通過圖示具體理解一下 qiankun 注冊子應用的過程:
- 依賴注入后,會先初始化標識變量參數?xx_QIANKUN__,用于子應用判斷所處環境等
- 當 qiankun 會通過?activeRule?的規則來判斷是否激活子應用 - `activeRule` 為字符串時,以路由攔截方式進行自主攔截
- `activeRule` 為函數時,根據函數返回值判斷是否激活
 
?
- 當激活子應用時,會通過 HTML-Entry 來解析子應用靜態資源地址,掛載到對應容器上
- 創建沙箱環境,查找子應用生命周期函數,初始化子應用
打造 qiankun 子應用
我們基于 Create React App 創建一個 React 項目應用,由上述的流程描述,我們知道子應用得向外暴露一系列生命周期函數供 qiankun 調用,在 index.js 文件中進行改造:
增加 public-path.js 文件
目錄外層添加 `public-path.js` 文件,當子應用掛載在主應用下時,如果我們的一些靜態資源沿用了 `publicPath=/` 的配置,我們拿到的域名將會是主應用域名,這個時候就會造成資源加載出錯,好在 Webpack 提供了修改方法,如下:if (window.__POWERED_BY_QIANKUN__) {__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; }路由 base 設置
因為通常來說,主應用會攔截瀏覽器路由變化以激活加載子應用。比如,上述的代碼里我們的路由配置,激活規則寫了 `activeRule: /react`,這是什么意思呢?這意味著,當瀏覽器 `pathname` 匹配到 `/react` 時,會激活子應用,但是如果我們的子應用路由配置是下面這樣的:<Router> <Route exact path="/" component={Home} /><Route path="/list" component={List} /> </Router>我們怎么實現域名?/react?能正確加載對應的組件呢?大家一定經歷過用域名二級目錄訪問的需求,這里是一樣的,我們判斷是否在 qiankun 環境下,調整下 base 即可,如下:
const BASE_NAME = window.__POWERED_BY_QIANKUN__ ? "/react" : ""; ... <Router base={BASE_NAME}> ... </Router>增加生命周期函數
子應用的入口文件加入生命周期函數初始化,方便主應用調用資源完成后按應用名稱調用子應用的生命周期/*** bootstrap 只會在微應用初始化的時候調用一次,下次微應用重新進入時會直接調用 mount 鉤子,不會再重復觸發 bootstrap。* 通常我們可以在這里做一些全局變量的初始化,比如不會在 unmount 階段被銷毀的應用級別的緩存等。*/ export async function bootstrap() {console.log("bootstraped"); }/*** 應用每次進入都會調用 mount 方法,通常我們在這里觸發應用的渲染方法*/ export async function mount(props) {console.log("mount", props);render(props); }/*** 應用每次切出/卸載 會調用的方法,通常在這里我們會卸載微應用的應用實例*/ export async function unmount() {console.log("unmount");ReactDOM.unmountComponentAtNode(document.getElementById("root")); } 注意:所有的生明周期函數都必須是 Promise修改打包配置
module.exports = {webpack: (config) => {// 微應用的包名,這里與主應用中注冊的微應用名稱一致config.output.library = `ReactMicroApp`;// 將你的 library 暴露為所有的模塊定義下都可運行的方式config.output.libraryTarget = "umd";// 按需加載相關,設置為 webpackJsonp_ReactMicroApp 即可config.output.jsonpFunction = `webpackJsonp_ReactMicroApp`;config.resolve.alias = {...config.resolve.alias,"@": path.resolve(__dirname, "src"),};return config;},devServer: function (configFunction) {return function (proxy, allowedHost) {const config = configFunction(proxy, allowedHost);// 關閉主機檢查,使微應用可以被 fetchconfig.disableHostCheck = true;// 配置跨域請求頭,解決開發環境的跨域問題config.headers = {"Access-Control-Allow-Origin": "*",};// 配置 history 模式config.historyApiFallback = true;return config;};}, }; 注意:配置的修改為了達到兩個目的,一個是暴露生命周期函數給主應用調用,第二點是允許跨域訪問,修改的注意點可以參考代碼的注釋。- 暴露生命周期: UMD 可以讓 qiankun 按應用名稱匹配到生命周期函數
- 跨域配置: 主應用是通過 Fetch 獲取資源,所以為了解決跨域問題,必須設置允許跨域訪問
項目中遇到的問題
1、子應用未成功加載
如果項目啟動完成后,發現子應用系統沒有加載,我們應該打開控制臺分析原因:
- 控制臺無報錯:子應用未激活,檢查激活規則配置是否正確
- 掛載容器未找到:檢查容器 DIV 是否在 qiankun?start?時一定存在,如不能保證需設法在 DOM 掛載后執行。
 2、基座應用路由模式
基座應用項目是 hash 模式路由,這種情況下子應用的路由模式必須跟主應用保持一致,否則會加載異常。原因很簡單,假設子應用采用 history 模式,每次切換路由都會改變 pathname,這個時候很難再通過激活規則去匹配到子應用,造成子應用 unmount
3、CSS 樣式錯亂
由于默認情況下 qiankun 并不會開啟 CSS 沙箱進行樣式隔離,當主應用和子應用產生樣式錯亂時,我們可以啟用?{ strictStyleIsolation: true }?配置開啟嚴格隔離樣式,這個時候會用 Shadow Dom 節點包裹子應用,相信大家看到這個也很熟悉,和微信小程序中頁面和自定義組件的樣式隔離方案一致。
4、另外,在接入過程中,總結了幾個需要注意的點
- 雖然 qiankun 支持 jQuery,但對多頁應用的老項目接入不是很友好,需要每個頁面都修改,成本也很高,這類老項目接入還是比較推薦 iframe ;
- 因為 qiankun 的方式,是通過 HTML-Entry 抽取 JS 文件和 DOM 結構的,實際上和主應用共用的是同一個 Document,如果子應用和主應用同時定義了相同事件,會互相影響,如,用?onClick?或?addEventListener?給 添加了一個點擊事件,JS 沙箱并不能消除它的影響,還得靠平時的代碼規范
- 部署上有點繁瑣,需要手動解決跨域問題
 5、未來可能需要考慮一些問題
- 公用組件依賴復用:項目中避免不了的比如請求庫的封裝,我可能并不想在子應用中再寫一套同樣的請求封裝代碼
- 自動化注入:每一個子應用改造的過程其實也是挺麻煩的事情,但是其實大多的工作都是標準化流程,在考慮通過腳本自動注冊子應用,實現自動化
總結
其實寫下來整個項目,最大的感受 qiankun 的開箱可用性非常強,需要更改的項目配置基本很少,當然遇到的一些坑點也肯定是踩過才能更清晰。
總結
以上是生活随笔為你收集整理的基于 qiankun 的微前端应用实践的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: Kafka Consumer多线程消费
- 下一篇: HTTP协议基础知识总结
