微前端技术分享

97 阅读8分钟

微前端应用场景

● 比如制作一个企业管理平台,把已有的采购系统和财务系统统一接入这个平台。

● 比如有一个巨大的应用,为了降低开发和维护成本,分拆成多个小应用进行开发和部署,然后用一个平台将这些小应用集成起来。

● 又比如一个应用使用vue框架开发,其中有一个比较独立的模块,开发者想尝试使用react框架来开发,等模块单独开发部署完,再把这个模块应用接回去。

微前端应具备能力

● 子应用的加载和卸载能力:页面需要从一个子应用切换到另一个子应用,框架必须具备加载、渲染、切换的能力。

● 子应用独立运行的能力:子应用运行会污染全局的 window 对象,样式会污染其他应用,必须有效的隔离起来。

● 子应用路由状态保持能力:激活子应用后,浏览器刷新、前进、后退子应用的路由都应该可以正常工作。

● 应用间通信的能力:应用间可以方便、快捷的通信。

Iframe

如果不考虑体验问题,iframe 几乎是最完美的微前端解决方案了。

iframe 最特性就是大的提供了浏览器原生的硬隔离方案,不论是样式隔离、js 隔离这类问题统统都能被完美解决。但他的最大问题也在于他的隔离性无法被突破,导致应用间上下文无法被共享,随之带来的开发体验、产品体验的问题。

● url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。

● dom割裂严重,弹窗只能在iframe内部展示,无法覆盖全局。

● 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。

● 慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。

主流微前端对比

● 蚂蚁金服团队 qiankun  介绍 - qiankun

● 京东零售团队 micro-app MicroApp

● 腾讯无极低代码团队 wujie-micro 无界 | 极致的微前端框架

● single-spa:是一个顶层路由。当路由处于活动状态时,它将下载并执行该路由的相关代码。路由的代码被称为应用,每个代码都可以(可选)拥有自己的git仓库、CI进程,并且可以独立部署。这些应用即可以用相同框架实现,也可以用不同框架实现。

● Web Component - Web API 接口参考 | MDN: 是一套不同的技术,允许你创建可重用的定制元素(它们的功能封装在你的代码之外)并且在你的 web 应用中使用它们。

对比single-spa类WebComponentWebComponent + iframe
框架qiankunmicro-appwujie-micro
首个版本v1.1.4 (2019-08-01)v0.1.0 (2021-07-09)1.0.0-rc.1 (2022-07-05)
最近更新Release v2.10.16 · umijs/qiankun · GitHub(2023-11-15)1.0.0-rc.4(2024-1-31)Comparing v1.0.20...v1.0.22 · Tencent/wujie · GitHub (2023-12-18)
ieYes==No==Yes,自动切换成iframe
接入成本较低
开箱即用==No====No==Yes
数据通信机制propsaddDataListenerprops、window、eventBus
js沙箱YesYesYes,iframe来实现js沙箱
样式隔离YesYesYes,webcomponent来实现页面的样式元素隔离
元素隔离==No==YesYes
静态资源地址补全==No==Yes==No==
预加载YesYesYes
keep-alive==No==YesYes
应用共享同一个资源YesYesYes
应用嵌套YesYesYes
插件系统==No==YesYes
子应用不改造接入==No==YesYes,满足跨域可以不改
内置降级兼容处理==No====No==Yes,通过 babel 来添加 polyfill

wujie-micro

1.模板列表

● 主应用列表

image.png

● 子应用列表:

image.png

2.快速创建

开发环境配置:

● Node.js 版本 < 18.0.0

● pnpm 脚手架示例模版基于 pnpm + turborepo 管理项目

● 如果您的当前环境中需要切换 node.js 版本, 可以使用 nvm or fnm 进行安装.

● 以下是通过 nvm 安装 Node.js 16 LTS 版本的示例:

image.png 安装:打开一个终端并使用以下命令创建一个新的wujie demo 示例:

image.png

3.文件目录

根据 npx命令生成的文件夹如下:

image.png

● 初始创建时,只有主应用examples/main-vue

● 若想创建子应用,可以直接在examples下创建,比如vue-2(基于vue2.0)

4.主应用接入子应用

1.使用setupApp

setupApp({
  name"vue2",
  urlhostMap("//localhost:7200/"),
  attrs,
  exectrue,
  fetch: credentialsFetch,
  plugins,
  alive:true,
  prefix: { "prefix-dialog""/dialog""prefix-location""/location" },
  degrade,
  ...lifecycles,
});

