首頁>技術>

背景

在中臺業務中,交易流轉、業務運營和商戶賦能等功能,主要集中在兩個系統中(暫且命名為 inner/outer )。兩個系統基座(功能框架)類似,以 inner 系統為例,如圖:

inner系統基座

業務現狀問題

維護迭代,隨時間延續是不可避免的

至今,inner/outer 均有以下特點:

頁面結構繁雜 分類較多,選單頁面多;佈局五花八門,不統一技術棧不統一 歷史原因,存在 jquery、靜態模板、react 等技術棧許可權不統一 不同使用者,許可權不一樣,使用的功能模組不同專案管理不統一 部分功能模組是由業務方維護;同一功能模組面向不同使用者角色,也需要在不同系統中使用

初次接觸上述問題時,閃現在腦海裡的是:用 iframe 呀。確實,剛開始也是這樣做的。

問題暴露,在維護迭代中是個契機

系統在一個長時間跨度的執行下,隨著維護人員的變遷、使用人群的增多,更多的問題也接踵而至:

樣式不統一

由於沒有統一規範,每個功能模組在不同的開發者鍵盤下設想的結構不同,輸出的風格也不統一,使整個系統看起來略顯雜亂。

瀏覽器前進/後退

首先,iframe 頁面沒有自己的歷史記錄,使用的是基座(父頁面)的瀏覽歷史。所以,當iframe 頁在內部進行跳轉時,瀏覽器位址列無變化,基座中載入的 src 資源也無變化,當瀏覽器重新整理時,無法停留在iframe內部跳轉後的頁面上,需要使用者重新走一遍操作,體驗上會大打折扣。

彈窗遮罩層覆蓋可視範圍

iframe 頁產生的彈窗,一般只能遮罩 iframe 區域。

頁面間訊息傳遞

與基座非同源下,iframe 無法直接獲取基座 url 的引數,訊息傳遞需要週轉一下,如使用postmessage來實現;而動態建立的 iframe 頁,或許還需要藉助本地儲存等。

頁面快取

iframe 資源變更上線後,開啟系統會發現 iframe 頁依舊是老資源。需要用時間戳方案或強制重新整理。

載入異常處理

與基座非同源下,onerror 事件無法使用。使用 try catch 解決此問題,嘗試獲取 contentDocument 時將丟擲異常

改進實踐 - 微前端

實踐新技術,在問題暴露時是方向

大多數工程師,包括我,一邊兒嘴裡說著:學不動啦!一邊兒想嘗試一些新方式來優化系統。

結合問題分類,有思考一些嘗試方向,如:

中後臺 UI 規範:歷經迭代,百花齊放,然而更需要的是找到合適我司的風格,保持一致性。

另外,大網際網路時代,從工程角度看,社群對類似系統的探索有很多,除了 iframe 外,也有不少相對成熟的替代方案:

1. single-spa

2. qiankun

提起這兩個,就要提一下微前端理念,目前社群有很多關於微前端架構的介紹,這裡簡單提一下:

Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently. — Micro Frontends

大致是說,微前端有以下特點:

技術棧無關:基座不限制子應用的技術棧完全獨立:子應用獨立部署維護,接入時基座同步更新;又可獨立執行

基於此,不難想到:iframe 也是符合微前端理念的。那其他方案又是如何做的呢?

single-spa

社群裡 single-spa 介紹也不少。根據 demo 比葫蘆畫瓢,可以知道它的架構分佈:

single-spa架構

啟動服務的配置主要是在single-spa-config 檔案中,包含專案名稱、 專案地址、路由配置等:

// single-spa-config.jsimport {registerApplication, start } from 'single-spa';// 子應用唯一IDconst microAppName = 'react';// 子應用入口const loadingFunction = () => import('./react/app.js');// url字首校驗const activityFunction = location => location.pathname.startsWith('/react');// 註冊registerApplication(  microAppName,  loadingFunction,  activityFunction);//singleSpa 啟動start();

single-spa 讓基座和子應用共用一個 document,那就需要對子應用進行改造:把子專案的容器和生成的 js 插入到基座專案中。

不需要 HTML 入口檔案js 入口檔案匯出的模組,必須包括 bootstrap、mount 和 unmount 三個方法
<div id='micro-react'></div><script src=/js/chunk-vendors.js> </script><script src=/js/app.js> </script>

不過這種方式需要對現有專案的打包方式和配置項進行改造,成本很大。所以,對於已有的工程專案,我選擇了放棄使用。

qiankun

qiankun 也是社群提到比較多的一個開源框架,是基於single-spa 實現了開箱即用。可以採用html entry 方式接入子應用,且子應用只需暴露一些生命週期,改動較少。【少】這個點,真是讓我躍躍欲試。

