我们在single-spa 技术分析 基本实现了一个微前端框架需要具备的各种功能,但是又实现的不够彻底,遗留了很多问题需要解决。虽然官方提供了很多样例和最佳实践,但是总显得过于单薄,总给人一种“问题解决了,但是又没有完全解决”的感觉。

qiankun 在 single-spa 的基础上做了二次开发,完善了很多功能,算是一个比较完备的微前端框架了。今天我们来聊一聊 qiankun 的技术原理。

在本系列的开头,我们提到微前端的核心问题其实就是解决如何加载子应用以及如果做好子应用间的隔离问题。所以,我们从这两点来看 qiankun 的实现。

如何加载子应用

single-spa 通过 js entry 的形式来加载子应用。而 qiankun 采用了 html entry 的形式。这两种方式的优缺点我们在理解微前端技术原理中已经做过分析,这里不再赘述,我们看看 qiankun 是如何实现 html entry 的。

qiankun 提供了一个 API registerMicroApps 来注册子应用,其内部调用 single-spa 提供的 registerApplication 方法。在调用 registerApplication 之前,会调用内部的 loadApp 方法来加载子应用的资源,初始化子应用的配置。

通过阅读 loadApp 的代码,我们发现,qiankun 通过 import-html-entry 这个包来加载子应用。import-html-entry 的作用就是通过解析子应用的入口 html 文件,来获取子应用的 html 模板、css 样式和入口 JS 导出的生命周期函数。

import-html-entry

import-html-entry 是这样工作的,假设我们有如下 html entry 文件:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>test</title>
</head>
<body> <!-- mark the entry script with entry attribute -->
<script src="https://unpkg.com/mobx@5.0.3/lib/mobx.umd.js" entry></script>
<script src="https://unpkg.com/react@16.4.2/umd/react.production.min.js"></script>
</body>
</html>

我们使用 import-html-entry 来解析这个 html 文件:

import importHTML from 'import-html-entry';

importHTML('./subApp/index.html')
.then(res => {
console.log(res.template); res.execScripts().then(exports => {
const mobx = exports;
const { observable } = mobx;
observable({
name: 'kuitos'
})
})
});

importHTML 的返回值有如下几个属性:

  • template 处理后的 HTML 模板
  • assetPublicPath 静态资源的公共路径
  • getExternalScripts 获取所有外部脚本的函数,返回脚本路径
  • getExternalStyleSheets 获取所有外部样式的函数,返回样式文件的路径
  • execScripts 执行脚本的函数

importHTML 的返回值中,除了几个工具类的方法,最重要的就是 templateexecScripts 了。

importHTML('./subApp/index.html') 的整个执行过程代码比较长,我们只讲一下大概的执行原理,感兴趣的同学可以自行查看importHTML 的源码

importHTML 首先会通过 fetch 函数请求具体的 html 内容,然后在 processTpl 函数 中通过一系列复杂的正则匹配,解析出 html 中的样式文件和 js 文件。

importHTML 函数返回值为 { template, scripts, entry, styles },分别是 html 模板,html 中的 js 文件(包含内嵌的代码和通过链接加载的代码),子应用的入口文件,html 中的样式文件(同样是包含内嵌的代码和通过链接加载的代码)。

之后通过 getEmbedHTML 函数 将所有使用外部链接加载的样式全部转化成内嵌到 html 中的样式。getEmbedHTML 返回的 html 就是 importHTML 函数最终返回的 template 内容。

现在,我们看看 execScripts 是怎么实现的。

execScripts 内部会调用 getExternalScripts 加载所有 js 代码的文本内容,然后通过 eval("code") 的形式执行加载的代码。

注意,execScripts 的函数签名是这样的 (sandbox?: object, strictGlobal?: boolean, execScriptsHooks?: ExecScriptsHooks): Promise<unknown>。允许我们传入一个沙箱对象,如果子应用按照微前端的规范打包,那么会在全局对象上设置 mountunmount 这几个生命周期函数属性。execScripts 在执行 eval("code") 的时候,会巧妙的把我们指定的沙箱最为全局对象包装到 "code" 中,子应用能够运行在沙盒环境中。

在执行完 eval("code") 以后,就可以从沙盒对象上获取子应用导出的生命周期函数了。

loadApp

现在我们把视线拉回 loadApp 中,loadApp 在获取到 templateexecScripts 这些信息以后,会基于 template 生成 render 函数用于渲染子应用的页面。之后会根据需要生成沙盒,并将沙盒对象传给 execScripts 来获取子应用导出的声明周期函数。

之后,在子应用生命周期函数的基础上,构建新的生命周期函数,再调用 single-spa 的 API 启动子应用。

