淺析微前端方案及qiankun介紹與開發實踐


一、什么是微前端?

  我們先來看兩個實際的場景:

1、復用別的的項目頁面

  如果我們的項目需要開發某個新的功能,而這個功能另一個項目已經開發好,我們想直接復用時。注意:我們需要的只是別人項目的這個功能頁面的「內容部分」,不需要別人項目的頂部導航和菜單。

  一個比較笨的辦法就是直接把別人項目這個頁面的代碼拷貝過來,但是萬一別人不是 vue 開發的,或者說 vue 版本、UI 庫等不同,以及別人的頁面加載之前操作(路由攔截,鑒權等)我們都需要拷貝過來,更重要的問題是,別人代碼有更新,我們如何做到同步更新。

  長遠來看,代碼拷貝不太可行,問題的根本就是,我們需要做到讓他們的代碼運行在他們自己的環境之上,而我們對他們的頁面僅僅是“引用”。

  這個環境包括各種插件( vue、 vuex 、 vue-router 等),也包括加載前的邏輯(讀 cookie,鑒權,路由攔截等)。私有 npm 可以共享組件,但是依然存在技術棧不同/UI庫不同等問題。

2、巨無霸項目的自由拆分組合

  • 代碼越來越多,打包越來越慢,部署升級麻煩,一些插件的升級和公共組件的修改需要考慮的更多,很容易牽一發而動全身
  • 項目太大,參與人員越多,代碼規范比較難管理,代碼沖突也頻繁。
  • 產品功能齊全,但是客戶往往只需要其中的部分功能。剝離不需要的代碼后,需要獨立制定版本,獨立維護,增加人力成本。

  舉個例子,你們的產品有幾百個頁面,功能齊全且強大,客戶只需要其中的部分頁面,而且需要你們提供源碼,這時候把所有代碼都給出去肯定是不可能的,只能挑出來客戶需要,這部分代碼需要另外制定版本維護,就很浪費。

二、常見微前端方案

  微前端的誕生也是為了解決以上兩個問題:

  (1)復用(嵌入)別人的項目頁面,但是別人的項目運行在他自己的環境之上。

  (2)巨無霸應用拆分成一個個的小項目,這些小項目獨立開發部署,又可以自由組合進行售賣。

  使用微前端的好處:

  (1)技術棧無關,各個子項目可以自由選擇框架,可以自己制定開發規范。

  (2)快速打包,獨立部署,互不影響,升級簡單。

  (3)可以很方便的復用已有的功能模塊,避免重復開發。

  目前微前端主要有兩種解決方案:iframe 方案和 single-spa 方案

1、iframe方案

  iframe 大家都很熟悉,使用簡單方便,提供天然的 js/css 隔離,也帶來了數據傳輸的不便,一些數據無法共享(主要是本地存儲、全局變量和公共插件),兩個項目不同源(跨域)情況下數據傳輸需要依賴 postMessage 。iframe 有很多坑,但是大多都有解決的辦法:

(1)頁面加載問題

  iframe 和主頁面共享連接池,而瀏覽器對相同域的連接有限制,所以會影響頁面的並行加載,阻塞 onload 事件。每次點擊都需要重新加載,雖然可以采用 display:none 來做緩存,但是頁面緩存過多會導致電腦卡頓。「(無法解決)」

(2)布局問題

  iframe 必須給一個指定的高度,否則會塌陷。

  解決辦法:子項目實時計算高度並通過 postMessage 發送給主頁面,主頁面動態設置 iframe 高度。有些情況會出現多個滾動條,用戶體驗不佳。

(3)彈窗及遮罩層問題

  彈窗只能在 iframe 范圍內垂直水平居中,沒法在整個頁面垂直水平居中。

  解決辦法1:通過與框架頁面消息同步解決,將彈窗消息發送給主頁面,主頁面來彈窗,對原項目改動大且影響原項目的使用。

  解決辦法2:修改彈窗的樣式:隱藏遮罩層,修改彈窗的位置。

(4)iframe 內的 div 無法全屏

  彈窗的全屏,指的是在瀏覽器可視區全屏。這個全屏指的是占滿用戶屏幕。

  全屏方案,原生方法使用的是 Element.requestFullscreen(),插件:vue-fullscreen。當頁面在 iframe 里面時,全屏會報錯,且 dom 結構錯亂。

在這里插入圖片描述

