qiankun 微前端方案實踐及總結

什麼是微前端?

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

1. 複用別的的項目頁面

一般,咱們的後臺項目都長這樣:html

在這裏插入圖片描述
若是咱們的項目須要開發某個新的功能,而這個功能另外一個項目已經開發好,咱們想直接複用時。PS:咱們須要的只是別人項目的這個功能頁面的**「內容部分」**,不須要別人項目的頂部導航和菜單。前端

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

長遠來看,代碼拷貝不太可行,問題的根本就是,咱們須要作到讓他們的代碼運行在他們本身的環境之上,而咱們對他們的頁面僅僅是「引用」。這個環境包括各類插件( vuevuexvue-router 等),也包括加載前的邏輯(讀 cookie,鑑權,路由攔截等)。私有 npm 能夠共享組件,可是依然存在技術棧不一樣/UI庫不一樣等問題。node

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

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

舉個栗子,大家的產品有幾百個頁面,功能齊全且強大,客戶只須要其中的部分頁面,並且須要大家提供源碼,這時候把全部代碼都給出去確定是不可能的,只能挑出來客戶須要,這部分代碼須要另外製定版本維護,就很浪費。react

常見微前端方案

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

  1. 複用(嵌入)別人的項目頁面,可是別人的項目運行在他本身的環境之上。
  2. 巨無霸應用拆分紅一個個的小項目,這些小項目獨立開發部署,又能夠自由組合進行售賣。

使用微前端的好處:ios

  1. 技術棧無關,各個子項目能夠自由選擇框架,能夠本身制定開發規範。
  2. 快速打包,獨立部署,互不影響,升級簡單。
  3. 能夠很方便的複用已有的功能模塊,避免重複開發。

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

iframe方案

iframe 你們都很熟悉,使用簡單方便,提供自然的 js/css 隔離,也帶來了數據傳輸的不便,一些數據沒法共享(主要是本地存儲、全局變量和公共插件),兩個項目不一樣源(跨域)狀況下數據傳輸須要依賴 postMessageajax

iframe 有不少坑,可是大多都有解決的辦法:

  1. 頁面加載問題

iframe 和主頁面共享鏈接池,而瀏覽器對相同域的鏈接有限制,因此會影響頁面的並行加載,阻塞 onload 事件。每次點擊都須要從新加載,雖然能夠採用 display:none 來作緩存,可是頁面緩存過多會致使電腦卡頓。「(沒法解決)」

  1. 佈局問題

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

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

  1. 彈窗及遮罩層問題

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

  • 解決辦法1:經過與框架頁面消息同步解決,將彈窗消息發送給主頁面,主頁面來彈窗,對原項目改動大且影響原項目的使用。
  • 解決辦法2:修改彈窗的樣式:隱藏遮罩層,修改彈窗的位置。
  1. iframe 內的 div 沒法全屏

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

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

在這裏插入圖片描述

  1. 瀏覽器前進/後退問題

iframe 和主頁面共用一個瀏覽歷史,iframe 會影響頁面的前進後退。大部分時候正常,iframe 屢次重定向則會致使瀏覽器的前進後退功能沒法正常使用。而且 iframe 頁面刷新會重置(好比說從列表頁跳轉到詳情頁,而後刷新,會返回到列表頁),由於瀏覽器的地址欄沒有變化,iframesrc 也沒有變化。

  1. iframe 加載失敗的狀況很差處理

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

  • 解決辦法1:onload 事件裏面判斷頁面的標題,是否 404 或者 500
  • 解決辦法2:使用 try catch 解決此問題,嘗試獲取 contentDocument 時將拋出異常。

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

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 是一個微前端框架,基本原理如上,在上述呈現子項目的基礎上,還新增了 bootstrapmountunmount 等生命週期。

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

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

qiankun 方案

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

微前端中子項目的入口文件常見的有兩種方式:JS entryHTML 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 文件,再用正則匹配到其中的 jscss

其實 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 的特色,限制了它只能支持 vuereactangular 等技術開發的項目,對一些 jQuery 老項目則無能爲力。qiankun 則沒有限制
  3. qiankun 支持子項目預請求功能。

js 沙箱

js/css 污染是沒法避免的,而且是一個可大可小的問題。就像一顆定時炸彈,不知道何時會出問題,排查也麻煩。做爲一個基礎框架,解決這兩個污染很是重要,不能僅憑「規範」開發。

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

