qiankun 框架分析

qiankun是基于 single-spa 做的二次封装,主要解决了single-spa 的一些痛点和不足。

single-spa存在的问题?

  • 1、对微应用侵入性太强
    微应用的改造步骤:
    • 微应用路由改造,添加一个特定的前缀
    • 微应用入口改造,挂载点变更和生命周期函数导出
    • 打包工具配置更改

single-spa 采用JS Entry 的方式接入微应用。也就是说single-spa 接入微应用需要将微应用整个打包成一个JS文件,发布到静态资源服务器,然后再主应用中配置该JS文件的地址告诉 single-spa 去这个地址加载微应用。问题出现了,如按需加载、首屏资源加载优化、css独立打包等优化没有了。

  • 2、样式隔离
    single-spa 没有做。怎么做到主应用和微应用之间的样式,微应用和微应用的样式互不影响?这个只能通过约定命名规范来实现,比如应用样式以自己的应用名称开头。

  • 3、JS隔离
    single-spa 没有做。JS全局对象污染,A应用在window上加一个自己的属性window.A,微应用B 也能访问到。

  • 4、资源预加载
    single-spa 没有做。例如怎么实现在第一个微应用加载完后,后台悄悄加载其他微应用。

  • 5、应用间通信
    single-spa 没有做。它只在注册微应用时给微应用注入一些状态信息,后续就不管了,没有任何通信的手段。

qiankun 如何解决以上问题

  • 1、HTML Entry
    qiankun 通过HTML Entry 的方式来解决JS Entry带来的问题

  • 2、样式隔离
    采用shadow dom 包裹没一个微应用,从而确保微应用的样式互不干扰
    采用css scoped 方式(实验性)动态改写 css 选择器来实现

  • 3、运行时沙箱
    qiankun的运行时沙箱分为 JS 沙箱和样式沙箱

  • 4、资源预加载
    qiankun 实现预加载的思路有两种,一种是当主应用执行 start 方法启动 qiankun 以后立即去预加载微应用的静态资源,另一种是在第一个微应用挂载以后预加载其它微应用的静态资源,这个是利用 single-spa 提供的 single-spa:first-mount 事件来实现的

  • 5、应用间通信
    qiankun 通过发布订阅模式来实现应用间通信

示例项目

官网地址
源码地址

yarn examples:install
yarn examples:start

qiankun 提供了6种实例,vue、vue3、react15、react16、angular9、purehtml。

image.png

主应用在 examples/main 目录下,提供了两种实现方式,基于路由配置的 registerMicroApps 和 手动加载微应用的loadMicroApp。通过 webpak.config.js 的 entry 可以知道有两个入口文件 multiple.js 和 index.js。

  • 1、基于路由配置
    在 examples/main/index.js 中,将微应用关联到一些 url 规则,实现当浏览器 url 发生变化时,自动加载相应的微应用。主应用可以使用react进行运行,也可以使用vue进行运行。
registerMicroApps(
  [
    {
      name: 'vue',
      entry: '//localhost:7101',
      container: '#subapp-viewport',
      loader,
      activeRule: '/vue',
    },
  ],
  {
    beforeLoad: [
      app => {
        console.log('[LifeCycle] before load %c%s', 'color: green;', app.name);
      },
    ],
    beforeMount: [
      app => {
        console.log('[LifeCycle] before mount %c%s', 'color: green;', app.name);
      },
    ],
    afterUnmount: [
      app => {
        console.log('[LifeCycle] after unmount %c%s', 'color: green;', app.name);
      },
    ],
  },
);
  • 2、手动加载微应用
    在 examples/main/multiple.js 中有loadMicroApp实现的例子
function mount() {
  app = loadMicroApp(
    { name: 'react15', entry: '//localhost:7102', container: '#react15' },
    { sandbox: { experimentalStyleIsolation: true } },
  );
}

vue微应用引入,需要修改 vue.config.js 和 mian.js 、public-path.js

{
  ...
  // publicPath 没在这里设置,是通过 webpack 提供的全局变量 __webpack_public_path__ 来即时设置的,webpackjs.com/guides/public-path/
  devServer: {
    ...
    // 设置跨域,因为主应用需要通过 fetch 去获取微应用引入的静态资源的,所以必须要求这些静态资源支持跨域
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
  },
  output: {
    // 把子应用打包成 umd 库格式
    library: `${name}-[name]`,  // 库名称,唯一
    libraryTarget: 'umd',
    jsonpFunction: `webpackJsonp_${name}`,
  }
  ...
}
let router = null;
let instance = null;

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

  instance = new Vue({
    router,
    store,
    render: h => h(App),
  }).$mount(container ? container.querySelector('#app') : '#app');
}