在这些新的生命周期函数中,会在不同时机负责启动沙盒、渲染子应用、清理沙盒等事务。

隔离

在完成子应用的加载以后,作为一个微前端框架,要解决好子应用的隔离问题,主要要解决 JS 隔离和样式隔离这两方面的问题。

JS 隔离

qiankun 为根据浏览器的能力创建两种沙箱,在老旧浏览器中会创建快照模式 的浏览器中创建 VM 模式的沙箱 ProxySandbox

篇幅限制,我们只看 ProxySandbox 的实现,在其构造函数中,我们可以看到具体的逻辑:首先会根据用户指定的全局对象(默认是 window)创建一个 fakeWindow,之后在这个 fakeWindow 上创建一个 proxy 对象,在子应用中,这个 proxy 对象就是全局变量 window

constructor(name: string, globalContext = window) {
const { fakeWindow, propertiesWithGetter } = createFakeWindow(globalContext);
const proxy = new Proxy(fakeWindow, {
set: (target: FakeWindow, p: PropertyKey, value: any): boolean => {},
get: (target: FakeWindow, p: PropertyKey): any => {},
has(target: FakeWindow, p: string | number | symbol): boolean {}, getOwnPropertyDescriptor(target: FakeWindow, p: string | number | symbol): PropertyDescriptor | undefined {}, ownKeys(target: FakeWindow): ArrayLike<string | symbol> {}, defineProperty(target: Window, p: PropertyKey, attributes: PropertyDescriptor): boolean {}, deleteProperty: (target: FakeWindow, p: string | number | symbol): boolean => {}, getPrototypeOf() {
return Reflect.getPrototypeOf(globalContext);
},
});
this.proxy = proxy;
}

其实 qiankun 中的沙箱分两个类型:

  • app 环境沙箱

    app 环境沙箱是指应用初始化过之后,应用会在什么样的上下文环境运行。每个应用的环境沙箱只会初始化一次,因为子应用只会触发一次 bootstrap 。子应用在切换时,实际上切换的是 app 环境沙箱。
  • render 沙箱

    子应用在 app mount 开始前生成好的的沙箱。每次子应用切换过后,render 沙箱都会重现初始化。

上面说的 ProxySandbox 其实是 render 沙箱。至于 app 环境沙箱,qiankun 目前只针对在应用 bootstrap 时动态创建样式链接、脚本链接等副作用打了补丁,保证子应用切换时这些副作用互不干扰。

之所以设计两层沙箱,是为了保证每个子应用切换回来之后,还能运行在应用 bootstrap 之后的环境下。

样式隔离

qiankun 提供了多种样式隔离方式,隔离效果最好的是 shadow dom,但是由于其存在诸多限制,qiankun 官方在将来的版本中将会弃用,转而推行 experimentalStyleIsolation 方案。

我们可以通过下面这段代码看到 experimentalStyleIsolation 方案的基本原理。

const styleNodes = appElement.querySelectorAll('style') || [];
forEach(styleNodes, (stylesheetElement: HTMLStyleElement) => {
css.process(appElement!, stylesheetElement, appInstanceId);
});

css.process 的核心逻辑,就是给读取到的子应用的样式添加带有子应用信息的前缀。效果如下:

/* 假设应用名是 react16 */
.app-main {
font-size: 14px;
} div[data-qiankun-react16] .app-main {
font-size: 14px;
}

通过上面的隔离方法,基本可以保证子应用间的样式互不影响。

小结

qiankun 在 single-spa 的基础上根据实际的生产实践开发了很多有用的功能,大大降低了微前端的使用成本。

本文仅仅针对如何加载子应用和如何做好子应用间的隔离这两个问题,介绍了 qiankun 的实现。其实,在隔离这个问题上,qiankun 也仅仅是根据实际中会遇到的情况做了必要的隔离措施,并没有像 iframe 那样实现完全的隔离。我们可以说 qiankun 实现的隔离有缺陷,也可以说是 qiankun 在实际的业务需求和完全隔离的实现成本之间做的取舍。

常见面试知识点、技术方案分析、教程,都可以扫码关注公众号“众里千寻”获取,或者来这里 https://everfind.github.io/posts/

