将微前端做到极致-无界(目前微前端最好的解决方案)

1,956 阅读7分钟

前言

微前端已经是一个非常成熟的领域了,但开发者不管采用哪个现有方案,在适配成本、样式隔离、运行性能、页面白屏、子应用通信、子应用保活、多应用激活、vite 框架支持、应用共享等用户核心诉求都或存在问题,或无法提供支持。本文提供一种基于 iframe 的全新微前端方案,完善的解决了这些核心诉求。

各个微前端的优势与劣势分析

qiankun

qiankun 方案是基于 single-spa 的微前端方案。

特点

1.  html entry 的方式引入子应用,相比 js entry 极大的降低了应用改造的成本;
2.  完备的沙箱方案,js 沙箱做了 SnapshotSandboxLegacySandboxProxySandbox 三套渐进增强方案,css 沙箱做了 strictStyleIsolation、experimentalStyleIsolation 两套适用不同场景的方案;
3.  做了静态资源预加载能力;

缺点

1.  适配成本比较高,工程化、生命周期、静态资源路径、路由等都要做一系列的适配工作;
2.  css 沙箱采用严格隔离会有各种问题,js 沙箱在某些场景下执行性能下降严重;
3.  无法同时激活多个子应用,也不支持子应用保活;
4.  无法支持 vite 等 esmodule 脚本运行;

micro-app

micro-app 是基于 webcomponent + qiankun sandbox 的微前端方案。

特点

1.  使用 webcomponet 加载子应用相比 single-spa 这种注册监听方案更加优雅;
2.  复用经过大量项目验证过 qiankun 的沙箱机制也使得框架更加可靠;
3.  组件式的 api 更加符合使用习惯,支持子应用保活;
4.  降低子应用改造的成本,提供静态资源预加载能力;

缺点

1.  接入成本较 qiankun 有所降低,但是路由依然存在依赖(虚拟路由已解决);
2.  多应用激活后无法保持各子应用的路由状态,刷新后全部丢失(虚拟路由已解决);
3.  css 沙箱依然无法绝对的隔离,js 沙箱做全局变量查找缓存,性能有所优化;
4.  支持 vite 运行,但必须使用 plugin 改造子应用,且 js 代码没办法做沙箱隔离;
5.  对于不支持 webcompnent 的浏览器没有做降级处理;

EMP

EMP 方案是基于 webpack 5 module federation 的微前端方案。

特点

1.  webpack 联邦编译可以保证所有子应用依赖解耦;
2.  应用间去中心化的调用、共享模块;
3.  模块远程 ts 支持;

缺点

1.  对 webpack 强依赖,老旧项目不友好;
2.  没有有效的 css 沙箱和 js 沙箱,需要靠用户自觉;
3.  子应用保活、多应用激活无法实现;
4.  主、子应用的路由可能发生冲突

结论

qiankun 方案对 single-spa 微前端方案做了较大的提升同时也遗留下来了不少问题长时间没有解决; micro-app 方案对 qiankun 方案做了较多提升但基于 qiankun 的沙箱也相应会继承其存在的问题; EMP 方案基于 webpack 5 联邦编译则约束了其使用范围; 目前的微前端方案在用户的核心诉求上都没有很好的满足,有很大的优化提升空间。

无界

无界微前端方案基于 webcomponent 容器 + iframe 沙箱,能够完善的解决适配成本、样式隔离、运行性能、页面白屏、子应用通信、子应用保活、多应用激活、vite 框架支持、应用共享等用户的核心诉求
无界微前端的成本非常低,主要体现在主应用的使用成本、子应用的适配成本两个方面。

安装方法

# vue2 框架
npm i wujie-vue2 -S
# vue3 框架
npm i wujie-vue3 -S

引入
// vue2
import WujieVue from "wujie-vue2";
// vue3
import WujieVue from "wujie-vue3";
const { bus, setupApp, preloadApp, destroyApp } = WujieVue;
Vue.use(WujieVue);


# react安装
npm i wujie-react -S

import WujieReact from "wujie-react";
const { bus, setupApp, preloadApp, destroyApp } = WujieReact;

属性

type lifecycle = (appWindow: Window) => any;
type loadErrorHandler = (url: string, e: Error) => any;

