稀土掘金技术社区 2024年12月30日
qiankun?这次我选了wujie!
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文作者分享了其使用无界微前端框架整合部门内三个管理系统的实践经验。文章详细介绍了选择无界的原因,包括其较低的子应用改造侵入性、方便独立开发和部署的特性。文章还深入探讨了如何进行子系统登录态管理、动态选择布局方案,并重点介绍了如何利用无界的EventBus进行去中心化通信,以及如何解决跨域问题和UI组件定位问题。最后,文章还分享了如何进行公共状态提升以优化微前端架构。总结来说,这是一篇关于无界微前端实践的干货文章,为其他开发者提供了宝贵的参考。

🚀 **选择无界的原因**:作者对比了qiankun等微前端方案,最终选择无界,主要是看中其子应用改造侵入程度低,方便独立开发和部署,并且作者想尝试新事物。

🔑 **子系统登录态管理**:子应用在单独运行时登录态失效应跳转到自己的登录页,集成到无界后则应跳转到主应用登录页。通过http响应拦截和路由守卫,利用全局变量`window.__POWERED_BY_WUJIE__`和自定义的`wujieEventBus`通知主应用进行跳转。

🎨 **动态Layout方案**:文章介绍了三种方案,最终采用主应用维护Header和Menu,子应用只保留Content区域的方案,类似于qiankun。通过`wujieEventBus`实现主应用菜单点击和子应用路由跳转的同步,并支持子应用主动发起路由跳转。

✉️ **wujieEventBus的封装**:文章对无界的eventBus进行了二次封装,增加了事件队列机制,确保在子应用加载完成后才进行通信,解决了子应用未加载完成时通信失败的问题。主应用通过 `$registerMountedQueue` 注册事件,子应用在 `afterMount` 钩子中调用 `$cleanMountedQueue` 清空事件队列。

🌐 **网络请求与跨域管理**:文章提出了两种跨域解决方案:通过修改主应用的fetch方法传递给子应用,或者通过配置服务器Response Header支持跨域资源共享。同时,文章也强调了生产环境不应使用 `Access-Control-Allow-Origin: *`。

Elecat 2024-12-30 08:31 重庆

点击关注公众号,“技术干货” 及时达!

点击关注公众号,“技术干货” 及时达!


写在最前:

本文面向对无界和微前端有一定了解的人群,不再对微前端的概念和无界的基础使用方式做解释说明

前言

掘金上搜wujie,那么大眼一看,好像全都是介绍的,并没有几个落地方案的分享。正好我上个月把部门内三个业务系统用wujie整合了一下,记录成文章和大家分享一下。(为什么不用qiankun?qiankun之前做了好多次了,这次想尝个鲜~)

背景说明

笔者部门内有三个管理系统,技术栈分别是:

A:Vue2 + Webpack4 + ant-design-vue@1.7.8:该项目是部门内“司龄”最长的,从部门成立之初起,所有的业务都堆在里边。

B:Vue3 + Webpack5 + ant-desgin-vue@3.2.20:由于业务目标不清晰以及前端开发各自为战,部分需求被拆出来了一个单独的项目进行开发,但实际上然并卵。

C:Vue3 + Vite2 + ant-design-vue@3.2.20:为了响应领导“统一前端UI规范”和“低代码降本增效”的号召,这个项目应运而生,使用JSON Scheme渲染列表页 + 手写Form表单的形式开发需求。

没错,就是3个纯业务向的管理系统。对接我们部门的大部分业务人员,日常都至少需要操作3个系统,甚至有些人还会用到别的部门的系统,甚至有的人习惯打开多个浏览器tab页来回切换对比同个页面的数据。。。poor guy。。。浏览器密密麻麻的全是tab页。。。

契机

某天,发生了如下对话:

为什么选择无界?

(此处省略万字长文对比分析qiankun、micro app、single-app...)

直接摆出站在个人角度以及团队技术、业务背景下选择无界的原因:

    喜欢吃螃蟹:之前有过多次qiankun的落地经验,直接上qiankun,一点都不酷。(第一次了解到无界是22年的10月份左右,彼时的无界还在beta版,想尝尝鲜。况且就算使用无界出了岔子,也有信心能cover住)

    子应用改造,侵入程度低:就像文档中宣传的那样,我用公司的项目跑demo,除去登录态的因素外,基本可以说是0改动接入,当时脑海中只有2个字----牛X!(当然,仅仅这样接入,离上生产的标准还相距甚远;而且最后我还是选择了类似qiankun根据宿主应用动态选择layout的布局方案,改造成本也可以说是不算低了,这个暂且按下不表)

    方便独立开发、部署:与第2点相似但又不同:现有的项目有独立的域名、部署方案、且在生产环境已经稳定运行,在保留这些基础的前提下,无界的WebComponent + iframe方案算是最理想的出路(另外也有一点私心,如果生产环境的无界挂了,业务人员可以直接使用老的域名访问独立的子应用进行业务操作,毕竟出了生产事故是要通报批评的)