(5)瀏覽器前進/后退問題

  iframe 和主頁面共用一個瀏覽歷史,iframe 會影響頁面的前進后退。大部分時候正常,iframe 多次重定向則會導致瀏覽器的前進后退功能無法正常使用。並且 iframe 頁面刷新會重置(比如說從列表頁跳轉到詳情頁,然后刷新,會返回到列表頁),因為瀏覽器的地址欄沒有變化,iframe 的 src 也沒有變化。

(6)iframe 加載失敗的情況不好處理

  非同源的 iframe 在火狐及 chorme 都不支持 onerror 事件。

  解決辦法1:onload 事件里面判斷頁面的標題,是否 404 或者 500

  解決辦法2:使用 try catch 解決此問題,嘗試獲取 contentDocument 時將拋出異常。

  解決辦法參考:stackoverflow上的問題:Catch error if iframe src fails to load

2、single-spa 微前端方案

  spa 單頁應用時代,我們的頁面只有 index.html 這一個 html 文件,並且這個文件里面只有一個內容標簽 <div id="app"></div>,用來充當其他內容的容器,而其他的內容都是通過 js 生成的。也就是說,我們只要拿到了子項目的容器 <div id="app"></div> 和生成內容的 js,插入到主項目,就可以呈現出子項目的內容。

<link href=/css/app.c8c4d97c.css rel=stylesheet>
<div id=app></div>
<script src=/js/chunk-vendors.164d8230.js> </script>
<script src=/js/app.6a6f1dda.js> </script> 

  我們只需要拿到子項目的上面四個標簽,插入到主項目的 HTML 中,就可以在父項目中展現出子項目。

  這里有個問題,由於子項目的內容標簽是動態生成的,其中的 img/video/audio 等資源文件和按需加載的路由頁面 js/css 都是相對路徑,在子項目的 index.html 里面,可以正確請求,而在主項目的 index.html 里面,則不能。

  舉個例子,假設我們主項目的網址是 www.baidu.com ,子項目的網址是 www.taobao.com ,在子項目的 index.html 里面有一張圖片 <img src="./logo.jpg">,那么這張圖片的完整地址是 www.taobao.com/logo.jpg,現在將這個圖片的 img 標簽生成到了父項目的 index.html,那么圖片請求的地址是 www.baidu.com/logo.jpg,很顯然,父項目服務器上並沒有這張圖

  解決思路:

  1. 這里面的 js/css/img/video 等都是相對路徑,能否通過 webpack 打包,將這些路徑全部打包成絕對路徑?這樣就可以解決文件請求失敗的問題。
  2. 能否手動(或借助 node )將子項目的文件全部拷貝到主項目服務器上,node 監聽子項目文件有更新,就自動拷貝過來,並且按 js/css/img 文件夾合並
  3. 能否像 CDN 一樣,一個服務器掛了,會去其他服務器上請求對應文件。或者說服務器之間的文件共享,主項目上的文件請求失敗會自動去子服務器上找到並返回。

  通常做法是動態修改 webpack 打包的 publicPath,然后就可以自動注入前綴給這些資源。

  single-spa 是一個微前端框架,基本原理如上,在上述呈現子項目的基礎上,還新增了 bootstrap 、 mount 、 unmount 等生命周期。

  相對於 iframesingle-spa 讓父子項目屬於同一個 document,這樣做既有好處,也有壞處。好處就是數據/文件都可以共享,公共插件共享,子項目加載就更快了,缺點是帶來了 js/css 污染。

  single-spa 上手並不簡單,也不能開箱即用,開發部署更是需要修改大量的 webpack 配置,對子項目的改造也非常多。

三、qiankun 方案

  qiankun 是螞蟻金服開源的一款框架,它是基於 single-spa 的。他在 single-spa 的基礎上,實現了開箱即用,除一些必要的修改外,子項目只需要做很少的改動,就能很容易的接入。如果說 single-spa 是自行車的話,qiankun 就是個汽車。

  微前端中子項目的入口文件常見的有兩種方式:JS entry 和 HTML entry

  純 single-spa 采用的是 JS entry,而 qiankun 既支持 JS entry,又支持 HTML entry

  JS entry 的要求比較苛刻:

(1)將 css 打包到 js 里面

(2)去掉 chunk-vendors.js

(3)去掉文件名的 hash 值

(4)將 single-spa 模式的入口文件( app.js )放置到 index.html 目錄,其他文件不變,原因是要截取 app.js 的路徑作為 publicPath