type preOptions  {
  /** 唯一性用户必须保证 */
  name: string;
  /** 需要渲染的url */
  url: string;
  /** 需要渲染的html, 如果用户已有则无需从url请求 */
  html?: string;
  /** 注入给子应用的数据 */
  props?: { [key: string]: any };
  /** 自定义运行iframe的属性 */
  attrs?: { [key: string]: any };
  /** 自定义降级渲染iframe的属性 */
  degradeAttrs?: { [key: string]: any };
  /** 代码替换钩子 */
  replace?: (code: string) => string;
  /** 自定义fetch,资源和接口 */
  fetch?: (input: RequestInfo, init?: RequestInit) => Promise<Response>;
  /** 子应用保活模式,state不会丢失 */
  alive?: boolean;
  /** 预执行模式 */
  exec?: boolean;
  /** js采用fiber模式执行 */
  fiber?: boolean;
  /** 子应用采用降级iframe方案 */
  degrade?: boolean;
  /** 子应插件 */
  plugins: Array<plugin>;
  /** 子应用生命周期 */
  beforeLoad?: lifecycle;
  /** 没有做生命周期改造的子应用不会调用 */
  beforeMount?: lifecycle;
  afterMount?: lifecycle;
  beforeUnmount?: lifecycle;
  afterUnmount?: lifecycle;
  /** 非保活应用不会调用 */
  activated?: lifecycle;
  deactivated?: lifecycle;
  /** 子应用资源加载失败后调用 */
  loadError?: loadErrorHandler
};

主应用成本低

主应用使用无界不需要学习额外的知识,无界提供基于 vue 封装的和基于 react 封装的 [wujie-react]用户可以当初普通组件一样加载子应用,以 wujie-vue 举例:
<WujieVue
  width="100%"
  height="100%"
  name="xxx"
  url="xxx"
  :sync="true"
  :fiber="true"
  :degrade="false"
  :fetch="fetch"
  :props="props"
  :plugins="plugins"
  :beforeLoad="beforeLoad"
  :beforeMount="beforeMount"
  :afterMount="afterMount"
  :beforeUnmount="beforeUnmount"
  :afterUnmount="afterUnmount"
></WujieVue>
子应用加载和普通 vue 组件加载并无二致,所有配置都收敛到组件的属性上。

子应用适配成本

子应用首先需要做支持跨域请求改造,这个是所有微前端框架运行的前提,除此之外子应用可以不做任何改造就可以在无界框架中运行,不过此时运行的方式是重建模式。
简单理解 我只需要做好跨域处理,其他啥都不用做

速度快

无界微前端非常快,主要体现在首屏打开快、运行速度快两个方面。

首屏加载快

目前大部分微前端只能做到静态资源预加载,但是就算子应用所有资源都预加载完毕,等到子应用打开时页面仍然有不短的白屏时间,这部分白屏时间主要是子应用 js 的解析和执行。

无界微前端不仅能够做到静态资源的预加载,还可以做到子应用的预执行。

预执行会阻塞主应用的执行线程,所以无界提供 [fiber 执行模式]采取类似 react fiber 的方式间断执行 js,每个 js 文件的执行都包裹在 requestidlecallback 中,每执行一个 js 可以返回响应外部的输入,但是这个颗粒度是 js 文件,如果子应用单个 js 文件过大,可以通过拆包的方式降低体积达到 fiber 执行模式效益最大化。

https://wujie-micro.github.io/demo-main-vue/home

运行速度快和原声隔离js

子应用的 js 在 iframe 内运行,由于 iframe 是一个天然的 js 运行沙箱,所以无需采用 with ( fakewindow ) 这种方式来指定子应用的执行上下文,从而避免由于采用 with 语句执行子应用代码而导致的性能下降,整体的运行性能和原生性能差别不大。

组件通信

## props 通信

<WujieVue name="xxx" url="xxx" :props="{ data: xxx, methods: xxx }"></WujieVue>

子应用获取数据
const props = window.$wujie?.props; // {data: xxx, methods: xxx}

## eventBus 通信

## 主应用使用
// 如果使用wujie
import { bus } from "wujie";

// 如果使用wujie-vue
import WujieVue from "wujie-vue";
const { bus } = WujieVue;