在這裏插入圖片描述
那麼如何監測 window 對象的變化呢,直接將 window 對象進行一下深拷貝,而後深度對比各個屬性顯然可行性不高,qiankun框架採用的是ES6新特性,proxy代理方法。具體如何操做的,以前的文章有寫(連接在文末),就再也不贅述。

可是 proxy 是不兼容 IE11 的,爲了兼容,低版本 IE 採用了 diff 方法:淺拷貝 window 對象,而後對比每個屬性。

css 沙箱

qiankuncss 沙箱的原理是重寫 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 沙箱還有個思路就是將子項目的樣式侷限到子項目的容器範圍內生效,這樣只須要給不一樣的子項目不一樣的容器就能夠了。可是這樣也會有新的問題,子項目中 appendbody 的彈窗,樣式就沒法生效。因此說樣式污染還須要制定規範才行,約定 class 命名前綴。

微前端方案實踐

在個人前幾篇文章(連接在文末)中,single-spaqiankundemo 已經實現了,開發部署流程也都有,接下來就是實踐出真知,用在實際項目中,才知道有那些坑。

改造已有的項目爲 qiankun 子項目

因爲咱們是 vue 技術棧,因此我就以改造一個 vue 項目爲例說明,其餘的技術棧原理是同樣的。

  1. src 目錄新增文件 public-path.js
if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
  1. 修改 index.html 中項目初始化的容器,不要使用 #app ,避免與其餘的項目衝突,建議換成項目 name 的駝峯寫法

  2. 修改入口文件 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 三個生命週期。

注意:

  • webpackpublicPath 值只能在入口文件修改,之因此單獨寫到一個文件並在入口文件最開始引入,是由於這樣作可讓下面全部的代碼都能使用這個。
  • 路由文件須要 export 路由數據,而不是實例化的路由對象,路由的鉤子函數也須要移到入口文件。
  • mount 生命週期,能夠拿到父項目傳遞過來的數據,router 用於跳轉到主項目/其餘子項目的路由,store 是父項目的實例化的 Vuex
  1. 修改打包配置 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.htmlurl 爲相對連接的 js/css 資源,qiankun 會給其注入前綴。

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

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

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

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

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

.el-dialog{
  /* 不推薦使用組件原有的class */
}
.my-el-dialog{
  /* 推薦使用自定義組件的class */
}
  1. 謹慎使用 position:fixed

在父項目中,這個定位未必準確,應儘可能避免使用,確有相對於瀏覽器窗口定位需求,能夠用 position: sticky,可是會有兼容性問題(IE不支持)。若是定位使用的是 bottomright,則問題不大。

還有個辦法,位置能夠寫成動態綁定 style 的形式:

<div :style="{ top: isisQiankun ? '10px' : '0'}">
  1. bodydocument 等綁定的事件,請在 unmount 週期清除

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

qiankun 常見問題及解決方案

qiankun 常見報錯

  1. 子項目未 export 須要的生命週期函數

在這裏插入圖片描述
先檢查下子項目的入口文件有沒有 export 生命週期函數,再檢查下子項目的打包,最後看看請求到的子項目的文件對不對。

  1. 子項目加載時,容器未渲染好

在這裏插入圖片描述
檢查容器 div 是不是寫在了某個路由裏面,路由沒匹配到全部未加載。若是隻在某個路由頁面加載子項目,能夠在頁面的 mounted 週期裏面註冊子項目並啓動。

主項目路由只能用history模式嗎?

因爲 qiankun 是經過 location.pathname 值來判斷當前應該加載哪一個子項目的,因此須要給每一個子項目注入不一樣的路由 path,而 hash 模式子項目路由跳轉不改變 path,因此無影響,history 模式子項目路由設置 base 屬性便可。

若是主項目使用 hash 模式,那麼得用 location.hash 值來判斷當前應該加載哪一個子項目,而且子項目都得是 hash 模式,還須要給子項目全部的路由都添加一個前綴,子項目的路由跳轉若是以前使用的是 path 也須要修改,用 name 跳轉則不用。

若是主項目是 hash 模式子項目爲 history 模式,那麼跳轉到子項目以後,沒法跳轉到另外一個 history 模式的子項目,也沒法回到主項目的頁面。

vue 項目 hash 模式改 history 模式也很簡單:

  1. new Router 時設置 modehistory

