真的有必要用微前端框架么?

lxf2023-03-10 19:23:01

前言

最近公司项目在用qiankun构建微前端的应用,深深体会到微前端的魅力,无框架限制,主应用统一管理,弹窗的统一位置等。如果是刚开始就植入微前端还好,不过基本上都是后期老项目植入微前端,各种拆分模块,也是一件很头疼的事情。

基石

我们为什么要用微前端

大的应用体量维护成本是很高的,拆分成单独的模块,由主应用处理登录等通用逻辑,子应用来只负责模块的业务实现,这样不管资源加载、按需加载、人员维护成本降低、增量升级、独立部署都有很好的体检提升。当然前提是体量非常大的web应用可以这么做,但是开始做的时候你会很头疼各种拆解带来的不确定性,但是长痛不如短痛。

Why Not Iframe

下面是我从qiankun文档摘抄的:

iframe 最大的特性就是提供了浏览器原生的硬隔离方案,不论是样式隔离、js 隔离这类问题统统都能被完美解决。那么为什么不用iframe呢 ?

  1. url 不同步。浏览器刷新 iframe url 状态丢失等(本地缓存不就行了?)
  2. UI 不同步,DOM 结构不共享(主应用控制不就行了?子应用通过postMessage传递数据给父应用)
  3. 一次性加载,慢!(个人感觉就是项目体积小了!跟iframe有啥区别么?)
  4. 全局上下文完全隔离,内存变量不共享。(当然这里通过postMessage是可以实现通信的!)

真的有必要用微前端框架么?

所以其实用iframe就够了,微前端是不是有点kpi的味道呢?当然学习下源码还是对自己有提升的,万一iframe没有,是不是可以手撸一个呢?

源码入口

真的有必要用微前端框架么?

最核心的就是手动加载loadMicroApp、registerMicroApps注册微应用,start开始构建这3个api,但其实qiankun的核心是基于single-spa框架封装的, 我们看下single-spa做了些什么,以及single-spa内部核心api的registerApplication做了什么

single-spa

single-spa是一个框架,用于将多个JavaScript微前端组合在一个前端应用程序中。使用单一页面中心构建前端可以带来许多好处,例如:

  • 在同一页面上使用多个框架而无需刷新页面(React,AngularJS,Angular,Ember或你正在使用的任何框架)
  • 独立部署您的微前端
  • 使用新框架编写代码,无需重写现有应用
  • 延迟加载代码可缩短初始加载时间

registerApplication 注册应用

