從0實現一個single-spa的前端微服務(下)

前言

上一篇文章:從0實現一個single-spa的前端微服務(中)中咱們已經實現了single-spa + systemJS的前端微服務以及完善的開發和打包配置,今天主要講一下這個方案存在的細節問題,以及qiankun框架的一些研究對比。javascript

single-spa + systemJs 方案存在的問題及解決辦法

single-spa的三個生命週期函數bootstrapmountunmount分別表示初始化、加載時、卸載時。css

  1. 子系統導出bootstrapmountunmount函數是必需的,可是unload是可選的。
  2. 每一個生命週期函數必須返回Promise
  3. 若是導出一個函數數組(而不僅是一個函數),這些函數將一個接一個地調用,等待一個函數的promise解析後再調用下一個。

css污染問題的解決

咱們知道,子系統卸載以後,其引入的css並不會被刪掉,因此在子系統卸載時刪掉這些css,是一種解決css污染的辦法,可是不太好記錄子系統引入了哪些csshtml

咱們能夠藉助換膚的思路來解決css污染,首先css-scoped解決95%的樣式污染,而後就是全局樣式可能會形成污染,咱們只須要將全局樣式用一個id/class包裹着就能夠了,這樣這些全局樣式僅在這個id/class範圍內生效。前端

具體作法就是:在子系統加載時(mount)給<body>加一個特殊的id/class,而後在子系統卸載時(unmount)刪掉這個id/class。而子系統的全局樣式都僅在這個id/class範圍內生效,若是子系統獨立運行,只須要在子系統的入口文件index.html裏面給<body>手動加上這個id/class便可。vue

代碼以下:java

async function mount(props){
  //給body加class,以解決全局樣式污染
  document.body.classList.add('app-vue-history')
}
async function unmount(props){
  //去掉body的class
  document.body.classList.remove('app-vue-history')
}
複製代碼

固然了,你寫的全局樣式也在這個class下面:node

.app-vue-history{
    h1{
        color: red
    }
}
複製代碼

js污染問題的解決

暫時沒有很好的辦法解決,可是能夠靠編碼規範來約束:頁面銷燬以前清除本身頁面上的定時器/全局事件,必要的時候,全局變量也應該銷燬。webpack

如何實現切換系統更換favicon.ico圖標

這是一個比較常見的需求,相似還有某個系統須要插入一段特殊的js/css,而其餘系統不須要,解決辦法任然是在子系統加載時(mount)插入須要的js/css,在子系統卸載時(unmount)刪掉。git

const headEle = document.querySelector('head');
let linkEle = null ;
// 由於新插入的icon會覆蓋舊的,因此舊的不用刪除,若是須要刪除,能夠在unmount時再插入進來
async function mount(props){
  linkEle = document.createElement("link");
  linkEle.setAttribute('rel','icon');
  linkEle.setAttribute('href','https://gold-cdn.xitu.io/favicons/favicon.ico');
  headEle.appendChild(linkEle);
}
async function unmount(props){
  headEle.removeChild(linkEle);
  linkEle = null;
}
複製代碼

注意:上面例子中是修改icon標籤,不影響頁面的加載。若是某個子系統須要在頁面加載以前加載某個js(例如配置文件),須要將加載 js 的函數寫成 promise,而且將這個周期函數放到 single-spa-vue 返回的週期前面。 github

系統之間如何通訊

系統之間通訊通常有兩種方式:自定義事件和本地存儲。若是是兩個系統相互跳轉,能夠用URL傳數據。

通常來講,不會同時存在A、B兩個子系統,常見的數據共享就是登錄信息,登錄信息通常使用本地存儲記錄。另一個常見的場景就是子系統修改了用戶信息,主系統須要從新請求用戶信息,這個時候通常用自定義事件通訊,自定義事件具體如何操做,能夠看上一篇文章的例子。

另外,single-spa的註冊函數registerApplication,第四個參數能夠傳遞數據給子系統,但傳遞的數據必須是一個對象