// 如果使用wujie-react
import WujieReact from "wujie-react";
const { bus } = WujieReact;

// 主应用监听事件
bus.$on("事件名字", function (arg1, arg2, ...) {});
// 主应用发送事件
bus.$emit("事件名字", arg1, arg2, ...);
// 主应用取消事件监听
bus.$off("事件名字", function (arg1, arg2, ...) {});


## 子应用使用
// 子应用监听事件
window.$wujie?.bus.$on("事件名字", function (arg1, arg2, ...) {});
// 子应用发送事件
window.$wujie?.bus.$emit("事件名字", arg1, arg2, ...);
// 子应用取消事件监听
window.$wujie?.bus.$off("事件名字", function (arg1, arg2, ...) {});

scss介绍

SCSS (Sassy CSS),它是一款css预处理语言,是 Sass 3 引入新的语法,其语法完全兼容 CSS3,并且继承了 Sass 的强大功能

并且Sass可以帮助我们减少css重复的代码,减少开发时间。那么Sass有什么强大并且高级的功能呢?例如:变量、嵌套、导入import、混合mixin、继承extend

’&‘关键字,引用父级选择器

.container{
  &:hover{
    background: #b084eb;
  }
}

转化为css
.container:hover{
background: #b084eb;
}

变量是使用’$‘符号开头的,用来存储想要复用的信息,例如颜色、字符串、数值等等

$ 变量名: 变量值
$btn-1: #A4C82B;
$btn-2: #499DC8;
$imgPath: '../assets';

.btn{
    background:$btn-1
}

字符串下使用
.con{
    background: url('#{$imgPath}/logo.png'); 
}

嵌套(选择器使用的嵌套规则,要注意的是最好不要过度的嵌套 这样会比较难维护)

nav {
  ul {
    margin: 0;
    padding: 0;
    list-style: none;
  }
  li {
    display: inline-block;
  }
}

很多 CSS 属性都有同样的前缀,例如:font-family, font-size 和 font-weight,这样我们就可以使用嵌套属性来写!

.btn{
    font: {
        size: 16px;
        weight: normal;
        family: Hei;
    };
}

混入(通过@mixin 指令来定义,下面创建一个名为flex-con和div-size的混入)

// 无参数型
@mixin flex-con{
  display: flex;
  flex-direction: column;
  align-items: center;
}
 
// 有参数型
@mixin div-size($width,$height){
  width: $width;
  height: $height;
}
.container{
  @include flex-con;
  @include div-size(200px,200px);
}

引入

通过@import引入文件,可以根据颜色相关文件、字体相关文件进行拆分,这样会更结构化哦!

另外,CSS @import 指令在每次调用时,都会创建一个额外的 HTTP 请求。但是Sass @import 指令将文件包含在 CSS 中,不需要额外的 HTTP 请求
// common.scss文件
$btn-1: #A4C82B;
$btn-2: #499DC8;
$btn-3: #933CC8;
@import 'common' // 根据自己的文件路径进行引入
 
// 引入后使用
.btn{
    background:$btn-1
}

继承

@[extend](https://so.csdn.net/so/search?q=extend&spm=1001.2101.3001.7020) 指令告诉 Sass 一个选择器的样式从另一选择器继承。如果一个样式与另外一个样式几乎相同,只有少量的区别,则使用 @extend 就比较有用
.btn-1{
  display: inline-block;
  padding: 3px;
}
.btn-2{
  background: #b084eb;
  color: #ffffff;
}
.btn-3{
  @extend .btn-1,.btn-2
}
转为css

.btn-1,.btn-3{
  display: inline-block;
  padding: 3px;
}
.btn-2,.btn-3{
  background: #b084eb;
  color: #ffffff;
}

vue中使用

<template>
  <div :style="cssVars">
    <p class="text">测试文本</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      color: "red"
    };
  },
  computed: {
    cssVars() {
      return {
        "--color": this.color
      };
    }
  }
};
</script>

<style lang="scss" scoped>
.text {
  color: var(--color);
}
</style>
<template>
  <div>
    <p class="text">测试文本</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      color: "red"
    };
  }
};
</script>

<style scoped vars="{ color }">
.text {
  color: var(--color);
}
</style>