基於 qiankun 的微前端應用實踐

本文做者:張延卿javascript

業務背景

雲音樂廣告 Dsp(需求方平臺)平臺分爲合約平臺(Vue 框架)和競價平臺(React 框架),因歷史緣由框架選型未能統一,最近來了新需求,須要同時在兩個平臺增長同樣的模塊,由於都是 Dsp 平臺,後期這樣的需求可能會不少,因此考慮到組件複用以及下降維護成本,在想怎麼統一技術棧,把 React 系統塞到 Vue 項目中進行呈現。html

項目應用結構

系統是傳統的左右佈局,左側側邊欄展現菜單欄,頭部導航展現基礎信息,應用內容所有填充到藍色的內容區。前端

說實話,第一反應我直接想嵌套 iframe ,可是應用過 iframe 技術的,你們都知道它的痛:vue

  • 瀏覽器歷史棧問題前進 / 後退
    不管你在 iframe 裏潛行了多深,你退一步就是一萬步,這個體驗真的很難受
  • 應用通訊
    有時候主應用可能只想知道子系統的 URL 參數,可是 iframe 應用跟它不一樣源,你就得想點其餘辦法去獲取參數了,咱們最經常使用的就是 postMessage
  • 緩存
    iframe 應用更新上線後,打開系統會發現系統命中緩存顯示舊內容,須要用時間戳方案解決或強制刷新


另外就是使用 MPA + 路由分發,當用戶訪問頁面時,由 Nginx 等負責根據路由分發到不一樣的業務應用,由各個業務應用完成資源的組裝後返回給瀏覽器,這種方式就須要把界面、導航都作成相似的樣子。
java

  • 優勢
    • 多框架開發;
    • 獨立部署運行;
    • 應用之間徹底隔離。
  • 缺點
    • 體驗差,每一個獨立應用加載時間較長;
    • 由於徹底隔離,致使在導航、頂部這些通用的地方改動大,複用性變的不好。


 還有就是目前比較主流的幾種微前端方案:react


  • 基座模式:主要基於路由分發,由一個基座應用監聽路由,按照路由規則去加載不一樣的應用,以實現應用間解耦
  • EMP:Webpack5 Module Federation,去中心化的微前端方案,能夠在實現應用隔離的基礎上,輕鬆實現應用間的資源共享和通訊;

總的來講,iframe 主要用於簡單而且性能要求不高的第三方系統;MPA 不管在實現成本和體驗上面都不能知足當前業務需求;基座模式和 EMP 都是不錯的選擇,因 qiankun 在業內使用比較廣,較爲成熟,最後仍是選擇了 qiankunwebpack

乾坤(qiankun)

qiankun(乾坤)是由螞蟻金服推出的基於Single-Spa實現的前端微服務框架,本質上仍是路由分發式的服務框架,不一樣於本來 Single-Spa 採用 JS Entry 加載子應用的方案,qiankun 採用 HTML Entry 方式進行了替代優化。git

JS Entry的使用限制要求github

  • 限制一個 JS 入口文件
  • 圖片、CSS 等靜態資源須要打包到 JS 裏
  • Code Splitting 沒法應用


對比 JS Entry, HTML Entry 使用就方便太多了,項目配置給定入口文件後,qiankun 會自行 Fetch 請求資源,解析出 JS 和 CSS 文件資源後,插入到給定的容器中,完美~
web

HTML Entry

JS Entry 的方式一般是子應用將資源打成一個 Entry Script, 相似 Single-Spa 的 例子

HTML Entry 則是使用 HTML 格式進行子應用資源的組織,主應用經過 Fetch html 的方式獲取子應用的靜態資源,同時將 HTML Document 做爲子節點塞到主應用的容器中。可讀性和維護性更高,更接近最後頁面掛載後的效果,也不存在須要雙向轉義的問題。

方案實踐

因爲 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

注意點:咱們選擇在 mounted 生命週期中進行初始化配置,是爲了保證掛載容器必定存在

咱們來經過圖示具體理解一下 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);
      // 關閉主機檢查,使微應用能夠被 fetch
      config.disableHostCheck = true;
      // 配置跨域請求頭,解決開發環境的跨域問題
      config.headers = {
        "Access-Control-Allow-Origin": "*",
      };
      // 配置 history 模式
      config.historyApiFallback = true;

      return config;
    };
  },
};
複製代碼