hostMap:

const map = {
  "//localhost:7200/""//wujie-micro.github.io/demo-vue2/",
}
export default function hostMap(host) {
  if (process.env.NODE_ENV === "production"return map[host];
  return host;
}

Lifecycles:

const lifecycles = {
  beforeLoad(appWindow) => console.log(`${appWindow.__WUJIE.id} beforeLoad 生命周期`),
  beforeMount(appWindow) => console.log(`${appWindow.__WUJIE.id} beforeMount 生命周期`),
  afterMount(appWindow) => console.log(`${appWindow.__WUJIE.id} afterMount 生命周期`),
  beforeUnmount(appWindow) => console.log(`${appWindow.__WUJIE.id} beforeUnmount 生命周期`),
  afterUnmount(appWindow) => console.log(`${appWindow.__WUJIE.id} afterUnmount 生命周期`),
  activated(appWindow) => console.log(`${appWindow.__WUJIE.id} activated 生命周期`),
  deactivated(appWindow) => console.log(`${appWindow.__WUJIE.id} deactivated 生命周期`),
  loadError(url, e) => console.log(`${url} 加载失败`, e),
};
export default lifecycles;

2.添加子应用路由

● 添加子应用无界元素:

<template>
  <!--保活模式,name相同则复用一个子应用实例,改变url无效,必须采用通信的方式告知路由变化 -->
  <WujieVue width="100%" height="100%" name="vue2" :url="vue2Url"></WujieVue>
</template>
<script>
import hostMap from "../hostMap";
import wujieVue from "wujie-vue2";
export default {
  data() {
    return {
      vue2UrlhostMap("//localhost:7200/#/") + this.$route.params.path,
    };
  },
  watch: {
    "$route.params.path": {
      handlerfunction () {
        wujieVue.bus.$emit("vue2-router-change"`/${this.$route.params.path}`);
      },
      immediatetrue,
    },
  },
};

</script>

● 将改元素加入主应用路由:

import Vue2Sub from "../views/vue2-sub.vue";
{
    path"/vue2-sub/:path",
    name"vue2-sub",
    componentVue2Sub,
  },

● 访问子应用路由地址:

<router-link to="/vue2-sub/testView">vue-2
        <a-icon :class="['main-icon', { active: vue2Flag }]" type="caret-up" @click.native="handleFlag('vue-2')" />
      </router-link>

5.子应用接入主应用

● Main.js(其中保活模式、重建模式子应用无需做任何改造工作,单例需要做生命周期改造)

vue2:

if (window.__POWERED_BY_WUJIE__) {
  let instance;
  window.__WUJIE_MOUNT = () => {
    const router = new VueRouter({ base, routes,mode"hash", });
    instance = new Vue({ router, render(h) => h(App) }).$mount("#app");
  };
  window.__WUJIE_UNMOUNT = () => {
    instance.$destroy();
  };
} else {
  new Vue({ routernew VueRouter({ base, routes,mode"hash", }), render(h) => h(App) }).$mount("#app");
}

vue3:

if (window.__POWERED_BY_WUJIE__) {
  let instance;
  window.__WUJIE_MOUNT = () => {
    const router = createRouter({ historycreateWebHistory(), routes });
    instance = createApp(App);
    instance.use(router);
    instance.mount("#app");
  };
  window.__WUJIE_UNMOUNT = () => {
    instance.unmount();
  };
} else {
  createApp(App).use(createRouter({ historycreateWebHistory(), routes })).mount("#app");
}

● 配置访问端口以及跨域(vue.config.js)

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependenciestrue,
  publicPath"./",
  devServer: {
    headers: {
      "Access-Control-Allow-Origin""*",
      "Access-Control-Allow-Headers""*",
      "Access-Control-Allow-Methods""*",
    },
    port"7200",
  },
  transpileDependencies: [
    "sockjs-client",
  ],
})

6.  运行模式

image.png

在微前端框架中,子应用放置在主应用页面中随着主应用页面的打开和关闭反复的激活和销毁,而在无界微前端框架中子应用是否保活以及是否进行生命周期的改造会进入完全不同的处理流程

6.1保活模式

子应用的 alive 设置为true时进入保活模式,内部的数据和路由的状态不会随着页面切换而丢失。

在保活模式下,子应用只会进行一次渲染,页面发生切换时承载子应用dom的webcomponent会保留在内存中,当子应用重新激活时无界会将内存中的webcomponent重新挂载到容器上。

保活模式下改变 url 子应用的路由不会发生变化,需要采用 无界提供三种方式进行通信 | 无界 的方式对子应用路由进行跳转。

6.2单例模式

子应用的alive为false且进行了生命周期改造时进入单例模式。

子应用页面如果切走,会调用window.__WUJIE_UNMOUNT销毁子应用当前实例,子应用页面如果切换回来,会调用window.__WUJIE_MOUNT渲染子应用新的子应用实例。

在单例式下,改变 url 子应用的路由会发生跳转到对应路由。

如果主应用上有多个菜单栏用到了子应用的不同页面,在每个页面启动该子应用的时候将name设置为同一个,这样可以共享一个wujie实例,承载子应用js的iframe也实现了共享,不同页面子应用的url不同,切换这个子应用的过程相当于:销毁当前应用实例 => 同步新路由 => 创建新应用实例。

6.3重建模式

子应用既没有设置为保活模式,也没有进行生命周期的改造则进入了重建模式,每次页面切换不仅会销毁承载子应用dom的webcomponent,还会销毁承载子应用js的iframe,相应的wujie实例和子应用实例都会被销毁。

重建模式下改变 url 子应用的路由会跳转对应路由,但是在 路由同步 | 无界 场景并且子应用的路由同步参数已经同步到主应用url上时则无法生效,因为改变url后会导致子应用销毁重新渲染,此时如果有同步参数则同步参数的优先级最高。

7.  路由跳转

7.1主应用到子应用

使用bus传参,子应用接受到事件后,使用router.push

主:vue2-sub.vue

watch: {
    "$route.params.path": {
      handlerfunction () {
        wujieVue.bus.$emit("vue2-router-change"`/${this.$route.params.path}`);
      },
      immediatetrue,
    },
  },

子:App.vue

mounted() {
    window.$wujie?.bus.$on("vue2-router-change"(path) => this.$router.push(path));
  },

7.2子应用跳转到主应用

使用prop传参,子应用直接调用主应用setupApp定义在props内的方法

子:testView.vue

<el-button @click="toMain">跳转主应用</el-button>
toMain() {
      window.$wujie?.props.jump("all");
    },

主:main.js

const props = {
  jump(name) => {
    router.push({ name });
  },
};
setupApp({
  name"vue2",
  urlhostMap("//localhost:7200/"),
  attrs,
  exectrue,
  fetch: credentialsFetch,
  plugins,
  prefix: { "prefix-dialog""/dialog""prefix-location""/location" },
  degrade,
  ...lifecycles,
  props,
});

7.3子应用(vue-3)跳转子应用(vue-2)

vue-3: 

页面点击跳转(communication.vue):

<el-button @click="handleClick">点击跳转testView</el-button>
handleClick() {
  window.$wujie?.props?.jumpOther(
    { path"/vue2-sub/testView" },
    `?vue2=${window.encodeURIComponent("/vue2-sub/testView")}`,
  );
},

设置props传参:

jumpOther(location, query) {
    // 跳转到主应用B页面
    router.push(location);
    const url = new URL(window.location.href);
    url.search = query
    // 手动的挂载url查询参数
    window.history.replaceState(null"", url.href);
  }

vue-2:

设置路由同步(vue2-sub.vue):

<WujieVue width="100%" height="100%" name="vue2" :url="vue2Url" :sync="true"></WujieVue>

8.  通信

8.1props 通信

主应用可以通过props注入数据和方法:

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

或使用setupApp传参:

const props = {
  jump(name) => {
    router.push({ name });
  },
};
setupApp({
  name"vue2",
  urlhostMap("//localhost:7200/"),
  attrs,
  exectrue,
  fetch: credentialsFetch,
  plugins,
  prefix: { "prefix-dialog""/dialog""prefix-location""/location" },
  degrade,
  ...lifecycles,
  props,
});

子应用可以通过$wujie来获取:

const props = window.$wujie?.props// {data: xxx, methods: xxx}

8.2window 通信

由于子应用运行的iframe的src和主应用是同域的,所以相互可以直接通信

主应用调用子应用的全局数据:

window.document.querySelector("iframe[name=子应用id]").contentWindow.xxx;

子应用调用主应用的全局数据:

window.parent.xxx;

8.3eventBus 通信

无界提供一套去中心化的通信方案,主应用和子应用、子应用和子应用都可以通过这种方式方便的进行通信,详见 api

主应用使用方式:

// 如果使用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, ...) {});