在這裏插入圖片描述
2. webpack 打包的配置( vue.config.js ) :

在這裏插入圖片描述
3. 一些資源會報 404,相對路徑改成絕對路徑:<img src="./img/logo.jpg"> 改成 <img src="/img/logo.jpg"> 便可

css 污染問題及加載 bug

  1. qiankun 只能解決子項目之間的樣式相互污染,不能解決子項目的樣式污染主項目的樣式

主項目要想不被子項目的樣式污染,子項目是 vue 技術,樣式能夠寫 css-scoped ,若是子項目是 jQuery 技術呢?因此主項目自己的 id/class 須要特殊一點,不能太簡單,被子項目匹配到。

  1. 從子項目頁面跳轉到主項目自身的頁面時,主項目頁面的 css 未加載的 bug

產生這個問題的緣由是:在子項目跳轉到父項目時,子項目的卸載須要一點點的時間,在這段時間內,父項目加載了,插入了 css,可是被子項目的 css 沙箱記錄了,而後被移除了。父項目的事件監聽也是同樣的,因此須要在子項目卸載完成以後再跳轉。我本來想在路由鉤子函數裏面判斷下,子項目是否卸載完成,卸載完成再跳轉路由,然而路由不跳轉,子項目根本不會卸載。

臨時解決辦法:先複製一下 HTMLHeadElement.prototype.appendChildwindow.addEventListener ,路由鉤子函數 beforeEach 中判斷一下,若是當前路由是子項目,而且去的路由是父項目的,則還原這兩個對象.

const childRoute = ['/app-vue-hash','/app-vue-history'];
const isChildRoute = path => childRoute.some(item => path.startsWith(item))
const rawAppendChild = HTMLHeadElement.prototype.appendChild;
const rawAddEventListener = window.addEventListener;
router.beforeEach((to, from, next) => {
  // 從子項目跳轉到主項目
  if(isChildRoute(from.path) && !isChildRoute(to.path)){
    HTMLHeadElement.prototype.appendChild = rawAppendChild;
    window.addEventListener = rawAddEventListener;
  }
  next();
});

路由跳轉問題

在子項目裏面如何跳轉到另外一個子項目/主項目頁面呢,直接寫 <router-link> 或者用 router.push/router.replace 是不行的,緣由是這個 router 是子項目的路由,全部的跳轉都會基於子項目的 base 。寫 <a> 連接能夠跳轉過去,可是會刷新頁面,用戶體驗很差。

解決辦法也比較簡單,在子項目註冊時將主項目的路由實例對象傳過去,子項目掛載到全局,用父項目的這個 router 跳轉就能夠了。

可是有一丟丟不完美,這樣只能經過 js 來跳轉,跳轉的連接沒法使用瀏覽器自帶的右鍵菜單(如圖:Chrome 自帶的連接右鍵菜單)

在這裏插入圖片描述

項目通訊問題

項目之間的不要有太多的數據依賴,畢竟項目仍是要獨立運行的。通訊操做須要判斷是否 qiankun 模式,作兼容處理。

經過 props 傳遞父項目的 Vuex ,若是子項目是 vue 技術棧,則會很好用。假如子項目是 jQuery/react/angular ,就不能很好的監聽到數據的變化。

qiakun 提供了一個全局的 GlobalState 來共享數據。主項目初始化以後,子項目能夠監聽到這個數據的變化,也能提交這個數據。

// 主項目初始化
import { initGlobalState } from 'qiankun';
const actions = initGlobalState(state);
// 主項目項目監聽和修改
actions.onGlobalStateChange((state, prev) => {
  // state: 變動後的狀態; prev 變動前的狀態
  console.log(state, prev);
});
actions.setGlobalState(state);

// 子項目監聽和修改
export function mount(props) {
  props.onGlobalStateChange((state, prev) => {
    // state: 變動後的狀態; prev 變動前的狀態
    console.log(state, prev);
  });
  props.setGlobalState(state);
}

vue 項目之間數據傳遞仍是使用共享父組件的 Vuex 比較方便,與其餘技術棧的項目之間的通訊使用 qiankun 提供的 GlobalState

子項目之間的公共插件如何共享

若是主項目和子項目都用到了同一個版本的 Vue/Vuex/Vue-Router 等,主項目加載一遍以後,子項目又加載一遍,就很浪費。