APP entry 優點 缺點
JS entry 可以配合 systemJs,按需加載公共依賴( vue , vuex , vue-router 等) 需要各種打包配置配合,無法實現預加載
HTML entry 打包配置無需做太多的修改,可以預加載 多一層請求,需要先請求到 HTML 文件,再用正則匹配到其中的 js 和 css

  其實 qiankun 還支持 config entry :

復制代碼
{
   entry: {
        scripts: [
          "app.3249afbe.js"
          "chunk-vendors.75fba470.js",
        ],
        styles: [
          "app.3249afbe.css"
          "chunk.75fba470.css",
        ],
        html: 'http://localhost:5000'
    }
}
復制代碼

  建議使用 HTML entry ,使用起來和 iframe 一樣簡單,但是用戶體驗比 iframe 強很多。

  qiankun 請求到子項目的 index.html 之后,會先用正則匹配到其中的 js/css 相關標簽,然后替換掉,它需要自己加載 js 並運行,然后去掉 html/head/body 等標簽,剩下的內容原樣插入到子項目的容器中 :

  使用 qiankun 的好處:

(1)qiankun 自帶 js/css 沙箱功能,singles-spa 可以解決 css 污染,但是需要子項目配合

(2)single-spa 方案只支持 JS entry 的特點,限制了它只能支持 vue 、 react 、 angular 等技術開發的項目,對一些 jQuery 老項目則無能為力。qiankun 則沒有限制

(3)qiankun 支持子項目預請求功能。

1、js 沙箱

  js/css 污染是無法避免的,並且是一個可大可小的問題。就像一顆定時炸彈,不知道什么時候會出問題,排查也麻煩。作為一個基礎框架,解決這兩個污染非常重要,不能僅憑“規范”開發。

  js 沙箱的原理是子項目加載之前,對 window 對象做一個快照,子項目卸載時恢復這個快照,如圖:

  那么如何監測 window 對象的變化呢,直接將 window 對象進行一下深拷貝,然后深度對比各個屬性顯然可行性不高,qiankun框架采用的是ES6新特性,proxy代理方法。但是 proxy 是不兼容 IE11 的,為了兼容,低版本 IE 采用了 diff 方法:淺拷貝 window 對象,然后對比每一個屬性。

2、css 沙箱

  qiankun 的 css 沙箱的原理是重寫 HTMLHeadElement.prototype.appendChild 事件,記錄子項目運行時新增的 style/link 標簽,卸載子項目時移除這些標簽。

  single-spa 方案中我用了換膚的思路來解決 css 污染:首先 css-scoped 解決大部分的污染,對於一些全局樣式,在子項目給 body/html 加一個唯一的 id/class(正常開發部署用),然后這個全局的樣式前面加上這個 id/class,而 single-spa 模式則在 mount 周期給 body/html 加上這個唯一的 id/class,在 unmount 周期去掉,這樣就可以保證這個全局 css 只對這個項目生效了。

  這兩個方案的致命點都在於無法解決多個子項目同時運行時的 css 污染,以及子項目對主項目的 css 污染。

  雖然說兩個項目同時運行並不常見,但是如果想實現 keep-alive ,就需要使用 display: none 將子項目隱藏起來,子項目不需要卸載,這時候就會存在兩個子項目同時運行,只不過其中一個對用戶不可見。

  css 沙箱還有個思路就是將子項目的樣式局限到子項目的容器范圍內生效,這樣只需要給不同的子項目不同的容器就可以了。但是這樣也會有新的問題,子項目中 append 到 body 的彈窗,樣式就無法生效。所以說樣式污染還需要制定規范才行,約定 class 命名前綴。

四、微前端方案實踐

  改造已有的項目為qiankun子項目,由於我們是 vue 技術棧,所以我就以改造一個 vue 項目為例說明,其他的技術棧原理是一樣的。

1、在 src 目錄新增文件 public-path.js

if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

2、修改 index.html 中項目初始化的容器,不要使用 #app ,避免與其他的項目沖突,建議換成項目 name 的駝峰寫法

3、修改入口文件 main.js

復制代碼
import './public-path';
import Vue from 'vue'
import App from './App.vue'
import VueRouter from 'vue-router'
import store from './store';

Vue.use(VueRouter)
Vue.config.productionTip = false