注意:配置的修改成了達到兩個目的,一個是暴露生命週期函數給主應用調用,第二點是容許跨域訪問,修改的注意點能夠參考代碼的註釋。

  • 暴露生命週期: UMD 可讓 qiankun 按應用名稱匹配到生命週期函數
  • 跨域配置: 主應用是經過 Fetch 獲取資源,因此爲了解決跨域問題,必須設置容許跨域訪問

小結:跳轉流程梳理,在主應用 router 中定義子應用跳轉 path ,以下圖,在調用組件 mounted 生命週期中使用 qiankun 暴露的 loadMicroApp 方法加載子應用,跳轉到子應用定義的路由,同時使用 addGlobalUncaughtErrorHandlerremoveGlobalUncaughtErrorHandler 監聽並處理異常狀況(例如子應用加載失敗),當子應用監聽到跳轉路由時,加載子應用(上述 <Router> 組件中)定義的 component,完成主應用到子應用的跳轉。

{
    path: '/xxx',
    component: Layout,
    children: [
      {
        path: '/xxx',
        component: () => import('@/micro/app/react'),
        meta: { title: 'xxx', icon: 'user' }
      }
    ]
  },
複製代碼

項目中遇到的問題

一、子應用未成功加載

   若是項目啓動完成後,發現子應用系統沒有加載,咱們應該打開控制檯分析緣由:

  • 控制檯無報錯:子應用未激活,檢查激活規則配置是否正確
  • 掛載容器未找到:檢查容器 DIV 是否在 qiankun start 時必定存在,如不能保證需設法在 DOM 掛載後執行。


二、基座應用路由模式

   基座應用項目是 hash 模式路由,這種狀況下子應用的路由模式必須跟主應用保持一致,不然會加載異常。緣由很簡單,假設子應用採用 history 模式,每次切換路由都會改變 pathname,這個時候很難再經過激活規則去匹配到子應用,形成子應用 unmount

三、CSS 樣式錯亂

   因爲默認狀況下 qiankun 並不會開啓 CSS 沙箱進行樣式隔離,當主應用和子應用產生樣式錯亂時,咱們能夠啓用 { strictStyleIsolation: true } 配置開啓嚴格隔離樣式,這個時候會用 Shadow Dom 節點包裹子應用,相信你們看到這個也很熟悉,和微信小程序中頁面和自定義組件的樣式隔離方案一致。

四、另外,在接入過程當中,總結了幾個須要注意的點

  • 雖然 qiankun 支持 jQuery,但對多頁應用的老項目接入不是很友好,須要每一個頁面都修改,成本也很高,這類老項目接入仍是比較推薦 iframe ;
  • 由於 qiankun 的方式,是經過 HTML-Entry 抽取 JS 文件和 DOM 結構的,實際上和主應用共用的是同一個 Document,若是子應用和主應用同時定義了相同事件,會互相影響,如,用 onClickaddEventListener<body>添加了一個點擊事件,JS 沙箱並不能消除它的影響,還得靠平時的代碼規範
  • 部署上有點繁瑣,須要手動解決跨域問題


五、將來可能須要考慮一些問題

  • 公用組件依賴複用:項目中避免不了的好比請求庫的封裝,我可能並不想在子應用中再寫一套一樣的請求封裝代碼
  • 自動化注入:每個子應用改造的過程其實也是挺麻煩的事情,可是其實大多的工做都是標準化流程,在考慮經過腳本自動註冊子應用,實現自動化

總結

其實寫下來整個項目,最大的感覺 qiankun 的開箱可用性很是強,須要更改的項目配置基本不多,固然遇到的一些坑點也確定是踩過才能更清晰。

若是文章有什麼問題或者錯誤,歡迎指正交流,謝謝!

本文發佈自 網易雲音樂大前端團隊,文章未經受權禁止任何形式的轉載。咱們常年招收前端、iOS、Android,若是你準備換工做,又剛好喜歡雲音樂,那就加入咱們 grp.music-fe (at) corp.netease.com!

相關文章
相關標籤/搜索