註冊子系統的時候:

singleSpa.registerApplication(
    'appVueHistory',
    () => System.import('appVueHistory'),
    location => location.pathname.startsWith('/app-vue-history/'),
    { authToken: "d83jD63UdZ6RS6f70D0" }
)
複製代碼

子系統(appVueHistory)接收數據:

export function mount(props) {
  //官方文檔寫的是props.customProps.authToken,實際上發現是props.authToken
  console.log(props.authToken); 
  return vueLifecycles.mount(props);
}
複製代碼

關於子系統的生命週期函數:

  1. 生命週期函數bootstrap,mountunmount均包含參數props
  2. 參數props是一個對象,包含namesingleSpamountParcelcustomProps 。不一樣的版本可能略有差別
  3. 參數對象中 customProps 就是註冊的時候傳遞過來的參數

子系統如何實現keep-alive

查看single-spa-vue源碼能夠發現,在unmount生命週期,它將vue實例destroy(銷燬了)而且清空了DOM。因此實現keep-alive的關鍵在於子系統的unmount週期中不銷燬vue實例而且不清空DOM,採用display:none來隱藏子系統。而在mount週期,先判斷子系統是否存在,若是存在,則去掉其display:none便可。

咱們須要修改single-spa-vue的部分源代碼:

function mount(opts, mountedInstances, props) {
  let instance = mountedInstances[props.name];
  return Promise.resolve().then(() => {
    //先判斷是否已加載,若是是,則直接將其顯示出來
    if(!instance){
      //這裏面都是其源碼,生成DOM並實例化vue的部分
      instance = {};
      const appOptions = { ...opts.appOptions };
      if (props.domElement && !appOptions.el) {
        appOptions.el = props.domElement;
      }
      let domEl;
      if (appOptions.el) {
        if (typeof appOptions.el === "string") {
          domEl = document.querySelector(appOptions.el);
          if (!domEl) {
            throw Error(
              `If appOptions.el is provided to single-spa-vue, the dom element must exist in the dom. Was provided as ${appOptions.el}`
            );
          }
        } else {
          domEl = appOptions.el;
        }
      } else {
        const htmlId = `single-spa-application:${props.name}`;
        // CSS.escape 的文檔(需考慮兼容性)
        // https://developer.mozilla.org/zh-CN/docs/Web/API/CSS/escape
        appOptions.el = `#${CSS.escape(htmlId)}`;
        domEl = document.getElementById(htmlId);
        if (!domEl) {
          domEl = document.createElement("div");
          domEl.id = htmlId;
          document.body.appendChild(domEl);
        }
      }
      appOptions.el = appOptions.el + " .single-spa-container";
      // single-spa-vue@>=2 always REPLACES the `el` instead of appending to it.
      // We want domEl to stick around and not be replaced. So we tell Vue to mount
      // into a container div inside of the main domEl
      if (!domEl.querySelector(".single-spa-container")) {
        const singleSpaContainer = document.createElement("div");
        singleSpaContainer.className = "single-spa-container";
        domEl.appendChild(singleSpaContainer);
      }
      instance.domEl = domEl;
      if (!appOptions.render && !appOptions.template && opts.rootComponent) {
        appOptions.render = h => h(opts.rootComponent);
      }
      if (!appOptions.data) {
        appOptions.data = {};
      }
      appOptions.data = { ...appOptions.data, ...props };
      instance.vueInstance = new opts.Vue(appOptions);
      if (instance.vueInstance.bind) {
        instance.vueInstance = instance.vueInstance.bind(instance.vueInstance);
      }
      mountedInstances[props.name] = instance;
    }else{
      instance.vueInstance.$el.style.display = "block";
    }
    return instance.vueInstance;
  });
}
function unmount(opts, mountedInstances, props) {
  return Promise.resolve().then(() => {
    const instance = mountedInstances[props.name];
    instance.vueInstance.$el.style.display = "none";
  });
}
複製代碼

