微前端框架设计目的及实现原理介绍

373 阅读4分钟

前言

在日常开发项目中,随着时间迭代,经常遇到当初认为很好很先进的技术其实并不那么理想。这时,业界出现了更好的替代品,作为项目负责人,你希望更新,但项目已经10万行代码了,没人知道更换技术栈会导致什么问题。那换种思路,可不可以新增代码用新技术栈,老代码不动或慢慢重构?

Single-spa 应运而生

应用被分为主应用和微应用,主应用作为整个应用入口,劫持路由,通过路由的不同加载不同的微应用。

微应用是正常js代码,但必须导出三个关键生命周期bootstrap, mount, unmount来处理微应用的初始化,加载和卸载

let domEl;
export function bootstrap(props) {
  // 初始化
}
export function mount(props) {
  // 这里渲染内容
  domEl = document.getElementById('id')
  reactDom.render(domEl, ...)
}
export function unmount(props) {
  // 这里卸载dom内容 
}

举例来说

目前有微应用AppAngular,使用vue编写。现在新增模块想使用react,创建新的微应用AppReact

在主应用注册路由,注册微应用

/angular -> AppAngular

/react -> AppReact

当url为/angular*时,加载AppAngular,mount方法中指定模块渲染到某个实际的dom元素(#root)中

当url变更为/react*时,AppAngular的unmount执行,卸载应用,清除dom,style,事件等。然后执行AppReact的mount方法

上述流程看起来没问题,一个普通应用改造成微应用也很简单,但实际会存在全局变量污染,样式污染等情况

AppAngular中声明并定义了一个window.test 变量,AppReact也用了它,此时就产生了冲突。那么有没有可能在微应用上加一层沙盒,隔离应用中的全局变量?

QianKun就是解决此问题的框架,它甚至是直接基于single-spa二次封装的

沙盒的原理是利用fetch请求js源码,然后复制当前window对象,利用eval执行源码并将复制的window对象注入进去

QianKun沙盒源码片段

function getExecutableScript(scriptSrc, scriptText, proxy, strictGlobal) {
  const sourceUrl = isInlineCode(scriptSrc) ? '' : `//# sourceURL=${scriptSrc}\n`;
  
  // 通过这种方式获取全局 window,因为 script 也是在全局作用域下运行的,所以我们通过 window.proxy 绑定时也必须确保绑定到全局 window 上
  // 否则在嵌套场景下, window.proxy 设置的是内层应用的 window,而代码其实是在全局作用域运行的,会导致闭包里的 window.proxy 取的是最外层的微应用的 proxy
  const globalWindow = (0, eval)('window');
  globalWindow.proxy = proxy;
  // TODO 通过 strictGlobal 方式切换 with 闭包,待 with 方式坑趟平后再合并
  return strictGlobal
    ? `;(function(window, self, globalThis){
        with(window){
          ;${scriptText}\n${sourceUrl}
        }
      })
      .bind(window.proxy)
      (window.proxy, window.proxy, window.proxy);`
    :	`;(function(window, self, globalThis){
        ;${scriptText}\n${sourceUrl}
      })
      .bind(window.proxy)
      (window.proxy, window.proxy, window.proxy);`;
}

(0, eval)('window')这句是为了取到真实的window对象,这么做是eval的一个特殊优化,目的是间接调用eval,此时被当作代码执行的字符串中,所有变量的作用域都在顶层作用域下

或是你把eval赋值给一个新变量再执行,也可以触发间接调用


proxy 是 qiankun 创建的 window 拷贝对象的 proxy 对象
函数最终返回一段由微应用源码拼成的字符串,同样在外部使用eval执行
源码执行的函数体内因为this, window, self, globalThis变量都指向了proxy,所以形成了沙盒(注释中关于对with的解释,我没理解他想表达的意思,但不妨碍我们解读源码)
注意:这里也是 qiankun 不支持 vite 的原因,因为 vite 加载的代码中包含import, export,eval无法直接执行。社区有插件解决

以上是js层面的隔离,css的隔离通过对css代码自动添加命名空间实现。QianKun还尝试过用shadow DOM隔离样式,但因为种种原因已经将这个方案列为不推荐(具体可以issue搜shadow DOM)

QianKun 的接入成本已经很低了,但还能不能更低一点?

MicroApp

<micro-app name='app1' url='http://localhost:3000/' baseroute='/my-page'></micro-app>

使用时只需要在对应组件里像iframe 一样引用,微应用的代码也几乎不用改变

micro-app组件是利用Web Components技术封装的,也就是说可以和react等框架无缝衔接,生命周期的管理也可以从微应用内部改为接入方,主应用来管理

沙盒方面和QianKun相似,作者贴心的将沙盒实现写了出来,我这里就不做复述

MicroApp的沙盒同样不支持vite,需要关闭沙盒才能使用(对于vite的支持作者自称已经在开发中了)

为什么这么关注vite

之前在项目中对webpack做了大量优化,这些优化最终都没vite快,所以vite的支持在我这里是优先级很高的。但是现在主流的框架对vite支持都很差

Qiankun 不支持,社区有插件可以支持vite,但会关闭沙盒

Garfish 不支持 ,字节出品的框架,用vite无法启动

MicroApp 半支持,必须关闭沙盒

无界 支持,写这篇文章的时候才知道的框架,腾讯出的,但是这个框架太新了,还需要时间验证。后面有时间单独写一篇使用体验

总结

国产这些框架非常优秀,webpack 项目放心用,vite项目再等等。使用微前端的理由有很多种,不只是文中列出来的,完全看你业务需求,比如维护人员多,代码臃肿想拆分,完全可以用微前端搞定。微应用拆多细看业务需求,但不推荐太细,尽可能粗一点,太细了还会出现更多的代码管理问题

我正在写一个针对微前端的cli,意图降低微前端接入成本,和代码管理复杂度。还在初期阶段,感兴趣可以共建 github.com/sukura-shri…