阅读 4947

从0实现一个single-spa的前端微服务(下)

前言

上一篇文章:从0实现一个single-spa的前端微服务(中)中我们已经实现了single-spa + systemJS的前端微服务以及完善的开发和打包配置,今天主要讲一下这个方案存在的细节问题,以及qiankun框架的一些研究对比。

single-spa + systemJs 方案存在的问题及解决办法

single-spa的三个生命周期函数bootstrapmountunmount分别表示初始化、加载时、卸载时。

  1. 子系统导出bootstrapmountunmount函数是必需的,但是unload是可选的。
  2. 每个生命周期函数必须返回Promise
  3. 如果导出一个函数数组(而不只是一个函数),这些函数将一个接一个地调用,等待一个函数的promise解析后再调用下一个。

css污染问题的解决

我们知道,子系统卸载之后,其引入的css并不会被删掉,所以在子系统卸载时删掉这些css,是一种解决css污染的办法,但是不太好记录子系统引入了哪些css

我们可以借助换肤的思路来解决css污染,首先css-scoped解决95%的样式污染,然后就是全局样式可能会造成污染,我们只需要将全局样式用一个id/class包裹着就可以了,这样这些全局样式仅在这个id/class范围内生效。

具体做法就是:在子系统加载时(mount)给<body>加一个特殊的id/class,然后在子系统卸载时(unmount)删掉这个id/class。而子系统的全局样式都仅在这个id/class范围内生效,如果子系统独立运行,只需要在子系统的入口文件index.html里面给<body>手动加上这个id/class即可。

代码如下:

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下面:

.app-vue-history{
    h1{
        color: red
    }
}
复制代码

js污染问题的解决

暂时没有很好的办法解决,但是可以靠编码规范来约束:页面销毁之前清除自己页面上的定时器/全局事件,必要的时候,全局变量也应该销毁。

如何实现切换系统更换favicon.ico图标

这是一个比较常见的需求,类似还有某个系统需要插入一段特殊的js/css,而其他系统不需要,解决办法任然是在子系统加载时(mount)插入需要的js/css,在子系统卸载时(unmount)删掉。

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 返回的周期前面。

系统之间如何通信

系统之间通信一般有两种方式:自定义事件和本地存储。如果是两个系统相互跳转,可以用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文件地址:https://github.com/gongshun/single-spa-vue-demo

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沙箱功能,就变得索然无味了。

补充:qiankun 2.x 更新,对于不支持 proxy 的浏览器,支持 diff 方法来实现沙箱,就是子项目加载前浅拷贝一下 window,子项目卸载后 for 循环之前浅拷贝的 window,恢复之前的状态,但是多个子项目同时运行,他们共用一个沙箱,这等于没有沙箱。

补充: 全局函数的影响如何消除

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版本,2.x版本请看:改造已有的项目为 qiankun 子项目github 的 demo

主项目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 ,主要是允许跨域、以及打包成umd格式
const { name } = require('./package');

const port = 7101; // dev port
module.exports = {
  devServer: {
    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文件不是打包生成的就需要手动配置。

最后,有什么问题或者错误欢迎指出,互相成长,感谢!