而子系統內部頁面則和正常vue系統同樣使用<keep-alive>標籤來實現緩存。

如何實現子系統的預請求(預加載)

vue-router路由配置的時候可使用按需加載(代碼以下),按需加載以後路由文件就會單獨打包成一個jscss

path: "/about",
name: "about",
component: () => import( "../views/About.vue")
複製代碼

vue-cli3生成的模板打包後的index.html中是有使用prefetchpreload來實現路由文件的預請求的:

<link href=/js/about.js rel=prefetch>
<link href=/js/app.js rel=preload as=script>
複製代碼

prefetch預請求就是:瀏覽器網絡空閒的時候請求並緩存文件

systemJs只能拿到入口文件,其餘的路由文件是按需加載的,沒法實現預請求。可是若是你沒有使用路由的按需加載,則全部路由文件都打包到一個文件(app.js),則能夠實現預請求。

上述完整demo文件地址:github.com/gongshun/si…

qiankun框架

qiankun是螞蟻金服開源的基於single-spa的一個前端微服務框架。

js沙箱(sandbox)是如何實現的

咱們知道全部的全局的方法(alertsetTimeoutisNaN等)、全局的變/常量(NaNInfinityvar聲明的全局變量等)和全局對象(ArrayStringDate等)都屬於window對象,而能致使js污染的也就是這些全局的方法和對象。

因此qiankun解決js污染的辦法是:在子系統加載以前對window對象作一個快照(拷貝),而後在子系統卸載的時候恢復這個快照,便可以保證每次子系統運行的時候都是一個全新的window對象環境。

那麼如何監測window對象的變化呢,直接將window對象進行一下深拷貝,而後深度對比各個屬性顯然可行性不高,qiankun框架採用的是ES6新特性,proxy代理方法。

具體代碼以下(源代碼是ts版的,我簡化修改了一些):

// 沙箱期間新增的全局變量
const addedPropsMapInSandbox = new Map();
// 沙箱期間更新的全局變量
const modifiedPropsOriginalValueMapInSandbox = new Map();
// 持續記錄更新的(新增和修改的)全局變量的 map,用於在任意時刻作 snapshot
const currentUpdatedPropsValueMap = new Map();
const boundValueSymbol = Symbol('bound value');
const rawWindow = window;
const fakeWindow = Object.create(null);
const sandbox = new Proxy(fakeWindow, {
    set(target, propKey, value) {
      if (!rawWindow.hasOwnProperty(propKey)) {
        addedPropsMapInSandbox.set(propKey, value);
      } else if (!modifiedPropsOriginalValueMapInSandbox.has(propKey)) {
        // 若是當前 window 對象存在該屬性,且 record map 中未記錄過,則記錄該屬性初始值
        const originalValue = rawWindow[propKey];
        modifiedPropsOriginalValueMapInSandbox.set(propKey, originalValue);
      }
      currentUpdatedPropsValueMap.set(propKey, value);
      // 必須從新設置 window 對象保證下次 get 時能拿到已更新的數據
      rawWindow[propKey] = value;
      // 在 strict-mode 下,Proxy 的 handler.set 返回 false 會拋出 TypeError,
      // 在沙箱卸載的狀況下應該忽略錯誤
      return true;
    },
    get(target, propKey) {
      if (propKey === 'top' || propKey === 'window' || propKey === 'self') {
        return sandbox;
      }
      const value = rawWindow[propKey];
      // isConstructablev :監測函數是不是構造函數
      if (typeof value === 'function' && !isConstructable(value)) {
        if (value[boundValueSymbol]) {
          return value[boundValueSymbol];
        }
        const boundValue = value.bind(rawWindow);
        Object.keys(value).forEach(key => (boundValue[key] = value[key]));
        Object.defineProperty(value, boundValueSymbol, 
            { enumerable: false, value: boundValue }
        )
        return boundValue;
      }
      return value;
    },
    has(target, propKey) {
      return propKey in rawWindow;
    },
});
複製代碼