export function reroute (pendingPromises = [], eventArguments) {
   //...
  const {
    appsToUnload,
    appsToUnmount,
    appsToLoad,
    appsToMount,
  } = getAppChanges();  //返回不同生命周期的队列
  
  //记录基础的应用信息
  let appsThatChanged,
    navigationIsCanceled = false,
    oldUrl = currentUrl,
    newUrl = (currentUrl = window.location.href);
    
   //这里根据是否已挂载做处理
  if (isStarted()) {
    //....
  } else {
    appsThatChanged = appsToLoad;
    return loadApps();
  }
   //加载apps
  function loadApps () {
    return Promise.resolve().then(() => {
      const loadPromises = appsToLoad.map(toLoadPromise);

      return (
        Promise.all(loadPromises)
          .then(callAllEventListeners)
          // there are no mounted apps, before start() is called, so we always return []
          .then(() => [])
          .catch((err) => {
            callAllEventListeners();
            throw err;
          })
      );
    });
  }
   //根据app状态改变发布对应的事件
  function performAppChanges () {
    return Promise.resolve().then(() => {
      // https://github.com/single-spa/single-spa/issues/545
      window.dispatchEvent(
        new CustomEvent(
          appsThatChanged.length === 0
            ? "single-spa:before-no-app-change"
            : "single-spa:before-app-change",
          getCustomEventDetail(true)
        )
      );  
      //...做了大量的自定义事件以及卸载事件
  }
  //....
}

说实话这里的源码很绕,这里只摘取最关键的,在registerApplication内部,将qiankun的registerMicroApps的参数传入做些兼容判断,然后调用了一个核心的reroute方法, 这里删除了不必要的干扰信息,说白了single-spa做了spa的生命周期的管理,每个应用有单独的html做页面的加载,但是环境的隔绝是需要qiankun做的

getAppChanges 状态管理

export function getAppChanges () {
  const appsToUnload = [],
    appsToUnmount = [],
    appsToLoad = [],
    appsToMount = [];

  // We re-attempt to download applications in LOAD_ERROR after a timeout of 200 milliseconds
  const currentTime = new Date().getTime();

  apps.forEach((app) => {
    const appShouldBeActive =
      app.status !== SKIP_BECAUSE_BROKEN && shouldBeActive(app);

    switch (app.status) {
      case LOAD_ERROR:
        if (appShouldBeActive && currentTime - app.loadErrorTime >= 200) {
          appsToLoad.push(app);
        }
        break;
      case NOT_LOADED:
      case LOADING_SOURCE_CODE:
        if (appShouldBeActive) {
          appsToLoad.push(app);
        }
        break;
      case NOT_BOOTSTRAPPED:
      case NOT_MOUNTED:
        if (!appShouldBeActive && getAppUnloadInfo(toName(app))) {
          appsToUnload.push(app);
        } else if (appShouldBeActive) {
          appsToMount.push(app);
        }
        break;
      case MOUNTED:
        if (!appShouldBeActive) {
          appsToUnmount.push(app);
        }
        break;
      // all other statuses are ignored
    }
  });

  return { appsToUnload, appsToUnmount, appsToLoad, appsToMount };
}

getAppChange返回了应用一旦改变那么不同生命周期的队列会更新

registerMicroApps注册微应用

简单的看下用法,qiankun的核心api并不多

真的有必要用微前端框架么?

真的有必要用微前端框架么?

registerMicroApps注册微应用中,传入apps的信息,如name、路由匹配规则、container挂载dom等,和生命周期的钩子lifeCycles。核心的应用状态管理在single-spa中,而qiankun在外层又做了统一的上层的主应用封装。这里比较重要的是loadApp,里面统一处理解决了全局变量混入的问题也就是沙箱隔离,样式隔离等。下面是loadApp的一部分核心逻辑

环境隔离

1. 全局变量的隔离

createSandboxContainer 创建沙箱

真的有必要用微前端框架么?

这里核心就是createSandboxContainer的用法,这里简单讲下几个核心的参数,initialAppWarpperGetter用于处理检查是否有无包裹的dom元素,scpredCSS是代表样式是否已经被隔离状态, useLooseSandbox和speedySandbox处理不同状态的沙箱。 ,在里面核心的方法是patchAtBootstrapping做了不同的沙箱隔离方式的隔绝处理

patchAtBootstrapping 启动器

真的有必要用微前端框架么?

patchAtBootstrapping中的策略者模式,我们可以看到有3种沙箱的处理方式,legacyProxy、Proxy、Snapshot,并分别对应了pathLooseSandbox、pathStrictSandbox、patchLooseSandbox,下面简单解析下原理,理解即可。

Snapshot沙箱隔离

先将主应用的window拷贝一份,一旦微应用切换到主应用做回退,如果微应用切换,那么会提前生成微应用的diff过程的对象,然后回退。而缺陷就是diff属性量一旦过大会性能不好

Legacy沙箱隔离

那么与Snapshot最大的不同是用了Proxy来处理,set做记录,一旦应用切换就回退,相对于我不断循环遍历diff,性能好了不少

Proxy沙箱隔离

前面两种应用场景在于都是一个路由对应一个微应用,那么如果是多个微应用同时出现在一个页面中,那么环境是不是不可控了呢。这种情况就不能在window直接操作,而是要每个应用都要有一个独立的fakeWindow,这样区分环境后,数据处理尽量在fakeWindow上处理,而不是原生window

Proxy模式的核心的我们看下pathStrictSandbox源码

pathStrictSandbox 严格模式

真的有必要用微前端框架么?

Proxy代理模式的沙箱,通过Object.defineProperty来拦截对象属性,但是不可枚举可写入, 这样每次切换应用我都重新获取新的nativeGlobal

nativeGlobal 全局对象

export const nativeGlobal = new Function('return this')();

通过new Function来更安全的返回全局对象

2. DOM的隔离

真的有必要用微前端框架么?

真的有必要用微前端框架么?

真的有必要用微前端框架么?

很明显这里是通过ShadowDOM来实现dom的隔离,我们常见的比如video、audio标签内部都是可以看到shadowDOM实现的,同时我们也可以看到做了兼容性的处理

3. 样式隔离

真的有必要用微前端框架么? scopedCSS代表是否要隔离css,如果要隔离首先去判断将微应用的根元素挂载qiankun的属性标记,然后遍历所有style标签,css.process对每个内部的样式属性名做了模块化的处理,而appInstanceId就是做微应用样式隔离的id区分

通信

import-html-entry

qiankun用的是import-html-entry这个库的execSceipts方法来请求获得并解析脚本的,然后直接把html插入到容器里,所以应用间需要允许跨域才行,在importEntry你可以发现他使用了浏览器空闲的api,requestIdleCallback以及为基础实现预加载prefetch

真的有必要用微前端框架么?

真的有必要用微前端框架么?

真的有必要用微前端框架么?

initGlobalState 全局状态

真的有必要用微前端框架么?

真的有必要用微前端框架么?

真的有必要用微前端框架么?

我们主要看下initGlobalState,通过emitGlobal来触发更新全局状态,从上图可以看出核心通过deps发布订阅模式来管理每个微应用,然后更新状态。返回的onGlobalChange和setGlobalState来监听变化和触发通知。状态管理还是比较简单的。

总结

花了几天时间看了源码,收获还是挺大的,微前端其实主要有3个的核心点在于应用通信、应用的生命周期及状态管理、沙箱环境隔离。相对来说iframe足够满足我们业务需求了,微前端提供了一种思路还是不错的,但是真的有必要用qiankun么?