要想複用公共依賴,前提條件是子項目必須配置 externals ,這樣依賴就不會打包進 chunk-vendors.js ,才能複用已有的公共依賴。

按需引入公共依賴,有兩個層面:

  1. 沒有使用到的依賴不加載
  2. 大插件只加載須要的部分,例如 UI 組件庫的按需加載、echarts/lodash 的按需加載。

webpackexternals 是支持大插件的按需引入的:

subtract : {
   root: ['math', 'subtract']
}

subtract 能夠經過全局 math 對象下的屬性 subtract 訪問(例如 window['math']['subtract'])。

single-spa 能夠按需引入子項目的公共依賴

single-spa 是使用 systemJs 加載子項目和公共依賴的,將公共依賴和子項目一塊兒配置到 systemJs 的配置文件 importmap.json ,就能夠實現公共依賴的按需加載:

{
 "imports": {
   "appVueHash": "http://localhost:7778/app.js",
   "appVueHistory": "http://localhost:7779/app.js",
   "single-spa": "https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.7/system/single-spa.min.js",
   "vue": "https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js",
   "vue-router": "https://cdn.jsdelivr.net/npm/vue-router@3.0.7/dist/vue-router.min.js",
   "echarts": "https://cdn.bootcss.com/echarts/4.2.1-rc1/echarts.min.js"
 }
}

qiankun 如何按需引入公共依賴

巨無霸應用的公共依賴和公共函數被太多的頁面使用,致使升級和改動困難,使用微前端可讓各個子項目獨立擁有本身的依賴,互不干擾。而咱們想要複用公共依賴,這與微前端的理念是相悖的。

因此個人想法是:父項目提供公共依賴,子項目能夠自由選擇用或者不用。

這個也很好實現,父項目先加載好依賴,而後在註冊子項目時,將 Vue/Vuex/Vue-Router 等經過 props 傳過去,子項目能夠選擇用或者不用。

主項目:

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import { registerMicroApps, start } from 'qiankun';
import Vuex from 'vuex';
import VueRouter from 'vue-router';

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount("#app");

registerMicroApps([
  { 
    name: 'app-vue-hash', 
    entry: 'http://localhost:1111', 
    container: '#appContainer', 
    activeRule: '/app-vue-hash', 
    props: { data : { store, router, Vue, Vuex, VueRouter } }
  },
]);

start();

子項目:

import Vue from 'vue'

export async function bootstrap() {
  console.log('vue app bootstraped');
}

export async function mount(props) {
  console.log('props from main framework', props);
  const { VueRouter, Vuex } = props.data;
  Vue.use(VueRouter);
  Vue.use(Vuex);
  render(props.data);
}

export async function unmount() {
  instance.$destroy();
  instance = null;
  router = null;
}

這樣作不太可行,緣由有兩個:

  1. 子項目獨立運行時,Vue-Router/Vuex這些依賴從哪裏來?子項目是隻部署一份的,既能夠獨立運行,也能夠被 qiankun 集成。
  2. 父項目只能傳遞它本身已經有的依賴,如何肯定子項目須要哪些依賴?不知足按需引入的需求

配置 webpackexternals 以後,子項目獨立運行時,這些依賴的來源**「有且僅有」** index.html 中的外鏈 script 標籤。

在這個前提下,子項目和主項目的 vue 版本一致的狀況下,使用同一份服務器文件。即便沒法共享,也是能夠作 http 緩存的。

那麼 qiankun 可否作到,某個依賴加載了以後,再也不加載,直接複用呢?好比說子項目 A 請求了服務器上的 2.6 版本 vue,切換到子項目 B,B 項目也用了這個 vue 文件,可否再也不次加載,直接複用呢?

實際上是能夠的,能夠看到 qiankun 將子項目的外鏈 script 標籤,內容請求到以後,會記錄到一個全局變量中,下次再次使用,他會先從這個全局變量中取。這樣就會實現內容的複用,只要保證兩個連接的url一致便可。

const fetchScript = scriptUrl => scriptCache[scriptUrl] ||
  (scriptCache[scriptUrl] = fetch(scriptUrl).then(response => response.text()));

因此只要子項目配置了 webpackexternals,並在 index.html 中使用外鏈 script 引入這些公共依賴,只要這些公共依賴在同一臺服務器上,即可以實現子項目的公共依賴的按需引入,一個項目使用了以後,另外一個項目使用再也不重複加載,能夠直接複用這個文件。