综上所述,确实没经过太多深思熟虑,想用就用,干就完了

干货区

下面,就是在我接入文章开头提到的3个系统后,总结出来的大致接入步骤:

    准备主应用,在接入第一个系统之前,不出意外的要先准备宿主应用。

    子系统登录态管理

    根据宿主环境,选择layout方案

    安装wujieEventBus(基于无界去中心化的通信系统做的二次封装)

    子应用afterMount生命周期

    子系统网络请求管理

    UI组件定位修复

    公共状态提升

1.准备主应用

一个比较常规、纯净的管理系统,没有过多的封装,因为宿主应用本身,也不需要什么内容。技术栈为Vue3 + Vite2 + ant-design-vue@3.2.20(没错,和系统C的技术栈一致,主打的就是一个偷懒),放张目录结构大家就明白了,没什么特殊的,有些细节后边会提到。

2.子系统登录态管理

简单来说,对于一个子应用,无论你是基于JWT还是Cookie的用户鉴权方案,在他单独运行时发生登陆态失效的情况,是要被redirect到自己的Login页面去;而当集成到了无界中运行的时候,登录态失效则应该被redirect到主应用的Login页面。

一般情况下,有两个地方需要做处理:

    http响应拦截,以axios为例:

if (response.status === 401) {  if (window.__POWERED_BY_WUJIE__) {    wujieEventBus.$emit("LOGIN_EXPIRED", APP_NAME_IN_WUJIE);  } else {    message.error("登录失效,请重新登录");    router.replace("/login");  }}

window.__POWERED_BY_WUJIE__是无界注入到子应用window当中的一个全局变量。

wujieEventBus是我对无界自带的去中心化通信方式eventBus的封装,具体内容放在第四点展开讲,这里只需要知道,是通知主应用“我”登录失效了,并且附上“我”在主应用中的身份标识(对应组件方式使用无界的<WujieVue />所需的name属性)

    路由守卫:可根据你的需要更改路由钩子,这里以beForeEach为例:

