源码解析系列-qiankun源码详解

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第17天,点击查看活动详情

前言

之前我们介绍了三大微前端框架的使用,现在我们再深入地看一看他们的实现原理;single-spa是通过router的形式实现的微前端,有两种模式:history和hash,hash模式则是监听hashchange,而history模式则是监听popstate;qiankun在single-spa基础上新增了html的解析,这个解析过程是qiankun的核心,也是今天我们要讨论的内容;

不想看分析过程的可以直接看总结

qiankun目录结构

git clone https://github.com/umijs/qiankun.git然后查看src下的目录结构

image.png

tests是单元测试

apis是暴露出去的一些方法,例如start、registerApplication

globalState组件间通信解决方案

interface ts类型声明

loader 加载微应用的核心:loadApp

prefetch预加载

sandbox 实现了js沙箱

还记得上一次我们讲的qiankun如何注册微应用了吗?用了registerMicroApps这个方法,那么现在我们就顺藤摸瓜,从这个方法开始来研究一下qiankun的源码

registerMicroApps

image.png

该方法主要是把我们要注册的apps进行一下过滤,将已经注册的过滤掉,然后执行registerApplication;这个registerApplication是从single-spa导出来的,这就是一个注册微应用的方法,我们重点关注一下这个app属性,它是具体指明如何加载微应用的钩子

接下来我们的关注重点到了app这个方法,我们看到最关键的是loadApp方法,进去看一看

loadApp

在这个方法里面,我们看到了加载html入口的方法:importEntry 屏幕快照 2022-06-10 上午8.14.10.png

createSandboxContainer:创建js沙盒 屏幕快照 2022-06-10 上午8.21.17.png

execScripts:执行js脚本 屏幕快照 2022-06-10 上午8.23.16.png

下面我们依次进入到函数内部一探究竟

import-html-entry

屏幕快照 2022-06-11 上午9.16.11.png

文件中依次声明了getEmbedHTML、getExecutableScript、_getExternalStyleSheets、_getExternalScripts、_execScripts、importHTML、importEntry,qiankun用到了importEntry,所以我们从importEntry函数开始

屏幕快照 2022-06-11 上午9.22.11.png 如果我们是这样注册微应用的,就是html entry,那么执行importHTML;如果entry是一个对象那么处理逻辑跟下面的类似就不再重复说明:

registerMicroApps([
  {
    name:'sub-vue',
    entry:'//localhost:8080',
    container:"#app1",
    activeRule:'/vue',
  }
])
复制代码

接下来看一看importHTML:

屏幕快照 2022-06-11 上午9.25.56.png

importHTML其实就是通过fetch请求获取到html内容,然后对html字符串进行解析,注意到一个关键函数processTpl,从processTpl中获取到了模板、脚本、样式、入口,也就是说processTpl对html字符串进行解析将这几个部分分离出来了

屏幕快照 2022-06-11 上午9.31.12.png

我们不用去看这个函数的具体内容,看到这一些正则就知道了,它是通过正则匹配获取到样式、js脚本,之后我们回到importHTML

屏幕快照 2022-06-11 上午9.35.05.png 样式分为两种,一种是内部样式表,直接将style标签内的内容返回;一种是外部样式表,这种就需要fetch请求获得样式文件内容然后以文本返回;脚本与样式类似处理,这里就不再赘述

最后就是执行脚本,我们这里先不看执行脚本,因为在执行脚本之前需要先创建js沙盒然后执行脚本

createSandboxContainer

js沙箱需要实现功能:切换微应用之后沙箱需要还原window环境、主应用和微应用之间的全局环境相互隔离

为了实现第一个功能,qiankun设置了三种沙箱:LegacySandbox、ProxySandbox、SnapshotSandbox 支持Proxy则使用LegacySandbox、ProxySandbox,否则使用SnapshotSandbox

屏幕快照 2022-06-11 上午10.07.30.png

LegacySandbox单例沙箱

创建了一个window对象的代理:proxy对象,当触发设置值的操作时判断是否新增属性,新增属性添加到addedPropsMapInSandbox,修改属性值时将原始值添加到modifiedPropsOriginalValueMapInSandbox,同时设置已更新的属性currentUpdatedPropsValueMap,defineProperty时也同样会执行这个判断;这三个对象是激活和卸载的核心; 屏幕快照 2022-06-11 上午11.06.54.png

当激活该沙盒时遍历currentUpdatedPropsValueMap然后修改到globalContext上

当卸载沙盒时只需要将addedPropsMapInSandbox中的值还原为undefined,然后把modifiedPropsOriginalValueMapInSandbox中的值设置到当前globalContext上就可以了

最后把这个proxy对象挂载到主应用的window上window.proxy=proxy

ProxySandbox多例沙箱

ProxySandbox和LegacySandbox的区别在于代理的对象不同,ProxySandbox中代理的fakeWindow是window对象的一个拷贝,而LegacySandbox中的fakeWindow是一个空对象,设置和取值还是在rawWindow上操作;

下面就是fakeWindow的创建操作: 屏幕快照 2022-06-11 下午12.34.56.png 为什么不能操作rawWindow呢,就是为了防止window污染,主要是为了维护多个微应用之间的沙盒环境

SnapshotSandbox

屏幕快照 2022-06-11 上午11.28.20.png 为了兼容低版本浏览器用diff实现的沙箱

激活时存储当前window环境windowSnapshot,将变更应用到window上,第一次激活时这个变更是一个空对象

当卸载时会遍历window上的属性看是否和激活之前的window对象也就是windowSnapshot相同,如果不同则记录变更并恢复环境,这个变更将会在下一次激活时使用

这三个沙盒都创建了一个proxy对象,那么这个proxy对象是如何绑定到微应用的全局环境的呢?这就要看一下_execScripts函数

_execScripts

执行脚本阶段将微应用的window挂载到主应用的window.proxy上面,然后通过eval执行

屏幕快照 2022-06-11 上午9.44.38.png 这里要搞清楚一点,就是用了with和不用with的区别,如果没有使用with,那么使用var声明变量会是一个局部变量,并且不会赋值到微应用的全局环境也就是window.proxy上面,仅仅是无法影响到主应用的全局变量;而使用了with则所有操作都是在window.proxy上面;

总结

最后总结一下大体的逻辑:

  1. 注册微应用时通过fetch请求HTML entry,然后正则匹配得到内部样式表、外部样式表、内部脚本、外部脚本
  2. 通过fetch获取外部样式表、外部脚本然后与内部样式表、内部脚本按照原来的顺序组合组合之前为样式添加属性选择器(data-微应用名称);将组合好的样式通过style标签添加到head中
  3. 创建js沙盒:不支持Proxy的用SnapshotSandbox(通过遍历window对象进行diff操作来激活和还原全局环境),支持Proxy且只需要单例的用LegcySandbox(通过代理来明确哪些对象被修改和新增以便于卸载时还原环境),支持Proxy且需要同时存在多个微应用的用ProxySandbox(创建了一个window的拷贝对象,对这个拷贝对象进行代理,所有的修改都不会在rawWindow上进行而是在这个拷贝对象上),最后将这个proxy对象挂到window上面
  4. 执行脚本:将上下文环境绑定到proxy对象上,然后eval执行

猜你喜欢

转载自juejin.im/post/7107841837675020302