目前我司業務場景是單例項模式(一個執行時只有一個子應用被啟用),我們可以根據一張圖來看看單例項下以html entry方式 qiankun 實現流程:

qiankun原理

如上圖所示,一個子應用的全過程有:

初始化配置,匹配出子應用初始化子應用,載入對應的 html 資源,以及建立 JS 沙箱環境掛載子應用,執行生命週期鉤子函式解除安裝子應用,當切換路由時,執行各解除安裝鉤子函式,以及解除安裝 JS 沙箱環境,清除容器節點

具體實現細節,大家可以參考qiankun原始碼。

實踐基座

從規範化開發角度,我司的中後臺系統是基於 umi 開發(詳細可參考我們之前的文章 umi 中後臺專案實踐)。在構建主應用使用了配套的 qiankun 外掛:@umijs/plugin-qiankun。

1. 初始化配置項,註冊子應用

外掛安裝之後,我們可以在入口檔案裡配置:

此處主要以執行時為例

// app.jsexport const qiankun = Promise.resolve().then(() => ({  // 執行時註冊子應用資訊  apps: [    {      // 結算單管理      name: 'settlement', // 唯一id,與子應用的library 保持一致      entry: '//xxx', // html entry      history: 'hash', // 子應用的 history 配置,預設為當前主應用 history 配置      container: '#root-content', // 子應用存放節點      mountElementId: 'root-content' // 子應用存放節點    }, {      // 公告訊息      name: 'news', // 唯一id,與子應用的library 保持一致      entry: '//xxx', // html entry      history: 'hash', // 子應用的 history 配置,預設為當前主應用 history 配置      container: '#root-content', // 子應用存放節點      mountElementId: 'root-content' // 子應用存放節點    }  ],  jsSandbox: { strictStyleIsolation: true }, // 是否啟用 js 沙箱,預設為 false  prefetch: true, // 是否啟用 prefetch 特性,預設為 true  lifeCycles: {    // see /file/2020/09/25/20200925140958_14.jpg 裝載子應用,在路由配置中使用microApp來獲取相應的子應用名稱:

// router.config.jsexport default [  {    path: '/',    component: '../layouts/BasicLayout',    routes: [      ...      {        path: '/settlement/list',        name: '結算單管理',        icon: 'RedEnvelopeOutlined',        microApp: 'settlement',  // 子應用唯一id      },      {        path: '/settlement/detail/:id',        name: '結算單管理',        icon: 'RedEnvelopeOutlined',        microApp: 'settlement', // 子應用唯一id        hideInMenu: true,      },      ...      ...      {        component: './404',      },    ],  },  {    component: './404',  },]

以上就是基座的改動點,看起來程式碼侵入性很少。

子應用

在子應用中,需要做如下的配置

1. 入口檔案設定 baseName,及暴露鉤子函式

//設定主應用下的子應用路由名稱空間const BASE_NAME = window.__POWERED_BY_QIANKUN__ ? "/settlement" : "";// 獨立執行時,直接掛載應用if (!window.__POWERED_BY_QIANKUN__) {  effectRender();}// 在子應用初始化的時候呼叫一次export async function bootstrap() {  console.log("ReactMicroApp bootstraped");}export async function mount(props) {  console.log("ReactMicroApp mount", props);  effectRender(props);}//解除安裝子應用的應用例項export async function unmount(props) {  const { container } = props || {};  ReactDOM.unmountComponentAtNode(document.getElementById('root-content')  );}

2. webpack 配置中,需要設定輸出為 umd 格式:

// 設定別名merge: {  plugins: [new webpack.ProvidePlugin({    React: 'react',    PropTypes: 'prop-types'  })],  output: {    library: `[name]`, // 子應用的包名,這裡與主應用中註冊子應用名稱一致    libraryTarget: "umd", // 所有的模組定義下都可執行的方式    jsonpFunction: `webpackJsonp_ReactMicroApp`, // 按需載入  }} //自定義webpack配置

OK,配置完成!

理論上,啟動專案,部署等都應該沒有問題了。咦,開啟地址,頁面一直在 loading,控制檯一堆報錯,看起來要踩一踩坑了。

踩坑

1. 版本一致性

如果主應用和子應用都是基於 umi 框架,在使用 @umijs/umi-plugin-qiankun 外掛時,要使用同一個版本,否則子應用報錯。

2. 跨域

qiankun 是通過 fetch 去獲取子應用資源的,所以必須支援跨域

const mountDOM = appWrapperGetter();const { fetch } = frameworkConfiguration;const referenceNode = mountDOM.contains(refChild) ? refChild : null;if (src) {  execScripts(null, [src], proxy, {    fetch,    strictGlobal: !singular,    beforeExec: () => {      Object.defineProperty(document, 'currentScript', {        get(): any {          return element;        },        configurable: true,      })    };  })}