if (!window.__POWERED_BY_QIANKUN__) {
  render();
}
export async function bootstrap() {
  console.log('[vue] vue app bootstraped');
}

export async function mount(props) {
  console.log('[vue] props from main framework', props);
  render(props);
}

export async function unmount() {
  instance.$destroy();
  instance.$el.innerHTML = '';
  instance = null;
  router = null;
}
if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

运行时沙箱

运行时沙箱包括 JS 沙箱 和 样式沙箱

JS 沙箱

JS 沙箱是通过 proxy 代理 window 对象,记录window对象上属性的增删改查

  • 单例模式
    直接代理了原生 window 对象,记录原生 window 对象的增删改查,当 window 对象激活时恢复 window 对象到上次即将失活时的状态,失活时恢复 window 对象到初始初始状态
  • 多例模式
    代理了一个全新的对象,这个对象是复制的 window 对象的一部分不可配置属性,所有的更改都是基于这个 fakeWindow 对象,从而保证多个实例之间属性互不影响

样式沙箱

样式沙箱实际做的事情其实很简单,就是将动态添加的 script、link、style 这三个元素插入到对的位置,属于主应用的插入主应用,属于微应用的插入到对应的微应用中,方便微应用卸载的时候一起删除,当然样式沙箱还额外做了两件事:
(1)在卸载之前为动态添加样式做缓存,在微应用重新挂载时再插入到微应用内
(2)将 proxy 对象传递给 execScripts 函数,将其设置为微应用的执行上下文

  • 样式隔离
    qiankun 的样式隔离有两种方式,一种是严格样式隔离,通过 shadow dom 来实现,另一种是实验性的样式隔离,就是 scoped css,两种方式不可共存。

    在 qiankun 中的严格样式隔离,就是在这个 createElement 方法中做的,通过 shadow dom 来实现, shadow dom 是浏览器原生提供的一种能力,在过去的很长一段时间里,浏览器用它来封装一些元素的内部结构。以一个有着默认播放控制按钮的

  • 实验性样式隔离
    实验性样式的隔离方式其实就是 scoped css,qiankun 会通过动态改写一个特殊的选择器约束来限制 css 的生效范围

HTML Entry

HTML Entry 是由 import-html-entry 库实现的,通过 http 请求加载指定地址的首屏内容即 html 页面,然后解析这个 html 模版得到 template, scripts , entry, styles。

{
  template: 经过处理的脚本,link、script 标签都被注释掉了,
  scripts: [脚本的http地址 或者 { async: true, src: xx } 或者 代码块],
  styles: [样式的http地址],
  entry: 入口脚本的地址,要不是标有 entry 的 script 的 src,要不就是最后一个 script 标签的 src
}

然后远程加载 styles 中的样式内容,将 template 模版中注释掉的 link 标签替换为相应的 style 元素。然后向外暴露一个 Promise 对象。

{
    // template 是 link 替换为 style 后的 template
    template: embedHTML,
    // 静态资源地址
    assetPublicPath,
    // 获取外部脚本,最终得到所有脚本的代码内容
    getExternalScripts: () => getExternalScripts(scripts, fetch),
    // 获取外部样式文件的内容
    getExternalStyleSheets: () => getExternalStyleSheets(styles, fetch),
    // 脚本执行器,让 JS 代码(scripts)在指定 上下文 中运行
    execScripts: (proxy, strictGlobal) => {
        if (!scripts.length) {
            return Promise.resolve();
        }
        return execScripts(entry, scripts, proxy, { fetch, strictGlobal });
    }
}

HTML Entry 最终会返回一个 Promise 对象,qiankun 就用了这个对象中的 template、assetPublicPath 和 execScripts 三项,将 template 通过 DOM 操作添加到主应用中,执行 execScripts 方法得到微应用导出的生命周期方法,并且还顺便解决了 JS 全局污染的问题,因为执行 execScripts 方法的时候可以通过 proxy 参数指定 JS 的执行上下文。

内容来源

微前端框架 之 qiankun 从入门到源码分析
qiankun 2.x 运行时沙箱 源码分析
HTML Entry 源码分析

你可能感兴趣的:(qiankun 框架分析)