<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          iframe 接班人-微前端框架 qiankun 在中后臺(tái)系統(tǒng)實(shí)踐

          共 8590字,需瀏覽 18分鐘

           ·

          2020-11-29 02:52


          背景

          在轉(zhuǎn)轉(zhuǎn)的中臺(tái)業(yè)務(wù)中,交易流轉(zhuǎn)、業(yè)務(wù)運(yùn)營和商戶賦能等功能,主要集中在兩個(gè)系統(tǒng)中(暫且命名為 inner/outer )。兩個(gè)系統(tǒng)基座(功能框架)類似,以 inner 系統(tǒng)為例,如圖:

          inner系統(tǒng)基座

          業(yè)務(wù)現(xiàn)狀問題

          維護(hù)迭代,隨時(shí)間延續(xù)是不可避免的

          至今,inner/outer 均有以下特點(diǎn):

          • 頁面結(jié)構(gòu)繁雜 分類較多,菜單頁面多;布局五花八門,不統(tǒng)一
          • 技術(shù)棧不統(tǒng)一 歷史原因,存在 jquery靜態(tài)模板react 等技術(shù)棧
          • 權(quán)限不統(tǒng)一 不同用戶,權(quán)限不一樣,使用的功能模塊不同
          • 項(xiàng)目管理不統(tǒng)一 部分功能模塊是由業(yè)務(wù)方維護(hù);同一功能模塊面向不同用戶角色,也需要在不同系統(tǒng)中使用

          初次接觸上述問題時(shí),閃現(xiàn)在腦海里的是:用 iframe 呀。確實(shí),剛開始也是這樣做的。

          問題暴露,在維護(hù)迭代中是個(gè)契機(jī)

          系統(tǒng)在一個(gè)長時(shí)間跨度的運(yùn)行下,隨著維護(hù)人員的變遷、使用人群的增多,更多的問題也接踵而至:

          • 樣式不統(tǒng)一

          由于沒有統(tǒng)一規(guī)范,每個(gè)功能模塊在不同的開發(fā)者鍵盤下設(shè)想的結(jié)構(gòu)不同,輸出的風(fēng)格也不統(tǒng)一,使整個(gè)系統(tǒng)看起來略顯雜亂。

          • 瀏覽器前進(jìn)/后退

          首先,iframe 頁面沒有自己的歷史記錄,使用的是基座(父頁面)的瀏覽歷史。所以,當(dāng)iframe 頁在內(nèi)部進(jìn)行跳轉(zhuǎn)時(shí),瀏覽器地址欄無變化,基座中加載的 src 資源也無變化,當(dāng)瀏覽器刷新時(shí),無法停留在iframe內(nèi)部跳轉(zhuǎn)后的頁面上,需要用戶重新走一遍操作,體驗(yàn)上會(huì)大打折扣。

          • 彈窗遮罩層覆蓋可視范圍

          iframe 頁產(chǎn)生的彈窗,一般只能遮罩 iframe 區(qū)域。

          • 頁面間消息傳遞

          與基座非同源下,iframe 無法直接獲取基座 url 的參數(shù),消息傳遞需要周轉(zhuǎn)一下,如使用postmessage來實(shí)現(xiàn);而動(dòng)態(tài)創(chuàng)建的 iframe 頁,或許還需要借助本地存儲(chǔ)等。

          • 頁面緩存

          iframe 資源變更上線后,打開系統(tǒng)會(huì)發(fā)現(xiàn) iframe 頁依舊是老資源。需要用時(shí)間戳方案或強(qiáng)制刷新。

          • 加載異常處理

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


          以上問題,從業(yè)務(wù)價(jià)值看,對(duì)用戶的使用體驗(yàn)會(huì)有損失;從工程價(jià)值看,希望能通過技術(shù)提升業(yè)務(wù)體驗(yàn)的同時(shí),也提高系統(tǒng)的維護(hù)性。

          改進(jìn)實(shí)踐 - 微前端

          實(shí)踐新技術(shù),在問題暴露時(shí)是方向

          大多數(shù)工程師,包括我,一邊兒嘴里說著:學(xué)不動(dòng)啦!一邊兒想嘗試一些新方式來優(yōu)化系統(tǒng)。

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

          • 中后臺(tái) UI 規(guī)范:歷經(jīng)迭代,百花齊放,然而更需要的是找到合適我司的風(fēng)格,保持一致性。

            此部分這次不再細(xì)說,可以 關(guān)注我們公眾號(hào) - 大轉(zhuǎn)轉(zhuǎn) FE,后續(xù)我們會(huì)有專門的文章講這部分。

          另外,大互聯(lián)網(wǎng)時(shí)代,從工程角度看,社區(qū)對(duì)類似系統(tǒng)的探索有很多,除了 iframe 外,也有不少相對(duì)成熟的替代方案:

          1. single-spa

          2. qiankun

          提起這兩個(gè),就要提一下微前端理念,目前社區(qū)有很多關(guān)于微前端架構(gòu)的介紹,這里簡單提一下:

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

          大致是說,微前端有以下特點(diǎn):

          1. 技術(shù)棧無關(guān):基座不限制子應(yīng)用的技術(shù)棧
          2. 完全獨(dú)立:子應(yīng)用獨(dú)立部署維護(hù),接入時(shí)基座同步更新;又可獨(dú)立運(yùn)行

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

          single-spa

          社區(qū)里 single-spa 介紹也不少。根據(jù) demo 比葫蘆畫瓢,可以知道它的架構(gòu)分布:

          single-spa架構(gòu)

          啟動(dòng)服務(wù)的配置主要是在single-spa-config 文件中,包含項(xiàng)目名稱、 項(xiàng)目地址、路由配置等:

          //?single-spa-config.js
          import?{registerApplication,?start?}?from?'single-spa';

          //?子應(yīng)用唯一ID
          const?microAppName?=?'react';

          //?子應(yīng)用入口
          const?loadingFunction?=?()?=>?import('./react/app.js');

          //?url前綴校驗(yàn)
          const?activityFunction?=?location?=>?location.pathname.startsWith('/react');

          //?注冊(cè)
          registerApplication(
          ??microAppName,
          ??loadingFunction,
          ??activityFunction
          );

          //singleSpa?啟動(dòng)
          start();

          single-spa 讓基座和子應(yīng)用共用一個(gè) document,那就需要對(duì)子應(yīng)用進(jìn)行改造:把子項(xiàng)目的容器和生成的 js 插入到基座項(xiàng)目中。

          • 不需要 HTML 入口文件
          • js 入口文件導(dǎo)出的模塊,必須包括 bootstrapmountunmount 三個(gè)方法
          <div?id='micro-react'>div>
          <script?src=/js/chunk-vendors.js>?script>
          <script?src=/js/app.js>?script>

          不過這種方式需要對(duì)現(xiàn)有項(xiàng)目的打包方式和配置項(xiàng)進(jìn)行改造,成本很大。所以,對(duì)于已有的工程項(xiàng)目,我選擇了放棄使用。

          qiankun

          qiankun 也是社區(qū)提到比較多的一個(gè)開源框架,是基于single-spa 實(shí)現(xiàn)了開箱即用。可以采用html entry 方式接入子應(yīng)用,且子應(yīng)用只需暴露一些生命周期,改動(dòng)較少。【】這個(gè)點(diǎn),真是讓我躍躍欲試。

          目前我司業(yè)務(wù)場(chǎng)景是單實(shí)例模式(一個(gè)運(yùn)行時(shí)只有一個(gè)子應(yīng)用被激活),我們可以根據(jù)一張圖來看看單實(shí)例下以html entry方式 qiankun 實(shí)現(xiàn)流程:

          qiankun原理

          如上圖所示,一個(gè)子應(yīng)用的全過程有:

          • 初始化配置,匹配出子應(yīng)用
          • 初始化子應(yīng)用,加載對(duì)應(yīng)的 html 資源,以及創(chuàng)建 JS 沙箱環(huán)境
          • 掛載子應(yīng)用,執(zhí)行生命周期鉤子函數(shù)
          • 卸載子應(yīng)用,當(dāng)切換路由時(shí),執(zhí)行各卸載鉤子函數(shù),以及卸載 JS 沙箱環(huán)境,清除容器節(jié)點(diǎn)

          具體實(shí)現(xiàn)細(xì)節(jié),大家可以參考qiankun源碼。

          實(shí)踐

          • 基座

          從規(guī)范化開發(fā)角度,我司的中后臺(tái)系統(tǒng)是基于 umi 開發(fā)(詳細(xì)可參考我們之前的文章 umi 中后臺(tái)項(xiàng)目實(shí)踐)。在構(gòu)建主應(yīng)用使用了配套的 qiankun 插件:@umijs/plugin-qiankun

          1. 初始化配置項(xiàng),注冊(cè)子應(yīng)用

          插件安裝之后,我們可以在入口文件里配置:

          此處主要以運(yùn)行時(shí)為例

          //?app.js
          export?const?qiankun?=?Promise.resolve().then(()?=>?({
          ??//?運(yùn)行時(shí)注冊(cè)子應(yīng)用信息
          ??apps:?[
          ????{
          ??????//?結(jié)算單管理
          ??????name:?'settlement',?//?唯一id,與子應(yīng)用的library?保持一致
          ??????entry:?'//xxx',?//?html?entry
          ??????history:?'hash',?//?子應(yīng)用的?history?配置,默認(rèn)為當(dāng)前主應(yīng)用?history?配置
          ??????container:?'#root-content',?//?子應(yīng)用存放節(jié)點(diǎn)
          ??????mountElementId:?'root-content'?//?子應(yīng)用存放節(jié)點(diǎn)
          ????},?{
          ??????//?公告消息
          ??????name:?'news',?//?唯一id,與子應(yīng)用的library?保持一致
          ??????entry:?'//xxx',?//?html?entry
          ??????history:?'hash',?//?子應(yīng)用的?history?配置,默認(rèn)為當(dāng)前主應(yīng)用?history?配置
          ??????container:?'#root-content',?//?子應(yīng)用存放節(jié)點(diǎn)
          ??????mountElementId:?'root-content'?//?子應(yīng)用存放節(jié)點(diǎn)
          ????}
          ??],
          ??jsSandbox:?{?strictStyleIsolation:?true?},?//?是否啟用?js?沙箱,默認(rèn)為?false
          ??prefetch:?true,?//?是否啟用?prefetch?特性,默認(rèn)為?true
          ??lifeCycles:?{
          ????//?see?https://github.com/umijs/qiankun#registermicroapps
          ????beforeLoad:?(props)?=>?{
          ??????return?Promise.resolve(props).then(()?=>?loading())
          ????},
          ????afterMount:?(props)?=>?{
          ??????console.log('afterMount',?props)
          ????},
          ????afterUnmount:?(props)?=>?{
          ??????console.log('afterUnmount',?props)
          ????}
          ??}
          }))

          2. 裝載子應(yīng)用,在路由配置中使用microApp來獲取相應(yīng)的子應(yīng)用名稱:

          //?router.config.js
          export?default?[
          ??{
          ????path:?'/',
          ????component:?'../layouts/BasicLayout',
          ????routes:?[
          ??????...
          ??????{
          ????????path:?'/settlement/list',
          ????????name:?'結(jié)算單管理',
          ????????icon:?'RedEnvelopeOutlined',
          ????????microApp:?'settlement',??//?子應(yīng)用唯一id
          ??????},
          ??????{
          ????????path:?'/settlement/detail/:id',
          ????????name:?'結(jié)算單管理',
          ????????icon:?'RedEnvelopeOutlined',
          ????????microApp:?'settlement',?//?子應(yīng)用唯一id
          ????????hideInMenu:?true,
          ??????},
          ??????...
          ??????...
          ??????{
          ????????component:?'./404',
          ??????},
          ????],
          ??},
          ??{
          ????component:?'./404',
          ??},
          ]

          以上就是基座的改動(dòng)點(diǎn),看起來代碼侵入性很少。

          • 子應(yīng)用

          在子應(yīng)用中,需要做如下的配置

          1. 入口文件設(shè)置 baseName,及暴露鉤子函數(shù)

          //設(shè)置主應(yīng)用下的子應(yīng)用路由命名空間
          const?BASE_NAME?=?window.__POWERED_BY_QIANKUN__???"/settlement"?:?"";

          //?獨(dú)立運(yùn)行時(shí),直接掛載應(yīng)用
          if?(!window.__POWERED_BY_QIANKUN__)?{
          ??effectRender();
          }

          //?在子應(yīng)用初始化的時(shí)候調(diào)用一次
          export?async?function?bootstrap()?{
          ??console.log("ReactMicroApp?bootstraped");
          }

          export?async?function?mount(props)?{
          ??console.log("ReactMicroApp?mount",?props);
          ??effectRender(props);
          }

          //卸載子應(yīng)用的應(yīng)用實(shí)例
          export?async?function?unmount(props)?{
          ??const?{?container?}?=?props?||?{};
          ??ReactDOM.unmountComponentAtNode(document.getElementById('root-content')
          ??);
          }

          2. webpack 配置中,需要設(shè)置輸出為 umd 格式:

          //?設(shè)置別名
          merge:?{
          ??plugins:?[new?webpack.ProvidePlugin({
          ????React:?'react',
          ????PropTypes:?'prop-types'
          ??})],
          ??output:?{
          ????library:?`[name]`,?//?子應(yīng)用的包名,這里與主應(yīng)用中注冊(cè)子應(yīng)用名稱一致
          ????libraryTarget:?"umd",?//?所有的模塊定義下都可運(yùn)行的方式
          ????jsonpFunction:?`webpackJsonp_ReactMicroApp`,?//?按需加載
          ??}
          }?//自定義webpack配置

          OK,配置完成!

          理論上,啟動(dòng)項(xiàng)目,部署等都應(yīng)該沒有問題了。咦,打開地址,頁面一直在 loading,控制臺(tái)一堆報(bào)錯(cuò),看起來要踩一踩坑了。

          踩坑

          1. 版本一致性

          如果主應(yīng)用和子應(yīng)用都是基于 umi 框架,在使用 @umijs/umi-plugin-qiankun 插件時(shí),要使用同一個(gè)版本,否則子應(yīng)用報(bào)錯(cuò)。

          2. 跨域

          qiankun 是通過 fetch 去獲取子應(yīng)用資源的,所以必須支持跨域

          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, 子應(yīng)用為 d.zhuanzhuan.com 。當(dāng)基座去加載子應(yīng)用時(shí),會(huì)出現(xiàn)跨域錯(cuò)誤。

          曾經(jīng)有采用通過 Node 服務(wù)做一層中轉(zhuǎn),跳過跨域問題:

          ??....
          ??maxDays:?3,?//?保留最大天數(shù)日志文件
          }

          //?代理
          config.httpProxy?=?{
          ??'/cors':?{
          ????target:?'https://d.zhuanzhuan.com',
          ????pathRewrite:?{'^/cors'?:?''}
          ??}
          };

          return?config

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

          3. 子應(yīng)用內(nèi)部跳轉(zhuǎn)

          子應(yīng)用內(nèi)部跳轉(zhuǎn),需要在基座路由上提前注冊(cè)好,否則在跳轉(zhuǎn)后,頁面識(shí)別不到。

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

          4. css 污染

          qiankun 只能解決子應(yīng)用之間的樣式相互污染,不能解決子應(yīng)用樣式污染基座的樣式。比如:當(dāng)切換到某個(gè)子應(yīng)用時(shí),左側(cè)菜單欄突然往右移了。

          系統(tǒng)右移

          查看控制臺(tái),不難發(fā)現(xiàn),子應(yīng)用的相同模塊覆蓋了基座:

          樣式覆蓋

          這個(gè)問題,可以通過改變基座的前綴來解決,搞一個(gè)postcss 插件給不同的組件添加不同的前綴。

          這里補(bǔ)充一個(gè) css 隔離常用的方式如:css前綴CSS Module動(dòng)態(tài)加載/卸載樣式表

          qiankun 中 css沙箱機(jī)制 采用的是 動(dòng)態(tài)加載/卸載樣式表

          1. 重寫 HTMLHeadElement.prototype.appendChild 事件
          //?Just?overwrite?it?while?it?have?not?been?overwrite
          if?(
          ??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;
          ....
          1. 當(dāng)子應(yīng)用加載時(shí),在 head 插入 style/link ; 當(dāng)卸載時(shí),直接移除。
          //?Just?overwrite?it?while?it?have?not?been?overwrite
          if?(
          ??HTMLHeadElement.prototype.removeChild?===?rawHeadRemoveChild?&&
          ??HTMLBodyElement.prototype.removeChild?===?rawBodyRemoveChild
          )?{
          ??HTMLHeadElement.prototype.removeChild?=?getNewRemoveChild({
          ????appWrapperGetter,
          ????headOrBodyRemoveChild:?rawHeadRemoveChild,
          ??});
          ??HTMLBodyElement.prototype.removeChild?=?getNewRemoveChild({
          ????appWrapperGetter,
          ????headOrBodyRemoveChild:?rawBodyRemoveChild,
          ??});
          }

          看起來很完美,但有時(shí)候會(huì)出現(xiàn),基座樣式丟失的問題。這個(gè)跟子應(yīng)用卸載的時(shí)機(jī)有關(guān)系:當(dāng)切換子應(yīng)用時(shí),當(dāng)前子應(yīng)用沙箱環(huán)境還未被卸載,但基座 css 已被插入,當(dāng)卸載時(shí)會(huì)連帶基座 css 一起被清除。

          5. 錯(cuò)誤捕獲,降級(jí)處理

          若子應(yīng)用加載失敗,需要給相應(yīng)的提示或動(dòng)態(tài)插入iframe頁:

          //?iframe.js
          export?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;
          ??//?加載失敗時(shí)提示
          ??if?(message?&&?message.includes("died?in?status?LOADING_SOURCE_CODE"))?{
          ????Modal.Confirm({
          ????content:?"子應(yīng)用加載失敗,請(qǐng)檢查應(yīng)用是否可運(yùn)行"
          ????onOk:?()?=>?import('./Inframe.js')
          ????});
          ??}
          });

          6. 路由懶加載樣式丟失

          子應(yīng)用中存在按需加載的路由,在加載時(shí)頁面樣式丟失,這是官方庫產(chǎn)生的問題,issue 里已有大佬提 PR 啦,可參考 https://github.com/umijs/qiankun/issues/857


          以上,就是我們的不完全踩坑。

          應(yīng)用間的通信,在我司的業(yè)務(wù)場(chǎng)景中復(fù)雜度不高,使用官方提供的方案就可以解決,此處沒有詳說。

          后續(xù)

          持續(xù)性思考會(huì)帶來的技術(shù)紅利

          此次接入 qiankun,也只是處于表面應(yīng)用。后續(xù)我們更要思考接入它之后更深的工程價(jià)值,如:

          - 自動(dòng)接入 qiankun

          結(jié)合我司已有的腳手架和 umi 模板,額外添加一個(gè)命令,自動(dòng)注冊(cè)子應(yīng)用,做到自動(dòng)化。

          - 子應(yīng)用間組件共享

          基座和子應(yīng)用大概率都用到了 react/dva 等,是否可以在基座加載完之后,子應(yīng)用直接復(fù)用?當(dāng)然,淺顯思考應(yīng)該少不了 webpackexternals


          ?

          瀏覽 81
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  成人网在线免费观看 | 国产黄色在线播放 | 日本成人黄色网址 | 婷婷色吧综合AV | 一本色道久久HEZYO亚洲精品 |