大體原理就是記錄window對象在子系統運行期間新增、修改和刪除的屬性和方法,而後會在子系統卸載的時候復原這些操做。

這樣處理以後,全局變量能夠直接復原,可是事件監聽和定時器須要特殊處理:用addEventListener添加的事件,須要用removeEventListener方法來移除,定時器也須要特殊函數才能清除。因此它重寫了事件綁定/解綁和定時器相關函數。

重寫定時器(setInterval)部分代碼以下:

const rawWindowInterval = window.setInterval;
const hijack = function () {
  const timerIds = [];
  window.setInterval = (...args) => {
    const intervalId = rawWindowInterval(...args);
    intervalIds.push(intervalId);
    return intervalId;
  };
  return function free() {
    window.setInterval = rawWindowInterval;
    intervalIds.forEach(id => {
      window.clearInterval(id);
    });
  };
}

複製代碼

小細節:切換子系統不能立馬清除子系統的延時定時器,好比說子系統有一個message提示,3秒鐘後自動關閉,若是你立馬清除掉了,就會一直存在了。那麼延遲多久再清除子系統的定時器合適呢?5s?7s?10s?彷佛都不太理想,做者最終決定不清除setTimeout,畢竟使用了一次以後就沒用了,影響不大。

因爲qiankun在js沙箱功能中使用了proxy新特性,因此它的兼容性和vue3同樣,不支持IE11及如下版本的IE。不過做者說能夠嘗試禁用沙箱功能來提升兼容性,可是不保證都能運行。去掉了js沙箱功能,就變得索然無味了。

補充: 全局函數的影響如何消除

function關鍵字直接聲明一個全局函數,這個函數屬於window對象,可是沒法被delete:

function a(){}
Object.getOwnPropertyDescriptor(window, "a")
//控制檯打印以下信息
/*{ value: ƒ a(), writable: true, enumerable: true, configurable: false }*/
delete window.a // 返回false,表示刪除失敗
複製代碼

configurable:當且僅當指定對象的屬性描述能夠被改變或者屬性可被刪除時,爲true

既然沒法被delete,那麼qiankunjs沙箱是如何作的呢,它是怎樣消除子系統的全局函數的影響的呢?

聲明全局函數有兩種辦法,一種是function關鍵字在全局環境下聲明,另外一種是以變量的形式添加:window.a = () => {}。咱們知道function聲明的全局函數是沒法刪除的,而變量的形式是能夠刪除的,qiankun直接避免了function關鍵字聲明的全局函數。

首先,咱們編寫在.vue文件或者main.js文件中function聲明的函數都不是全局函數,它只屬於當前模塊的。只有index.html中直接寫的全局函數,或者不被打包文件裏面的函數是全局的。

index.html中編寫的全局函數,會被處理成局部函數。 源代碼:

<script> function b(){} //測試全局變量污染 console.log('window.b',window.b) </script>
複製代碼

qiankun處理後:

(function(window){;
    function b(){}
    //測試全局變量污染
    console.log('window.b',window.b)
}).bind(window.proxy)(window.proxy);
複製代碼

那他是如何實現的呢?首先用正則匹配到index.html裏面的外鏈js和內聯js,而後外鏈js請求到內容字符串後存儲到一個對象中,內聯js直接用正則匹配到內容也記錄到這個對象中:

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

而後運行的時候,採用eval函數:

//內聯js
eval(`;(function(window){;${inlineScript}\n}).bind(window.proxy)(window.proxy);`)
//外鏈js
eval(`;(function(window){;${downloadedScriptText}\n}).bind(window.proxy)(window.proxy);`))
複製代碼

同時,他還會考慮到外鏈jsasync屬性,即考慮到js文件的前後執行順序,不得不說,這個做者真的是細節滿滿。

css污染他是如何解決的