微前端框架 qiankun 技术分析的更多相关文章

  1. 微前端框架 single-spa 技术分析

    在理解微前端技术原理中我们介绍了微前端的概念和核心技术原理.本篇我们结合目前业内主流的微前端实现 single-spa 来说明在生产实践中是如何实现微前端的. single-spa 的文档略显凌乱,概 ...

  2. 微前端框架 之 qiankun 从入门到源码分析

    封面 简介 从 single-spa 的缺陷讲起 -> qiankun 是如何从框架层面解决 single-spa 存在的问题 -> qiankun 源码解读,带你全方位刨析 qianku ...

  3. 极致简洁的微前端框架-京东MicroApp开源了

    前言 MicroApp是一款基于类WebComponent进行渲染的微前端框架,不同于目前流行的开源框架,它从组件化的思维实现微前端,旨在降低上手难度.提升工作效率.它是目前市面上接入微前端成本最低的 ...

  4. 微前端(qiankun)主应用共享React组件

    前言 最近需要重构一个老项目,定的方案用微前端去改造.主应用是老的项目,微应用是新的项目,由于重构时间比较紧张,子应用还需要使用父应用的一些组件.过程中遇到一些问题,记录一下. 方案 我们知道qian ...

  5. 基于 iframe 的微前端框架 —— 擎天

    vivo 互联网前端团队- Jiang Zuohan 一.背景 VAPD是一款专为团队协作办公场景设计的项目管理工具,实践敏捷开发与持续交付,以「项目」为核心,融合需求.任务.缺陷等应用,使用敏捷迭代 ...

  6. 微前端框架single-spa初探

    前言 最近入职的一家公司采用single-spa这个微前端框架,所以自学了此框架. single-spa这个微前端框架虽然有中文文档,但是有些零散和晦涩. 所以我想在学习之余,写篇博客拉平一下这个学习 ...

  7. 【微前端】微前端最终章-qiankun指南以及微前端整体探索

    序 这才2月中旬,广州就已经渐渐地进入了夏季,--夏天总是让人焦虑的.过年闲暇时间写下了微前端这系列的终章,欢迎拍砖.如果你习惯直接上手代码,不妨跳到实践一节,直接上代码教程玩一玩. qiankun原 ...

  8. 微前端框架 single-spa

    单体应用对比前端微服务化 普通的前端单体应用 微前端架构 1.基本概念 实现一套微前端架构,可以把其分成四部分(参考:https://alili.tech/archive/11052bf4/) 加载器 ...

  9. 基于微前端qiankun的多页签缓存方案实践

    作者:vivo 互联网前端团队- Tang Xiao 本文梳理了基于阿里开源微前端框架qiankun,实现多页签及子应用缓存的方案,同时还类比了多个不同方案之间的区别及优劣势,为使用微前端进行多页签开 ...

随机推荐

  1. 第02章_MySQL环境搭建

    第02章_MySQL环境搭建 1. MySQL的卸载 步骤1:停止MySQL服务 在卸载之前,先停止MySQL8.0的服务.按键盘上的"Ctrl + Alt + Delete"组合 ...

  2. html+css第一篇

    行间样式表 <div style="--"></div> 内部样式表 <style>----</style> 外部样式表 <l ...

  3. go定义接口以及类怎么使用接口

    go定义接口以及类怎么使用接口 多态是指代码可以根据类型的具体实现采取不同行为的能力.如果一个类型实现了某个接口,所有使用这个接口的地方,都可以支持这种类型的值. 接口是用来定义行为的类型.这些被定义 ...

  4. 24-Longest Palindromic Substring-Leetcode

    Given a string S, find the longest palindromic substring in S. You may assume that the maximum lengt ...

  5. Redis篇:单线程I/O模型

    关注公众号,一起交流,微信搜一搜: 潜行前行 redis 单线程 I/O 多路复用模型 纯内存访问,所有数据都在内存中,所有的运算都是内存级别的运算,内存响应时间的时间为纳秒级别.因此 redis 进 ...

  6. 用JS实现方块碰撞

    首先我们应用上次的内容--方块拖拽,利用方块拖拽来让两个方块进行碰撞. 我们可以先定义两个正方形方块,红色的div1,绿色的div2,我们来实现当div1碰撞div2时div2的颜色变为黄色 HTML ...

  7. 解决CentOS7 docker容器映射端口只监听ipv6的问题

    问题现象 docker容器起来以后,查看9100端口监听情况,如下图: $ ss -lntp State Recv-Q Send-Q Local Address:Port Peer Address:P ...

  8. ZooKeeper 04 - ZooKeeper 集群的节点为什么必须是奇数个

    目录 1 - 关于节点个数的说明 2 - ZooKeeper 集群的容错数 3 - ZooKeeper 集群可用的标准 4 - 为什么不能是偶数个节点 4.1 防止由脑裂造成的集群不可用 4.2 奇数 ...

  9. 用原生CSS编写动态字体

    HTML部分: <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <t ...

  10. LeetCode1579题——圆圈中最后剩下的数字

    1.题目描述:0,1,,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字.求出这个圆圈里剩下的最后一个数字.例如,0.1.2.3.4这5个数字组成一个圆圈,从数字0开始每次删 ...