qiankun微前端实践

什么微前端?

这几年微前端还是比较火的概念,旨在将后端微服务的理念应用于浏览器端,
把Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用

微前端的价值

场景一

作为项目管理者,我希望降低单体项目的维护成本(子应用拆分场景/在业务系统聚合的同时降低复杂度)

场景二

作为开发,我希望使用在保持老业务代码不动的情况下使用新的技术(子应用技术栈无关/避免全量升级版本)

场景三

作为开发,我希望在不同的项目技术环境中复用相同的代码模块以提升开发效率(微前端模块)

为什么不用iframe

  1. 子项目需要改造,需要提供一组不带导航的功能
  2. iframe嵌入的显示区大小不容易控制,存在一定局限性
  3. URL的记录完全无效,页面刷新不能够被记忆,刷新会返回首页
  4. iframe功能之间的跳转是无效的
  5. iframe的样式显示、兼容性等都具有局限性

通过这篇文章你将了解到

  1. qiankun是怎样运行的
  2. qiankun沙盒是如何实现的
  3. qiankun是通过什么方式获取到子应用入口文件的钩子函数的

官网文档

qiankun运行机制

single-spa

不多讲

HTML-Entry

import-html-entry

qiankun支持通过html直接加载子应用,需要子应用在应用入口处暴露出相应的钩子函数并通过webpack打包成umd格式,
qiankun会在执行子应用js代码时通过magic获取到子应用的钩子函数并执行。

相比于JS-Entry,好处是更加灵活,不需要在主应用声明很多scripts地址,
也不需要担心external库的更新问题,坏处是把解析消耗放在用户端

sandbox

js沙箱

snapShotSandbox

非proxy沙箱用于兼容不支持proxy的浏览器,原理就是在子应用加载前保存window对象的快照,
子应用销毁之后使用快照对比window,获取到子应用加载期间的增量并保存,等子应用再次被加载时重新挂在window上

legacySandbox

基于proxy实现的沙箱,用于单例模式,也是qiankun初代版本的沙箱,该版本由singular字段控制是否使用legacySandbox,2.2版本之后使用设置loose为true

1
sandbox: { loose: true }
  1. 该沙箱通过一个空对象fakeWindow去劫持全局window,重写空对象的set和get,
  • 1.1 获取fakeWindow某个值触发 get 就从window上取,
  • 1.2 增加window属性触发set就会记录到addedPropsMapInSandbox,
  • 1.3 修改window属性触发set,将key和window[key]记录到modifiedPropsOriginalValueMapInSandbox,第二次修改这个属性不会再做记录
  • 1.4 将变化的数据记录到currentUpdatedPropsValueMap
  • 1.5 映射到window上以便下次 get 时能拿到已更新的数据
  1. 子应用卸载时,将增量的数据从window上删除,将修改过的key对应的原数据还原到window上
  2. 子应用第二次加载时,通过currentUpdatedPropsValueMap恢复

这种沙箱会影响到全局window,因为会将proxy变化映射到window上,并没有做到真正意义上的js隔离,只适用于单例模式

proxySandbox
  1. 新的proxy沙箱在legacySandbox上进行了改进
  • 1.1 先记录window上不可配置的属性(Infinity、NaN、undefined、document、location等),创建一个空的fakeWindow,浅拷贝这些不可枚举属性的值
  • 1.2 增加或者修改window属性时触发set将值记录到fakeWindow,如果这个值之前存在,直接赋值,不存在就通过Object.defineProperty创建一个descriptor
  • 1.3 get操作则会从fakeWindow或者window取

新的proxy不会污染全局window,因为一些不可配置的属性已经被复制到fakeWindow上

css沙箱(css隔离)

experimentalStyleIsolation开启会让子应用的css添加scope

dom沙箱(shadowDom)

开启strictStyleIsolation可使用shadowDom对子应用进行隔离

应用通信

GlobalState

qiankun提供了全局state的功能,子应用可以订阅主应用的全局state,
globalState变化检测方法类似redux管理state树的实现,只会检测第一层数据的变化。

问题是目前只适用于单例模式,只能同时存在一个订阅者

magic

qiankun如何在html内容里找到项目的入口文件呢?

通过import-html-entry解析出所有的script标签 会找出带有 「entry」属性的script标签

1
2
<script src="https://r.xxx.com/xxx/vendor.xxxx.js"></script>
<script src="https://r.xxx.com/xxx/xxx.app.js" entry></script>

以上情况就会知道到xxx.app.js是我们的入口文件,那如果没有设置entry呢?

1
2
<script src="https://r.xxx.com/xxx/vendor.xxxx.js"></script>
<script src="https://r.xxx.com/xxx/xxx.app.js"></script>

如果没有设置entry,就会默认取最后一个script标签作为入口文件,这就牵扯到打包的顺序问题了
chunksSortMode

如何获取到子应用的钩子函数呢?

上面我们获取到了子应用entry,import-html-entry采用了systemjs加载动态文件的方式,
执行入口文件代码前记录window的第一个属性、第二个属性和最后一个属性,执行完再对比这三个属性,如果第一个第二个属性一样但是最后一个属性变化,表示是entry执行的结果,
则会将这个属性对应的值作为子应用入口文件export出来的钩子。

然鹅这样实现有些漏洞,由于副作用或者浏览器兼容性问题
import-html-entry的getGlobalProp实现不合理
global遍历顺序变化,导致子应用entry无法找到导出的生命周期函数
于是乎,qiankun增加了fallback方案,统一子应用注册的name和子应用打包时的libraryName,可在上面方案找不到时进行fallback

踩坑

常见问题汇总

RFC

不算完善的RFC

缺陷

  1. 官网文档不是很明确,demo也不是很清晰
  2. 官网文档跟新速度跟不上release发布

参考

  1. qiankun作者讲微前端
  2. 微前端在小米的实践