qiankun3.0学习总结

1,345 阅读5分钟

qiankun3.0学习总结

小白第一次看源码(详细看),qiankun2也还没看过,只做过demo,为了完成大佬分配的任务啃了一段时间终于能得出个勉强的结论了==

一、前言

本文主要针对qiankun3.0是否能够支持Vite项目这个问题,从源码入手进行可行性验证,因此只针对部分源码进行解析。源码来自于github中发布的v3.0.0-alpha.0版本

下面先介绍一下相关概念,并说明为何在之前版本中不支持Vite创建的项目。

  1. 微前端

    微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。

    微前端类似于iframe,但是iframe中存在弹窗遮罩无法超越iframe边界等等问题,所以微前端作为新的方案来解决这些问题。

    优点:

    • 增量升级:对已存在的老应用可以直接作为子应用引入而不需要整体进行技术栈更新或重构
    • 独立开发、独立部署:每个子应用可以单独的进行开发、部署,不用去维护一个巨大的应用
    • 技术栈无关:基座应用不限制接入应用的技术栈,每个微应用都拥有独立的运行时,能够避免 DOM、CSS、JS 受到外部的影响,或者对其它应用产生影响。
  2. qiankun

    qiankun是对single-spa进行封装的一个库

无法支持Vite的两大原因:

  1. 在Vite3.2之前,Vite不支持runtime publicPath,webpack中由内置变量__ webpack_public_path __来提供,runtime publicPath是qiankun加载子应用的核心,qiankun需要根据publicPath来预加载子应用和异步引入脚本,Vite3.2中引入了experimental.renderBuiltUrl来支持runtime publicPath
  2. Vite打包后是ESM,qiankun的js沙箱需要通过IIFE执行来绑定自己代理的window,而import语句只能出现在顶级作用域

二、实践

tip:由于qiankun3.0中目前只有loadMicroApp方法,即手动加载微应用,后面使用该方法进行实践

2.1 配置

主应用(Vite + Vue):

import { loadMicroApp } from '../packages/qiankun/src' // 从源码导入方法

const microApp = loadMicroApp({
    name: 'son', // 子应用名称
    entry: '//localhost:3000', // 子应用入口
    container: document.querySelector('#app')!, // 基座应用中的容器,用于挂载子应用节点,type: HTMLElement
}, {
    sandbox: true // 开启沙盒,没有默认值,因此显示设置为true才能启用沙盒
})

microApp.mount() // 手动加载子应用

子应用(Vite + Vue):

  • vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
  base: 'http://localhost:3000/',
  plugins: [vue()],
  server: {
    port: 3000, // 端口号
    cors: true, // 设置为可跨域,因为qiankun需要通过entry请求资源
    origin: 'https://localhost:3000'
  }
})
  • main.ts
// @ts-nocheck
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'

let app;

function render(props = {}) {
    const { container } = props;
    app = createApp(App)
    app.mount(container ? container.querySelector('#app') : '#app')
}
// __POWERED_BY_QIANKUN__:是否为qiankun子应用
if (!window.__POWERED_BY_QIANKUN__) { 
    render();
}

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

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

export async function update() {
    console.log('[vue] vue app update');
}

export async function unmount() {
    app.unmount();
}

2.2 结果

看控制台可以知道loader报错了,提示子应用需要导出生命周期,可是我在子应用已经导出了生命函数,这个时候就需要看源码来寻找问题了

image-20230818104917302.png

三、源码阅读

3.1 整体流程(简略)

image-20230821165206415转存失败,建议直接上传图片文件

  1. 进入loadMicroApp函数
  2. 通过拼接name(注册子应用时的name)和XPath(注册子应用时的container的XPath)得到子应用实例的key,用于后面缓存
  3. 判断是否有parcelConfig(single-spa所需参数,使用Map进行缓存)缓存
    • 若有,则调用处理remount的函数将配置缓存的bootstrap改写为() => Promise.resolve(),即不执行bootstrap,同时在mount流水线第一步通过key得到实例缓存数组,并将当前实例所在下标前没有终止的实例进行unmount操作
    • 若无,调用核心load函数进行配置实例的创建,返回的配置实例进行缓存,接着调用mountRootParcel(single-spa函数),返回子应用实例,将子应用实例进行缓存,并且在unmount时调用清除缓存函数
  4. 返回子应用实例

3.2 针对问题部分探究(load函数)

image-20230821165206415.png

  1. 判断sandbox是否为true

    • 若是,则调用createSandboxContainer生成沙盒实例
  2. 根据entry计算得到publicPath(请求子应用资源的路径)

  3. 在生命周期中控制__ POWERED_BY_QIANKUN __ 和 __ INJECTED_PUBLIC_PATH_BY_QIANKUN __的值

  4. 执行beforeLoad生命周期

  5. 执行loadEntry,加载入口文件

    1. 请求entry文件

    2. 通过TreeWalker读取entry文件内容,拦截script标签和link标签

    3. 通过transpileAssets进行处理

      1. 对于script标签,判断sandbox实例是否存在

        • 若存在,计算得出实际src并请求,将请求到的代码通过IIFE(立即执行函数)绑定qiankun的代理window,并且通过Blob将返回字符串转换为URL赋值给script.src来实现控制script的加载

          const sourceMapURL = sourceURL ? `//# sourceURL=${sourceURL}\n` : '';
          
          const globalObjectConstants = ['window', 'globalThis'];
          const globalObjectOptimizer = `const {${globalObjectConstants.join(',')}} = this;`;
          
          nativeGlobal[this.id] = this.globalThis; // this.id为window上的qiankun的代理window对应Id,this.globalThis为qiankun代理的window实例
          
          return `;(function(){with(this){${globalObjectOptimizer}${source}\n${sourceMapURL}}}).bind(window.${this.id})();`;
          
        • 若不存在,即没有开启沙盒模式,计算出实际src赋值并返回

      2. 对于link标签:计算出实际src赋值并返回

  6. 获取生命周期(在qiankun2中会通过import-entry-html包来处理第5步,并返回了生命周期),而qiankun3.0中目前第5步没有返回值,并且获取生命周期函数的第一个参数设置为空,这就是为什么在前面会报无法获取生命周期的错误

    const { bootstrap, mount, unmount, update } = getLifecyclesFromExports(
        {},
        appName,
        global,
        sandboxInstance?.latestSetProp,
    );
    
  7. 整合得出single-spa所需的parcelConfig并返回

四、结论

根据以上对源码的阅读,得知目前qiankun发布的v3.0.0-alpha.0版本暂时无法支持Vite进行应用,甚至现在无法启动,主要原因在于生命周期的获取。

采用了loadEntry代替import-entry-html包,内部还有未完成部分。