它解決css污染的辦法是:在子系統卸載的時候,將子系統引入css使用的<link><style>標籤移除掉。移除的辦法是重寫<head>標籤的appendChild方法,辦法相似定時器的重寫。

子系統加載時,會將所須要的js/css文件插入到<head>標籤,而重寫的appendChild方法會記錄所插入的標籤,而後子系統卸載的時候,會移除這些標籤。

預請求是如何實現的

解決子系統預請求的的根本在於,咱們須要知道子系統有哪些js/css須要加載,而藉助systemJs加載子系統,只知道子系統的入口文件(app.js)。qiankun不只支持app.js做爲入口文件,還支持index.html做爲入口文件,它會用正則匹配出index.html裏面的js/css標籤,而後實現預請求。

網絡很差和移動端訪問的時候,qiankun不會進行預請求,移動端大可能是使用數據流量,預請求則會浪費用戶流量,判斷代碼以下:

const isMobile = 
   /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
const isSlowNetwork = navigator.connection
  ? navigator.connection.saveData || /(2|3)g/.test(navigator.connection.effectiveType)
  : false;
複製代碼

請求js/css文件它採用的是fetch請求,若是瀏覽器不支持,還須要polyfill

如下代碼就是它請求js並進行緩存:

const defaultFetch = window.fetch.bind(window);
//scripts是用正則匹配到的script標籤
function getExternalScripts(scripts, fetch = defaultFetch) {
    return Promise.all(scripts.map(script => {
	if (script.startsWith('<')) {
	    // 內聯js代碼塊
	    return getInlineCode(script);
	} else {
	    // 外鏈js
	    return scriptCache[script] ||
	           (scriptCache[script] = fetch(script).then(response => response.text()));
	}
    }));
}
複製代碼

用qiankun框架實現微前端

qiankun源碼中已經給出了使用示例,使用起來也很是簡單好用。接下來我演示下如何從0開始用qianklun框架實現微前端,內容改編自官方使用示例。PS:基於qiankun1版本

主項目main

  1. vue-cli3生成一個全新的vue項目,注意路由使用history模式。
  2. 安裝qiankun框架:npm i qiankun -S
  3. 修改app.vue,使其成爲菜單和子項目的容器。其中兩個數據,loading就是加載的狀態,而content則是子系統生成的HTML片斷(子系統獨立運行時,這個HTML片斷會被插入到#app裏面的)
<template>
  <div id="app">
    <header>
      <router-link to="/app-vue-hash/">app-vue-hash</router-link>
      <router-link to="/app-vue-history/">app-vue-history</router-link>
    </header>
    <div v-if="loading" class="loading">loading</div>
    <div class="appContainer" v-html="content">content</div>
  </div>
</template>

<script>
export default {
  props: {
    loading: {
      type: Boolean,
      default: false
    },
    content: {
      type: String,
      default: ''
    },
  },
}
</script>
複製代碼
  1. 修改main.js,註冊子項目,子項目入口文件採用index.html
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import { registerMicroApps, start } from 'qiankun';
Vue.config.productionTip = false
let app = null;
function render({ appContent, loading }) {
  if (!app) {
    app = new Vue({
      el: '#container',
      router,
      data() {
        return {
          content: appContent,
          loading,
        };
      },
      render(h){
        return h(App, {
          props: {
            content: this.content,
            loading: this.loading,
          },
        })
      } 
    });
  } else {
    app.content = appContent;
    app.loading = loading;
  }
}
function initApp() {
  render({ appContent: '', loading: false });
}
initApp();
function genActiveRule(routerPrefix) {
  return location => location.pathname.startsWith(routerPrefix);
}
registerMicroApps([
  { 
    name: 'app-vue-hash',
    entry: 'http://localhost:80', 
    render, 
    activeRule: genActiveRule('/app-vue-hash')
  },
  { 
    name: 'app-vue-history', 
    entry: 'http://localhost:1314',
    render, 
    activeRule: genActiveRule('/app-vue-history') 
  },
]);
start();
複製代碼

注意:主項目中的index.html模板裏面的<div id="app"></div>須要改成<div id="container"></div>

子項目app-vue-hash

  1. vue-cli3生成一個全新的vue項目,注意路由使用hash模式。
  2. src目錄新增文件public-path.js,注意用於修改子項目的publicPath
if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
複製代碼
  1. 修改main.js,配合主項目導出single-spa須要的三個生命週期。注意:路由實例化須要在main.js裏面完成,以便於路由的銷燬,因此路由文件只須要導出路由配置便可(原模板導出的是路由實例)
import './public-path';
import Vue from 'vue';
import VueRouter from 'vue-router';
import App from './App.vue';
import routes from './router';
import store from './store';

Vue.config.productionTip = false;
let router = null;
let instance = null;
function render() {
  router = new VueRouter({
    routes,
  });
  instance = new Vue({
    router,
    store,
    render: h => h(App),
  }).$mount('#appVueHash');// index.html 裏面的 id 須要改爲 appVueHash,不然子項目沒法獨立運行
}
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();
}
export async function unmount() {
  instance.$destroy();
  instance = null;
  router = null;
}
複製代碼
  1. 修改打包配置文件vue.config.js ,主要是容許跨域、關閉熱更新、去掉文件的hash值、以及打包成umd格式