qiankun 更完美的按需引入

雖然 qiankun 不會重複請求相同 url 的公共依賴,可是這也僅比 http 緩存強了一丟丟。

有缺陷的地方在於:

  1. 主項目中的公共依賴沒有記錄到這個緩存中,也就不會被其餘的項目複用
  2. 只是沒有重複請求,仍是須要重複執行一次。可否不執行,直接複用?。js 沙箱在子項目卸載時,會移除 window 上新增的變量,而 webpackexternals偏偏是將這些公共依賴掛載在 window 上,可否看狀況移除這些公共依賴?
  3. 相同版本的依賴會複用,版本不一樣可是使用無差異,可否作到也複用?(版本不一樣 url 也就不一樣,就不會複用)可是這裏可能會有一些疑問,既然使用無差異,爲何不升級插件?

這些問題可能須要去改動 qiankun 的源碼。

jQuery 老項目的資源加載問題

子項目的內容標籤插到父項目的 index.html 後,其中的資源( img/video/audio 等)路徑都是相對的,致使資源沒法正確顯示。上面我列舉了三種解決方案。

通常來講,jQuery 項目是不通過 webpack 打包的,因此無法經過修改 publicPath 來注入路徑前綴。後面兩種方法操做起來比較麻煩,或者說咱們應該**「優先從框架自己」**解決這個問題,而不是其餘方法。因此我想了以下三種方案:

方案一:動態插入 <base> 標籤

html 有一個原生標籤 <base>,這個標籤只能放在 <head> 裏面,它的 href 屬性是一個 url 值。 mdn 地址: base 文檔根 URL 元素

設置了 <base> 標籤以後,頁面上全部的連接和 url 都基於它的 href。例如頁面訪問地址是 https://www.taobao.com ,設置 <base href="https://www.baidu.com"> 以後,頁面中本來的圖 <img src="./img/jQuery1.png" alt=""> 的實際請求地址會變成 https://www.baidu.com/img/jQuery1.png ,頁面上的 <a> 連接:<a href="/about"></a>,點擊以後,頁面會跳轉到:https://www.baidu.com/about

能夠看到,<base> 標籤和 webpackpublicPath 有同樣的效果,那麼可否在 jQuery 項目加載以前,把 jQuery 項目的地址賦給 <base> 標籤,而後插入到 <head> ?這樣就能夠解決 jQuery 項目的資源加載問題。

作法也很簡單,在 qiankun 提供的 beforeLoad 生命週期,判斷當前是不是 jQuery 項目:

beforeLoad: app => {
   if(app.name === 'purehtml'){
       const baseTag = document.createElement('base');
       baseTag.setAttribute('href',app.entry);
       console.log(baseTag);
       document.head.appendChild(baseTag);
   }
},
beforeUnmount: app => {
   if(app.name === 'purehtml'){
      const baseTag = document.head.querySelector('base');
      document.head.removeChild(baseTag);
   }
}

這樣作子項目資源能夠正確加載,可是 <base> 標籤的威力太強大了,會致使全部的路由沒法正常跳轉,跳轉到其餘的子項目時,<a> 連接是基於 <base> 的,會跳轉到 jQuery 子項目的不存在的路由。解決了一個 bug ,又出現了新的 bug ,這樣是不行的。因此這個方案可行性特別小。

方案二:劫持標籤插入函數

這個方案分兩步:

  1. 對於 HTML 中已有的 img/audio/video 等標籤,qiankun 支持重寫 getTemplate 函數,能夠將入口文件 index.html 中的靜態資源路徑替換掉
  2. 對於動態插入的 img/audio/video 等標籤,劫持 appendChildinnerHTMLinsertBefore 等事件,將資源的相對路徑替換成絕對路徑

前面咱們說到,對於子項目是 HTML entry 的,qiankun 拿到入口文件 index.html 以後,會用正則匹配到 <body> 標籤及其內容,<head> 中的 link/style/script/meta 等標籤,而後插入到父項目的容器中。

在這裏插入圖片描述
咱們能夠傳遞一個 getTemplate 函數,將圖片的相對路徑轉爲絕對路徑,它會在處理模板時使用:

start({
  getTemplate(tpl,...rest) {
    // 爲了直接看到效果,因此寫死了,實際中須要用正則匹配
    return tpl.replace('<img src="./img/jQuery1.png">', '<img src="http://localhost:3333/img/jQuery1.png">');
  }
});