比如:基座地址為 b.zhuanzhuan.com, 子應用為 d.zhuanzhuan.com 。當基座去載入子應用時,會出現跨域錯誤。

曾經有采用通過 Node 服務做一層中轉,跳過跨域問題:

  ....  maxDays: 3, // 保留最大天數日誌檔案}// 代理config.httpProxy = {  '/cors': {    target: 'https://d.zhuanzhuan.com',    pathRewrite: {'^/cors' : ''}  }};return config

但考慮應用的訪問量,以及線上線下環境維護成本,覺得必要性不是很大,最終選擇通過 nginx 解決跨域。

3. 子應用內部跳轉

子應用內部跳轉,需要在基座路由上提前註冊好,否則在跳轉後,頁面識別不到。

{  path: '/settlement/detail/:id',  name: '結算單管理',  icon: 'RedEnvelopeOutlined',  microApp: 'settlement',  hideInMenu: true,},

4. css 汙染

qiankun 只能解決子應用之間的樣式相互汙染,不能解決子應用樣式汙染基座的樣式。比如:當切換到某個子應用時,左側選單欄突然往右移了。

系統右移

檢視控制檯,不難發現,子應用的相同模組覆蓋了基座:

樣式覆蓋

這個問題,可以通過改變基座的字首來解決,搞一個postcss 外掛給不同的元件新增不同的字首。

這裡補充一個 css 隔離常用的方式如:css字首、CSS Module、動態載入/解除安裝樣式表。

qiankun 中 css沙箱機制 採用的是 動態載入/解除安裝樣式表。

重寫 HTMLHeadElement.prototype.appendChild 事件
// Just overwrite it while it have not been overwriteif (  HTMLHeadElement.prototype.appendChild === rawHeadAppendChild &&  HTMLBodyElement.prototype.appendChild === rawBodyAppendChild &&  HTMLHeadElement.prototype.insertBefore === rawHeadInsertBefore) {  HTMLHeadElement.prototype.appendChild = getOverwrittenAppendChildOrInsertBefore({    rawDOMAppendOrInsertBefore: rawHeadAppendChild,    appName,    appWrapperGetter,    proxy,    singular,    dynamicStyleSheetElements,    scopedCSS,    excludeAssetFilter,  }) as typeof rawHeadAppendChild;....
當子應用載入時,在 head 插入 style/link ; 當解除安裝時,直接移除。
// Just overwrite it while it have not been overwriteif (  HTMLHeadElement.prototype.removeChild === rawHeadRemoveChild &&  HTMLBodyElement.prototype.removeChild === rawBodyRemoveChild) {  HTMLHeadElement.prototype.removeChild = getNewRemoveChild({    appWrapperGetter,    headOrBodyRemoveChild: rawHeadRemoveChild,  });  HTMLBodyElement.prototype.removeChild = getNewRemoveChild({    appWrapperGetter,    headOrBodyRemoveChild: rawBodyRemoveChild,  });}

看起來很完美,但有時候會出現,基座樣式丟失的問題。這個跟子應用解除安裝的時機有關係:當切換子應用時,當前子應用沙箱環境還未被解除安裝,但基座 css 已被插入,當解除安裝時會連帶基座 css 一起被清除。

5. 錯誤捕獲,降級處理

若子應用載入失敗,需要給相應的提示或動態插入iframe頁:

// iframe.jsexport default ({ sourceUrl }) =>  <iframe    src={sourceUrl}    title="xxxx"    width="100%"    height="100%"    border="0"    frameBorder="0"  />import { render } from 'react-dom';// 全域性未捕獲異常處理器addGlobalUncaughtErrorHandler((event) => {  console.error(event);  const { message, location: { hash } } = event;  // 載入失敗時提示  if (message && message.includes("died in status LOADING_SOURCE_CODE")) {    Modal.Confirm({    content: "子應用載入失敗,請檢查應用是否可執行"    onOk: () => import('./Inframe.js')    });  }});

6. 路由懶載入樣式丟失

子應用中存在按需載入的路由,在載入時頁面樣式丟失,這是官方庫產生的問題,issue 裡已有大佬提 PR 啦,可參考 https://github.com/umijs/qiankun/issues/857

以上,就是不完全踩坑。

後續

持續性思考會帶來的技術紅利

此次接入 qiankun,也只是處於表面應用。後續我們更要思考接入它之後更深的工程價值,如:

- 自動接入 qiankun

結合我司已有的腳手架和 umi 模板,額外新增一個命令,自動註冊子應用,做到自動化。

- 子應用間元件共享

基座和子應用大概率都用到了 react/dva 等,是否可以在基座載入完之後,子應用直接複用?當然,淺顯思考應該少不了 webpack 的 externals。

最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 超優秀 Vue3.0 開源UI元件庫