const path = require('path');
const { name } = require('./package');
function resolve(dir) {
  return path.join(__dirname, dir);
}
const port = 7101; // dev port
module.exports = {
  filenameHashing: true,
  devServer: {
    hot: true,
    disableHostCheck: true,
    port,
    overlay: {
      warnings: false,
      errors: true,
    },
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
  },
  // 自定義webpack配置
  configureWebpack: {
    output: {
      // 把子應用打包成 umd 庫格式
      library: `${name}-[name]`,
      libraryTarget: 'umd',
      jsonpFunction: `webpackJsonp_${name}`,
    },
  },
};
複製代碼

子項目app-vue-history

history模式的vue項目與hash模式只有一個地方不一樣,其餘的如出一轍。

main.js裏面路由實例化的時候須要加入條件判斷,注入路由前綴

function render() {
  router = new VueRouter({
    base: window.__POWERED_BY_QIANKUN__ ? '/app-vue-history' : '/',
    mode: 'history',
    routes,
  });

  instance = new Vue({
    router,
    store,
    render: h => h(App),
  }).$mount('#appVueHistory');
}
複製代碼

項目之間的通訊

自定義事件能夠傳遞數據,可是彷佛不太完美,數據不具有「雙向傳遞性」。若是想在父子項目都能修改這個數據,而且都能響應,咱們須要實現一個頂級vuex

具體思路:

  1. 在主項目實例化一個Vuex,而後在子項目註冊時候傳遞給子項目
  2. 子項目在mounted生命週期拿到主項目的Vuex,而後註冊到全局去:new Vue的時候,在data中聲明,這樣子項目的任何一個組件均可以經過this.$root訪問到這個Vuex

大體代碼以下,

主項目main.js:

import store from './store';

registerMicroApps([
  { 
    name: 'app-vue-hash', 
    entry: 'http://localhost:7101', 
    render, 
    activeRule: genActiveRule('/app-vue-hash'), 
    props: { data : store } 
  },
  { 
    name: 'app-vue-history', 
    entry: 'http://localhost:1314', 
    render, 
    activeRule: genActiveRule('/app-vue-history'), 
    props: { data : store } 
  },
]);
複製代碼

子項目的main.js:

function render(parentStore) {
  router = new VueRouter({
    routes,
  });
  instance = new Vue({
    router,
    store,
    data(){
      return {
        store: parentStore,
      }
    },
    render: h => h(App),
  }).$mount('#appVueHash');
}
export async function mount(props) {
  render(props.data);
}
複製代碼