對於動態插入的標籤,劫持其插入 DOM 的函數,注入前綴。

假如子項目動態插入一張圖:

const render = $ => {
  $('#purehtml-container').html('<p>Hello, render with jQuery</p><img src="./img/jQuery2.png">');
  return Promise.resolve();
};

主項目劫持 jQueryhtml 方法:

beforeMount: app => {
   if(app.name === 'purehtml'){
       // jQuery 的 html 方法是一個挺複雜的函數,這裏只是爲了看效果,簡寫了
       $.prototype.html = function(value){
          const str = value.replace('<img src="/img/jQuery2.png">', '<img src="http://localhost:3333/img/jQuery2.png">')
          this[0].innerHTML = str;
       }
   }
}

固然了,還有個簡單粗暴的寫法,給 jQuery 項目的圖片路徑寫成絕對路徑,可是不建議這麼作,換個服務器部署就不能用了。

方案三:給 jQuery 項目加上 webpack 打包

這個方案的可行性不高,都是陳年老項目了,不必這樣折騰。

老項目的資源加載總結

qiankun 自己就對接入 jQuery 多頁應用比較乏力,通常使用場景就是,一個大項目只接入某個/某幾個頁面,這樣的話使用方案二比較合理。

qiankun 使用總結

  1. 只有一個子項目時,要想啓用預加載,必須使用start({ prefetch: 'all' })

  2. js 沙箱並不能解決全部的 js 污染,例如我用 onclickaddEventListener<body> 添加了一個點擊事件,js 沙箱並不能消除它的影響,因此說,還得靠代碼規範和本身自覺

  3. qiankun 框架不太好實現 keep-alive 需求,由於解決 css/js 污染的辦法就是刪除子項目插入的 css 標籤和劫持 window 對象,卸載時還原成子項目加載前的樣子,這與 keep-alive 相悖: keep-alive 要求保留這些,僅僅是樣式上的隱藏。

  4. qiankun 沒法很好嵌入一些老項目

雖然 qiankun 支持 jQuery 老項目,可是彷佛對**「多頁應用」**沒有很好的解決辦法。每一個頁面都去修改,成本很大也很麻煩,可是使用 iframe 嵌入這些老項目就比較方便。

  1. 安全和性能的問題

qiankun 將每一個子項目的 js/css 文件內容都記錄在一個全局變量中,若是子項目過多,或者文件體積很大,可能會致使內存佔用過多,致使頁面卡頓。

另外,qiankun 運行子項目的 js,並非經過 script 標籤插入的,而是經過 eval 函數實現的,eval 函數的安全和性能是有一些爭議的:MDN的eval介紹

  1. 微前端調試時,每次都須要分別進入子項目和主項目運行和打包,很是麻煩,可使用 npm-run-all 插件來實現:一個命令,運行全部項目。
{
  "scripts": {
    "install:hash": "cd app-vue-hash && npm install",
    "install:history": "cd app-vue-history && npm install",
    "install:main": "cd main && npm install",
    "install:purehtml": "cd purehtml && npm install",
    "install-all": "npm-run-all install:*",
    "start:hash": "cd app-vue-hash && npm run serve ",
    "start:history": "cd app-vue-history && npm run serve",
    "start:main": "cd main && npm run serve",
    "start:purehtml": "cd purehtml && npm run serve",
    "start-all": "npm-run-all --parallel start:*",
    "serve-all": "npm-run-all --parallel start:*",
    "build:hash": "cd app-vue-hash && npm run build",
    "build:history": "cd app-vue-history && npm run build",
    "build:main": "cd main && npm run build",
    "build-all": "npm-run-all --parallel build:*"
  }
}

其中 --parallel 參數表示並行,沒有這個參數則是等上一個命令執行完纔會執行下一個命令。

結尾

不要對 iframe 抱有偏見,它也是微前端的一種實現方式,若是頁面上無彈窗、無全屏等操做,iframe 也是很好用的。配置緩存和 cdn 加速,若是是內網訪問,也不會很慢。

iframeqiankun 能夠並存,jQuery 多頁應用使用 iframe 接入就挺好,何時什麼場景該用哪一種方案,具體狀況具體分析。

最後,文章有什麼問題或錯誤歡迎指出,謝謝!

做者:沉末_
連接:https://juejin.im/post/5ed73b73e51d4578724e3fa4

服務推薦