router.beforeEach((to, from, next) => {    if(validToken()) {        // some your logic ...        next();    }else {        wujieEventBus.$emit("LOGIN_EXPIRED", APP_NAME_IN_WUJIE);    }}

当然,通过路由守卫拦截下登录态失效的情况可能很少很少,但操作和上面是一样的:通知主应用“我”登录失效了,并且附上“我”在主应用中的身份标识

3.根据宿主环境,子应用动态选择layout方案

如果你的主应用布局是打算这样:

8f1fb5771d3992651707926b38a8e5d.png

子应用甚至不用切换layout方案,在下方content区域中保留子应用所有的模块;上方的Menu区作为一个应用级的切换菜单。

但如果你的主应用是打算像这样常规布局:

想实现应用级的切换,大体上有三种思路:

    主应用不设任何layout模块:即Header、Menu、Content全都是子应用的模块。那么就需要所有子应用都是这种布局,且每个子应用的Menu菜单都必须是所有应用菜单的集合,当切换到非自身的路由时,与宿主通信进行应用切换。

    与1相同,Header、Menu、Content全都是子应用的模块,但Menu仍是自己的菜单。你问我怎么切换应用?加个position: fixed的悬浮球呗(或类似的可折叠菜单)。

bb92fd682e09d744792a370c513d41c.png

通过hover悬浮球,展开/折叠菜单,点击进行应用切换。

说实话,这方案我自己都不相信有人会用。

    而第三个,也就是我选择的方案:主应用设有Header和Menu,剔除所有子应用的Header和Menu,只保留子应用的Content模块接入进来。熟悉吗?就是接qiankun那套。

大概长这样:

<template v-if="!isInWujieContainer">    <Menu />    <Layout>        <Header />        <Layout>            <keep-alive>                <router-view />            </keep-alive>        </Layout>    </Layout></template><template v-else>    <keep-alive>      <router-view />    </keep-alive></template>
// const isInWujieContainer = window.__POWERED_BY_WUJIE__

为什么选择方案3,在我看来:Menu维护在主应用中,相比于对每个子应用的Menu进行侵入式改造,开发成本和维护成本都更小。Header维护在主应用中,可以方便的管理路由栈(面包屑、tab页签,这里多提一下,我的子应用接入方式是保活+sync路由同步)

既然Menu维护在了主应用中,那么问题来了:点击了Menu中的某个菜单,怎么通知子应用跳转到对应的路由?

我们都知道,当无界开启了url sync同步的时候,主应用、子应用的url变化规则是:子应用url发生变化时,子应用的iframe会与主应用进行通信,主应用同步更新url;当页面刷新时,子应用iframe会从主应用的url中读取路由信息,保证子应用路由状态不丢失。但是并没有一种规则是主应用主动发起改变url、并且子应用能同步更新路由的方案。

我的做法其实也很简单,点击主应用Menu中的菜单时,通过wujieEventBus进行广播,对应的子应用收到消息时,切换路由:

// 主应用中点击Menu菜单export const openChildRoute = (  _router: RouterObj,  app: AppCollection,) => {    // 通知子应用路由已改变,registerMountedQueue可以理解为给子应用注册一个mounted后需要立即执行的事件,防止出现跳转到一个还未初始化的子应用时,$emit miss的问题。    EventBus.$registerMountedQueue(      app,      "CHANGE_ROUTE",      { path: _router.path, app }    );       // 更新主应用自己的url和tab页签   router.push(fullPath);   store.commit("tabs/setList", {     fullPath,     name: _router?.name || "",     title: _router?.name,   });   setActiveKey(fullPath);};
// 子应用收到消息wujieEventBus.$on("CHANGE_ROUTE", function ({ path, query, app }) {  if (app !== APP_NAME_IN_WUJIE) return;  router.push({ path, query });});

并且CHANGE_ROUTE这个事件可以是双向的:可以由主应用主动发起,通知子应用改变路由;也可以由子应用主动发起,通知主应用改变url和tab页签的显示状态。

企业微信截图_16991865379344.png

之所以这样设计,是因为我们的系统中存在一种特殊的路由页面,他不存在于Menu菜单中,是必须通过点击页面中的指定按钮才能进入。所以对于这类页面,必须是由子应用主动发起的。

4.安装wujieEventBus

无界提供了一套去中心化的通信方案,去中心化的优点显而易见:

但同时也有一个致命的缺点:通信成功的前提是建立在通信双方都online的情况下

假设这样一个场景:用户从站外的某个带参链接进入系统,参数的目的是告诉系统要重定向到指定子应用的指定路由,甚至具体要打开某个弹框。

bb72a5d9b7de765bdf88bd8d089d942.png

正常情况下,主应用判断url参数做跳转的逻辑不管放在哪里,都存在目标子应用未加载完成的可能性。

(如果你说每个子应用component的afterMount事件里都写一遍,fine,你赢了)

这个时候,只需要对无界的eventBus稍作改动,即可满足需求:

import WujieVue from "wujie-vue3";import { AppCollection } from "@/constant";import store from '@/store';const { bus } = WujieVue;type EventList = "LOGIN_EXPIRED" | "EVENT_NAME1" | "EVENT_NAME2"; // 一些事件类型涉及到公司业务,这里省去了
type EventBusInstance = { $emit: (e: EventList, params: Record<string, any>) => void; $on: (e: EventList, fn: (...args: any[]) => void) => void; $registerMountedQueue: ( app: AppCollection, e: EventList, params: Record<string, any> ) => void; // 将事件注册到子应用mount成功的的事件队列中 $cleanMountedQueue: (app: AppCollection) => void; // 清空子应用mount事件队列};
type Queue = { [app in AppCollection]?: any[];};
let instance: EventBusInstance | undefined = undefined;
export default () => { const queue: Queue = {}; if (!instance) { instance = { $emit: (event, params) => bus.$emit(event, params), $on: (event, fn) => bus.$on(event, fn), $registerMountedQueue: (app, event, params) => { const isMounted = store.state.globalState.appMounted[app]; // store中存储了子应用是否mount完成的状态 const fn = () => bus.$emit(event, params);
// 子应用已挂载完成可以直接通信 if (isMounted) return fn();
if (queue[app] && queue[app]!.length) { queue[app]!.push(fn); } else { queue[app] = [fn]; } }, $cleanMountedQueue: (app) => { while (queue[app] && queue[app]!.length) { const fn = queue[app]!.shift(); fn(); } }, }; }
return instance;};

为每个子应用都维护一个事件队列,主应用通过$registerMountedQueue注册事件时,若对应子应用已经mount完成,则直接emit进行通信;若子应用没有mount完成,则将注册的事件推入队列中。

子应用afterMount钩子中调用$cleanMountedQueue,清空属于自己的事件队列。

目前根据业务需要,只做了这一点封装,后续有可能会继续补充。

当然前边提到的这个场景,肯定还有许多不同的解决方案,根据自己的项目因地制宜才是最重要的。

5.子应用afterMount生命周期

上边第4点已经提到过,子应用afterMount钩子中要做两件事情:

    store中保存自己mount完成的状态。

    调用$cleanMountedQueue清空自己的事件队列。

6.子系统网络请求管理

网络请求管理,主要解决的是跨域问题,分两种:

刚才为啥要让买两杯咖啡,因为一杯是改后端服务支持跨域,还有一杯是改前端静态资源服务器(比如Nginx)支持跨域。

至此,你(wo)的无界微前端方案已经落地大半了,不出意外的话,除了个别地方的样式比较古怪,业务流程已经没啥大问题了,下面的工作就是各个页面点一点,修一修奇怪的样式问题。

7.UI组件定位修复

无界官方针对element-plus冒泡系列组件弹出位置不正确的解决方案是给子应用的body添加position: relative,但我这边使用ant-design-vue@1.7.8的项目并不是弹出位置不正确,而是弹出方向不对,只能暂时通过调整组件位置+修改placement的方式见一个改一个。

我这边还有一些使用左弹出的drawer组件也会有问题,起始位置并不是屏幕最左边,而是content区域的最左边。

企业微信截图_16991870384309.png

不知是否是无界的bug,drawer有个fixed定位的包裹容器,按理来说,创建这个包裹容器的时候会使用webcomponent代理的appendChild方法,可以突破子应用自身的区域限制,但通过审查元素发现,这个position: fixed; left: 0的元素,开始位置还是子应用的左侧。。。导致drawerposition: absolute的主体开始位置也只能是子应用的左侧。但并不是所有的左弹出drawer都有这个问题,很神奇。。。没办法,只好把这些有问题的暂且改为右弹出。。。有解决方案的朋友也可以交流一下。。。

8.公共状态提升

其实从这里开始,就属于优化的范畴了,目前只做了这一趴,后续有其他优化会持续补充。

做公共状态提升的原因,简单来讲就是:除了登录用户的信息以外,我们不同系统中也有着很多相同的枚举数据,这些数据本身也是从同样的接口中读的,存在vuex/pinia中。所以当一个系统独立运行时,他数据获取的逻辑不变;当作为子应用接入了微前端体系中时,只需要从主应用中等待数据同步,不需要自己再调接口去取。

// 主应用export default () => {  const duties = [    // some http request callbacks  ];  duties.forEach(async (d) => {    const { action, type, commition } = d;    const data = await action();    store.commit(commition, data);    bus.$registerMountedQueue(      'APP_NAME', // 业务系统name标识      "SYNC_STATE",      {        type,        data: toRaw(data),      }    );  });};
// 子应用const state = {    // a vuex state}
const mutations = { // a vuex mutation}
const actions = { // a vuex action}
if(window.__POWERED_BY_WUJIE__){ wujieEventBus.$on("SYNC_STATE", ({ type, data }) => { const [updateFn, stateKey, ...restPath] = type; let config = state[stateKey]; if (restPath && restPath.length) { set(config, restPath, data); // lodash set } else { config = data; } mutations[updateFn](state, config); });}else { // old logic, init all states by actions}

结语

这篇文章从开篇到写下结语,中间经历了一整个星期。后半部分整体写的比较仓促,可能有些地方和提笔之初的设想有所出入;并且许多的细节之处涉及到公司业务也没有做过多的说明。有不明白的地方、或者有想交流的同学也可以留言,我会尽可能的做答复。

另外做个说明,其实最开始的时候文章标题叫【无界(wujie-micro)微前端落地方案分享】,后来才改成现在这个名字,原因有二:

行吧,第一版先到这里,欢迎真诚交流,但如果你来抬杠?阿,对对对~ 你说的都对~


点击关注公众号,“技术干货” 及时达!

阅读原文

跳转微信打开

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

无界微前端 微前端架构 前端工程化 跨域问题 去中心化通信
相关文章