子項目的Home.vue中使用:

<template>
  <div class="home">
    <span @click="changeParentState">主項目的數據:{{ commonData.parent }},點擊變爲2</span>
  </div>
</template>
<script> export default { computed: { commonData(){ return this.$root.store.state.commonData; } }, methods: { changeParentState(){ this.$root.store.commit('setCommonData', { parent: 2 }); } }, } </script>
複製代碼

其餘

  1. 若是想關閉js沙箱和預請求,在start函數中配置便可
start({
    prefetch: false, //默認是true,可選'all'
    jsSandbox: false, //默認是true
})
複製代碼
  1. 子項目註冊函數registerMicroApps也能夠傳遞數據給子項目,而且能夠設置全局的生命週期函數
// 其中app對象的props屬性就是傳遞給子項目的數據,默認是空對象
registerMicroApps(
  [
    { 
        name: 'app-vue-hash', 
        entry: 'http://localhost:80', 
        render, activeRule: 
        genActiveRule('/app-vue-hash') , 
        props: { data : 'message' } 
    },
    { 
        name: 'app-vue-history', 
        entry: 'http://localhost:1314', 
        render, 
        activeRule: genActiveRule('/app-vue-history') 
    },
  ],
  {
    beforeLoad: [
      app => { console.log('before load', app); },
    ],
    beforeMount: [
      app => { console.log('before mount', app); },
    ],
    afterUnmount: [
      app => { console.log('after unload', app); },
    ],
  },
);
複製代碼
  1. qiankun的官方文檔:qiankun.umijs.org/zh/api/#reg…

  2. 上述demo的完整代碼github.com/gongshun/qi…

總結

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

  2. 拋開兼容性,我以爲qiankun真的太好用了,無需對子項目作過多的修改,開箱即用。也不須要對子項目的開發部署作任何額外的操做。

  3. qiankun框架使用index.html做爲子項目的入口,會將裏面的style/link/script標籤以及註釋代碼解析並插入,可是他沒有考慮metatitle標籤,若是切換系統,其中meta標籤有變化,則不會解析並插入,固然了,meta標籤不影響頁面展現,這樣的場景並很少。而切換系統,修改頁面的title,則須要經過全局鉤子函數來實現。

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

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

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

JS entry的要求比較苛刻:

(1)將css打包到js裏面

(2)去掉chunk-vendors.js

(3)去掉文件名的hash

(4)將入口文件(app.js)放置到index.html目錄,其餘文件不變,緣由是要截取app.js的路徑做爲publicPath

APP entry 優勢 缺點
JS entry 能夠複用公共依賴(vue,vuex,vue-router等) 須要各類打包配置配合,沒法實現預加載
HTML entry 簡單方便,能夠預加載 多一層請求,須要先請求到HTML文件,再用正則匹配到其中的js和css,沒法複用公共依賴(vue,vuex,vue-router等)

我以爲能夠將入口文件改成二者配合,使用一個對象來配置:

{
  publicPath: 'http://www.baidu.com',
  entry: [
    "app.3249afbe.js"
    "chunk-vendors.75fba470.js",
  ],
  preload: [
    "about.3149afve.js",
    "test.71fba472.js",
  ]
}
複製代碼

這樣既能夠實現預加載,又能夠複用公共依賴,而且不用修改太多的打包配置。難點在於如何將子系統須要的js文件寫到配置文件裏面去,有兩個思路:方法1:寫一個node服務,按期(或者子系統有更新時)去請求子系統的index.html文件,而後正則匹配到裏面的js。方法2:子系統打包時,webpack會將生成的js/css文件的請求插入到index.html中(HtmlWebpackPlugin),那麼是否也能夠將這些js文件的名稱發送到服務器記錄,可是有些靜態js文件不是打包生成的就須要手動配置。

最後,有什麼問題或者錯誤歡迎指出,互相成長,感謝!

相關文章
相關標籤/搜索