let router = null;
let instance = null;
function render(parent = {}) {
  const router = new VueRouter({
    // histroy模式的路由需要設置base,app-history-vue根據項目名稱來定
    base: window.__POWERED_BY_QIANKUN__ ? '/app-history-vue' : '/',
    mode: 'history',
    // hash模式不需要上面兩行
    routes: []
  })
  instance = new Vue({
    router,
    store,
    render: h => h(App),
    data(){
      return {
        parentRouter: parent.router,
        parentVuex: parent.store,
      }
    },
  }).$mount('#appVueHistory');
}
//全局變量來判斷環境,獨立運行時
if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

export async function bootstrap() {
  console.log('vue app bootstraped');
}
export async function mount(props) {
  console.log('props from main framework', props);
  render(props.data);
}
export async function unmount() {
  instance.$destroy();
  instance = null;
  router = null;
}
復制代碼

  主要改動是引入修改 publicPath 的文件和 export 三個生命周期。

  注意:

  • webpack 的 publicPath 值只能在入口文件修改,之所以單獨寫到一個文件並在入口文件最開始引入,是因為這樣做可以讓下面所有的代碼都能使用這個。
  • 路由文件需要 export 路由數據,而不是實例化的路由對象,路由的鈎子函數也需要移到入口文件。
  • 在 mount 生命周期,可以拿到父項目傳遞過來的數據,router 用於跳轉到主項目/其他子項目的路由,store 是父項目的實例化的 Vuex

4、修改打包配置 vue.config.js:

復制代碼
const { name } = require('./package');

module.exports = {
  devServer: {
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
  },
  // 自定義webpack配置
  configureWebpack: {
    output: {
      library: `${name}-[name]`,
      libraryTarget: 'umd',// 把子應用打包成 umd 庫格式
      jsonpFunction: `webpackJsonp_${name}`,
    },
  },
};
復制代碼

  注: 這個 name 默認從 package.json 獲取,可以自定義,只要和父項目注冊時的 name 保持一致即可。

  這個配置主要就兩個,一個是允許跨域,另一個是打包成 umd 格式。為什么要打包成 umd 格式呢?是為了讓 qiankun 拿到其 export 的生命周期函數。我們可以看下其打包后的 app.js 就知道了:

  root 在瀏覽器環境就是 window , qiankun 拿這三個生命周期,是根據注冊應用時,你給的 name 值,name 不一致則會導致拿不到生命周期函數

五、子項目開發的一些注意事項

1、所有的資源(圖片/音視頻等)都應該放到 src 目錄,不要放在 public 或 者static

  資源放 src 目錄,會經過 webpack 處理,能統一注入 publicPath。否則在主項目中會404。

  參考:vue-cli3的官方文檔介紹:何時使用-public-文件夾

  暴露給運維人員的配置文件 config.js,可以放在 public 目錄,因為在 index.html 中 url 為相對鏈接的 js/css 資源,qiankun 會給其注入前綴。

2、請給 axios 實例添加攔截器,而不是 axios 對象

  后續會考慮子項目共享公共插件,這時就需要避免公共插件的污染

// 正確做法:給 axios 實例添加攔截器
const instance = axios.create();
instance.interceptors.request.use(function () {/*...*/});
// 錯誤用法:直接給 axios 對象添加攔截器
axios.interceptors.request.use(function () {/*...*/});

3、避免 css 污染

  組件內樣式的 css-scoped 是必須的。

  對於一些插入到 body 的彈窗,無法使用 scoped,請不要直接使用原 class 修改樣式,請添加自己的 class,來修改樣式。

復制代碼
.el-dialog{
  /* 不推薦使用組件原有的class */
}
.my-el-dialog{
  /* 推薦使用自定義組件的class */
}
復制代碼

4、謹慎使用 position:fixed

  在父項目中,這個定位未必准確,應盡量避免使用,確有相對於瀏覽器窗口定位需求,可以用 position: sticky,但是會有兼容性問題(IE不支持)。如果定位使用的是 bottom 和 right,則問題不大。

  還有個辦法,位置可以寫成動態綁定 style 的形式:<div :style="{ top: isisQiankun ? '10px' : '0'}">

5、給 body 、 document 等綁定的事件,請在 unmount 周期清除

  js 沙箱只劫持了 window.addEventListener,使用 document.body.addEventListener 或者 document.body.onClick 添加的事件並不會被沙箱移除,會對其他的頁面產生影響,請在 unmount 周期清除

原文鏈接:https://juejin.cn/post/6844904185910018062,作者:沉末_


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM