微前端实践

May 30, 2024

#微前端

微前端实践

一、引言

微前端的核心思想是将前端应用视为由多个微服务组成的整体,每个微前端模块都有自己的业务逻辑、数据和用户界面,且能够独立运行和部署。

背景:新运维接入平台,qiankun和iframe都带来了较大的适配问题,从而引起微前端新框架的尝试想法

二、微前端现状

微前端方案:

​ ● 基于 NPM 包的微前端:将微应用打包成独立的 NPM 包,然后在主应用中安装和使用;

​ ● 基于代码分割的微前端:在主应用中使用懒加载技术,在运行时动态加载不同的微应用;

​ ● 基于 Web Components 的微前端:将微应用封装成自定义组件,在主应用中注册使用;

​ ● 基于 Module Federation 的微前端:借助 Webpack 5 的 Module Federation 实现微前端;

​ ● 基于动态 Script 的微前端:在主应用中动态切换微应用的 Script 脚本来实现微前端;

​ ● 基于 iframe 的微前端:在主应用中使用 iframe 标签来加载不同的微应用;

​ ● 基于框架(JavaScript SDK)的微前端:使用 single-spa、qiankun、wujie( Web Components) 等通用框架。

微前端技术现状对比:

选型静态资源预加载子应用保活iframejs沙箱css沙箱接入成本地址
EMP×××github.com/efoxTeam/em…
Qiankun××中低qiankun.umijs.org/zh/
无界中低wujie-micro.github.io/doc/
micro-app中低zeroing.jd.com/micro-app/

三、Qiankun的劣势

从全域土地和基础信息平台两个系统上感受下qiankun的实际应用,总结以下不足:

  1. 基于路由匹配,切换时自动卸载,在一张图加载时出现较为明显的白屏,即无法保活
  2. 对于qiankun初学者来说,需要掌握webpack以及qinakun的一些基础配置,路由匹配规则容易出现错误导致子应用无法加载,有一定学习成本
  3. 组件transfer后样式丢失(挂载到了主应用的dom下,但是css隔离了)
  4. 社区停摆,不支持vite框架

四、基于WebComponents的微前端框架实践

1、什么是WebComponents?

Web Components 可以理解为浏览器的原生组件,它通过组件化的方式封装微应用,从而实现应用自治。(类vue组件)

主要包含三部分:

  • Custom element(自定义元素):一组 JavaScript API,允许你定义 custom elements 及其行为,然后可以在你的用户界面中按照需要使用它们。
  • Shadow DOM(影子 DOM):一组 JavaScript API,用于将封装的“影子”DOM 树附加到元素(与主文档 DOM 分开呈现)并控制其关联的功能。通过这种方式,你可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。
  • HTML template(HTML 模板): template和 slot 元素使你可以编写不在呈现页面中显示的标记模板。然后它们可以作为自定义元素结构的基础被多次重用。

实现简单的WebComponents

参考:https://juejin.cn/post/7212603829572911159#heading-9

2、框架实践

目前市面上两款基于WebComponents的微前端框架:

  • wujie(WebComponents+iframe)
  • microApp(类WebComponents+qiankun sandbox)

2.1、Wujie

2.1.1、实现原理

将子应用的js注入主应用同域的iframe中运行,iframe是一个原生的window沙箱,内部有完整的historylocation接口,子应用实例instance运行在iframe中,通过代理 iframedocumentwebcomponent,实现两者的互联

2.1.2、实现步骤

前置条件:支持跨域

无界提供基于 vue 封装的 wujie-vue 和基于 react 封装的 wujie-react组件,可直接使用

以vite主子应用为例子

主应用:

1⃣️安装

// vue3版本
yarn add wujie-vue3

// vue2版本
yarn add wujie-vue2

2⃣️入口文件全局引用组件

typescript
import WujieVue from "wujie-vue3" app.use(WujieVue)

3⃣️应用配置、启动、预加载

ts
/** * 配置应用,主要是设置默认配置 * preloadApp、startApp的配置会基于这个配置做覆盖 */ wujieList.forEach((app) => { const { name, url, isPreload } = app setupApp({ name, // 子应用名称 url,// 子应用匹配地址 attrs: {}, // 自定义iframe属性,子应用运行在iframe内,attrs可以允许用户自定义iframe的属性 exec: true, // 预执行模式,为false时只会预加载子应用的资源,为true时会预执行子应用代码,极大的加快子应用打开速度 props: {}, // 注入给子应用的数据 // @ts-ignore fetch: credentialsFetch, // 自定义 fetch,添加自定义fetch后,子应用的静态资源请求和采用了 fetch 的接口请求全部会走自定义fetch alive: true, // 保活 plugins: [], // 插件系统 prefix: {},// 当子应用开启路由同步模式后,如果子应用链接过长,可以采用短路径替换的方式缩短同步的链接 degrade: false, // 降级处理 ...lifeCycles // 生命周期改造(单例模式) }) isPreload && preloadApp({ name, url }) }) /** * wujie组件配置 */ <WujieVue class="h-full" :name="props.name" :url="props.url" :sync="true" // 路由同步模式,开启后无界会将子应用的name作为一个url查询参数,实时同步子应用的路径作为这个查询参数的值,这样分享 URL 或者刷新浏览器子应用路由都不会丢失。 :props="propParams" :plugins="plugins" :beforeLoad="beforeLoad" :beforeMount="beforeMount" :afterMount="afterMount" :beforeUnmount="beforeUnmount" :afterUnmount="afterUnmount" ></WujieVue>

子应用:

无界有三种运行模式:单例模式保活模式重建模式

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

改造入口函数:

  • 将子应用路由的创建、实例的创建渲染挂载到window.__WUJIE_MOUNT函数上
  • 将实例的销毁挂载到window.__WUJIE_UNMOUNT
  • 如果子应用的实例化是在异步函数中进行的,在定义完生命周期函数后,请务必主动调用无界的渲染函数 window.__WUJIE.mount()

vite例子:

ts
declare global { interface Window { // 是否存在无界 __POWERED_BY_WUJIE__?: boolean // 子应用mount函数 __WUJIE_MOUNT: () => void // 子应用unmount函数 __WUJIE_UNMOUNT: () => void // 子应用无界实例 __WUJIE: { mount: () => void } } } if (window.__POWERED_BY_WUJIE__) { let instance: any window.__WUJIE_MOUNT = () => { const router = createRouter({ history: createWebHistory(), routes }) instance = createApp(App) instance.use(router) instance.mount("#app") } window.__WUJIE_UNMOUNT = () => { instance.unmount() } /* 由于vite是异步加载,而无界可能采用fiber执行机制 所以mount的调用时机无法确认,框架调用时可能vite 还没有加载回来,这里采用主动调用防止用没有mount 无界mount函数内置标记,不用担心重复mount */ window.__WUJIE.mount() } else { createApp(App) .use(createRouter({ history: createWebHistory(), routes })) .mount("#app") }

2.1.3、为什么用wujie

  1. 主应用使用统一封装好的组件,子应用无需过多的适配,**学习成本**相对较低

  2. 预加载+保活的机制实现了**首屏的高效率加载,js在空的iframe内执行,整体运行性能高,速度**快

  3. 特色功能点:

    • 三种简易的通信方式

      props

      主应用通过data传参给子应用, 子应用通过methods方法传参给主应用

      ts
      // 主应用 <WujieVue name="xxx" url="xxx" :props="{ data: xxx, methods: xxx }"></WujieVue> // 子应用 const props = window.$wujie?.props; // {data: xxx, methods: xxx}

      window

      利用子应用运行在主应用的iframe

      类似iframe的传参和调用

      ts
      // 主应用获取子应用的全局变量数据 window.document.querySelector("iframe[name=子应用id]").contentWindow.xxx //子应用获取主应用的全局变量数据 window.parent.xxx

      eventBus

      去中心化的通信方案,方便。类似于组件间的通信

      主应用

      ts
      // 使用 wujie-vue import WujieVue from"wujie-vue"; const{ bus }= WujieVue; // 主应用监听事件 bus.$on("事件名字",function(arg1,arg2, ...){}); // 主应用发送事件 bus.$emit("事件名字", arg1, arg2,...); // 主应用取消事件监听 bus.$off("事件名字",function(arg1,arg2, ...){});

      子应用

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

    • 强大的插件系统,方便用户在运行时去修改子应用代码从而避免去改动仓库代码

      ts
      const plugins = [ { jsBeforeLoaders: [{ content: "window.lodash = window.parent.lodash" }] }, // 应用共享 { // 在子应用所有的css之前 cssBeforeLoaders: [ // 强制使子应用body定位是relative { content: "body{position: relative !important}" } ] }, { jsLoader: (code: any) => { // 替换popper.js内计算偏左侧偏移量 var codes = code.replace( "left: elementRect.left - parentRect.left", "left: fixed ? elementRect.left : elementRect.left - parentRect.left" ) // 替换popper.js内右侧偏移量 return codes.replace("popper.right > data.boundaries.right", "false") } }, // 子应用样式切换问题 { patchElementHook(element: any, iframeWindow: any) { if (element.nodeName === "STYLE") { element.insertAdjacentElement = function ( _position: any, ele: any ) { iframeWindow.document.head.appendChild(ele) } } } } ]
    • 天然支持vite框架

2.1.4、遇到的问题

**Q1:**冒泡组件的处理(poptip)

A1: 官方:body设置relative(iview无效)

民间方法: 设置无界应用的plugins

参考:https://segmentfault.com/a/1190000044158847

js
plugins: [ { // 在子应用所有的css之前 cssBeforeLoaders: [ // 强制使子应用body定位是relative { content: "body{position: relative !important}" }, ], }, { jsLoader: (code) => { // 替换popper.js内计算偏左侧偏移量 var codes = code.replace( "left: elementRect.left - parentRect.left", "left: fixed ? elementRect.left : elementRect.left - parentRect.left" ); // 替换popper.js内右侧偏移量 return codes.replace("popper.right > data.boundaries.right", "false"); }, }, ],

Q2:改变url 子应用的路由切换无效

A2: 保活模式的原因, 需要在子应用监听路由变化进行跳转,单例和重建模式不会出现该情况

ts
window.$wujie?.bus.$on("vue3-router-change", (path: string) => router.push(path) )

Q3: 运维通过改变root下的theme实现换肤,子应用因为shadowdom的原因无法获取子应用自身的root

A3: shadowDom的原因导致子应用无法获取自身的root,需要把root下的全局样式挂到html下,通过获取shadowRoot的html去进行去全局样式的替换

js
const root = window.__POWERED_BY_WUJIE__ ? window.$wujie.shadowRoot.querySelector("html") : document.querySelector(":root")

PS:MicroApp不开启shadowDom的话这块可忽略


Q4: 子应用与浏览器url隔离,地图基础库适配问题

A4: 地图应用库部分是根据平台qiankun的形式,需要通过浏览器url去拼凑地址,比如dora的cesiumpath,会导致使用异常,后续看看和基础库沟通下


2.2、MicroApp

2.2.1、实现原理

MicroApp借鉴了WebComponent的思想,通过CustomElement结合自定义的ShadowDom,将微前端封装成一个类WebComponent组件,实现微前端的组件化渲染。

两套js沙箱:1、proxy+with(同qiankun) 2、iframe沙箱(支持vite)

样式隔离:每个样式添加前缀,将子应用的样式影响禁锢在当前标签区域

css
.test { color: red; } /* 转换为 */ micro-app[name="xxx"] .test { color: red; }

2.2.2、实现步骤

**前提:**子应用支持跨域

以接入新运维为例子

主应用:

1⃣️安装插件

// 安装microApp插件
npm i @micro-zoe/micro-app --save

2⃣️初始化全局启动以及相应配置项全局修改

ts
// main.ts import microApp from "@micro-zoe/micro-app" microApp.start({ 对应配置项全局修改 })

3⃣️在micro组件中进行相应配置即可

子应用:

1⃣️设置webpack动态public-path,处理静态资源

js
if (window.__MICRO_APP_ENVIRONMENT__) { __webpack_public_path__ = window.__MICRO_APP_PUBLIC_PATH__ }

2⃣️可选,用于设置渲染和卸载操作

ts
// 👇 将渲染操作放入 mount 函数,子应用初始化时会自动执行 // window.mount = () => { // instance = createApp(App).use(store).use(router) // instance.mount("#app") // } // // 👇 将卸载操作放入 unmount 函数,就是上面步骤2中的卸载函数 // window.unmount = () => { // instance.unmount() // }

3⃣️效果

2.1.3、功能点

  1. 虚拟路由系统(1.0版本新增)

    默认开启,通过disable-memory-router关闭,关闭后通过浏览器url更新页面

    开启后子应用需要完全跟浏览器url脱钩,相关功能不生效(例如:运维tab)

    虚拟路由系统拥有router的功能包括:push、replace、go、back等,轻松实现跨应用等路由跳转

    js
    /** * @param {string} name 必填,子应用的name * @param {string} path 必填,子应用除域名外的全量地址(也可以带上域名) * @param {boolean} replace 可选,是否使用replace模式,不新增堆栈记录,默认为false */ router.push({ name: "子应用名称", path: "页面地址", replace: 是否使用replace模式 })

    拥有导航守卫进行路由拦截等操作

  2. 灵活方便的通信系统

    1⃣️发送数据

    micro-app会遍历新旧值中的每个key判断值是否有变化,如果所有数据都相同则不会发送(注意:只会遍历第一层key),如果数据有变化则将新旧值进行合并后发送。

    子=>主:

    js
    // res为监听数据函数的回调 window.microApp.dispatch({city: 'HK'}, (res: any[]) => { console.log(res) // ['返回值1', '返回值2'] })

    主=>子:

    js
    // 返回值会放入数组中传递给setData的回调函数 microApp.setData('my-app', {city: 'HK'}, (res: any[]) => { console.log(res) // ['返回值1', '返回值2'] })

    子=>子:

    js
    // micro不支持直接子应用数据通信,为了有效的避免数据污染,防止多个子应用之间相互影响,需通过全局通信实现 //主应用全局通信 microApp.setGlobalData({city: 'HK'}, (res: any[]) => { console.log(res) // ['返回值1', '返回值2'] }) //子应用全局通信 window.microApp.setGlobalData({city: 'HK'}, () => { console.log('数据已经发送完成') })

    主动清空数据缓存:

    js
    microApp.clearData("my-app")

    2⃣️接收数据

    子应用:

    js
    window.microApp.addDataListener(dataListener)

    主应用:

    js
    microApp.addDataListener("my-app", dataListener)
  3. 预加载

    三个level

    • 1、加载静态资源
    • 2、将载静态资源解析成可执行代码
    • 3、执行代码并在后台渲染
    js
    // micro预加载 microApp.preFetch([ { name: "oms", url: "http://localhost:8082/oms/", level: 3 // "default-page": "/role" } ])
  4. 保活

    设置keep-alive

2.1.4、 问题

Q1、开启shadowDom后页面渲染不出来

A1: 暂未发现解决方案


Q2:运维在with沙箱中无法渲染,浏览器url不对

A2:修改为iframe沙箱即可运行


Q3:存在和无界Q3一样的问题

A3:后续沟通


Q4:对于webcompoents不支持的浏览器没做降级处理,兼容性有待考虑

A4:等待后续优化


Q5:Vite无法加载,需要额外配置

A5:更新到1.0版本后讲沙箱切换为iframe沙箱即可

0.x版本按照以下步骤处理:

官方文档参考:[Vite (jd.com)](https://zeroing.jd.com/micro-app/docs.html#/zh-cn/framework/vite)

  1. 主应用配置

    1⃣️

    vue
    <micro-app name="app1" url="http://localhost:8081/" inline disableSandbox keep-alive ></micro-app>

    Tip:这里关闭沙箱机制,所以baseUrl失效,样式隔离也失效了,基座的数据传输需要单独去处理;如果项目中接入了vite子应用需要考虑怎么处理样式隔离;考虑没有了沙箱机制出现的问题该如何去解决(我也不知道会出现什么问题,官网的建议是不接入等,1.0版本发布;)

    2⃣️main.js增加plugins

    js
    microApp.start({ plugins: { modules: { // appName即应用的name值 app1: [ { loader(code) { if (process.env.NODE_ENV === "development") { // 这里 basename 需要和子应用vite.config.js中base的配置保持一致也就是micro-vite code = code.replace( /(from|import)(\s*['"])(\/micro-vite\/)/g, (all) => { return all.replace( "/micro-vite/", "http://localhost:8081/micro-vite/" ) } ) } return code } } ] } } })
  2. 子应用配置

    1⃣️修改容器id,app => vite-app

    2⃣️修改route为hash路由(此处用history也可以)

    基座是hash路由,子应用也必须是hash路由

    基座是history路由,子应用可以是hash或history路由

    3⃣️修改vite.config

    js
    base: `${process.env.NODE_ENV === 'production' ? 'http://my-site.com' : ''}/micro-vite/`, plugins: [ vue(), vueJsx(), // 自定义插件 (function () { let basePath = '' return { name: 'vite:micro-app', apply: 'build', configResolved(config) { basePath = `${config.base}${config.build.assetsDir}/` }, writeBundle(options, bundle) { for (const chunkName in bundle) { if (Object.prototype.hasOwnProperty.call(bundle, chunkName)) { const chunk = bundle[chunkName] if (chunk.fileName && chunk.fileName.endsWith('.js')) { //@ts-ignore chunk.code = chunk.code.replace( /(from|import\()(\s*['"])(\.\.?\/)/g, //@ts-ignore (all, $1, $2, $3) => { return all.replace($3, new URL($3, basePath)) } ) //@ts-ignore const fullPath = join(options.dir, chunk.fileName) //@ts-ignore writeFileSync(fullPath, chunk.code) } } } } } })() ],

3、小结

两者共同特点:

  1. 基于 Web Components 架构,接入简单便捷,子应用无需繁琐适配。
  2. 实现应用保活,确保快速首屏加载。
  3. 通过与浏览器 URL 隔离,实现多应用协同存在。
  4. 具备强大的插件系统,最小化对子应用的干扰。

对比与总结:

qiankun问题

  1. 基于路由匹配,切换时自动卸载,在一张图加载时出现较为明显的白屏,即无法保活

    首屏快速预加载和应用保活,有效解决了微前端加载速度的问题

  2. 对于qiankun初学者来说,需要掌握webpack以及qinakun的一些基础配置,路由匹配规则容易出现错误导致子应用无法加载,有一定学习成本

    无论是无界还是 MicroApp,相较于 qiankun,学习成本和接入成本较低。Web Components 方式更为直观易懂

  3. 组件transfer后样式丢失(挂载到了主应用的dom下,但是css隔离了)

    弹窗组件的适配也成功解决了 qiankun 中弹窗样式丢失的问题。

  4. 社区停摆,不支持vite框架

    尽管两者社区活跃度相对 qiankun 较为逊色,但市面上应用较为丰富。官方对于 issue 的回复速度和维护效率较高。

    两者对于vite都做了适配工作

综合考虑,整体而言,我认为无界更为优越。其实现原理更直观,便于开发者理解。虽然 MicroApp 1.0 刚推出,解决了之前的一些问题,但社区上仍存在一些尚需沉淀的问题。


demo地址https://github.com/Lizh606/wujie-app.git

目录