美文网首页
qiankun 框架分析

qiankun 框架分析

作者: 我是小布丁 | 来源:发表于2021-10-17 21:25 被阅读0次

    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 是浏览器原生提供的一种能力,在过去的很长一段时间里,浏览器用它来封装一些元素的内部结构。以一个有着默认播放控制按钮的 <video> 元素为例,实际上,在它的 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 框架分析

          本文链接:https://www.haomeiwen.com/subject/lozioltx.html