美文网首页web前端高级-vue
联邦模块之微前端

联邦模块之微前端

作者: 老鼠AI大米_Java全栈 | 来源:发表于2023-09-07 16:18 被阅读0次

    微前端概念及诞生背景

    微前端的出现背景可以追溯到大型前端应用的开发和维护过程中所面临的挑战和问题。

    1. 大型应用的复杂性:随着前端应用规模的扩大,应用的复杂性也增加。大型应用通常由多个团队协同开发,每个团队负责一部分功能模块,但整合和部署变得困难,因为不同团队使用的技术栈、构建工具和开发风格可能不同。

    2. 高度耦合的架构:传统的单体应用架构通常采用紧耦合的[模块依赖],即不同模块之间直接引用和依赖彼此。这样的架构使得代码的重用和独立部署变得困难,同时增加了代码的维护成本。

    3. 技术栈多样性:现代前端开发中存在着大量的技术栈和框架选择。不同的团队可能使用不同的技术栈来开发各自的模块,而传统的单体应用架构往往限制了团队的选择,导致技术栈的冲突和限制,理想状态是,当前一个模块可以有自己的技术选型,比如说A模块用vue2开发,B模块,是新模块,想使用当前流行的[vue3]开发,二C模块为了能基于开源项目二次开发,直接使用了[react]实现。

    4. 独立部署和快速迭代:快速交付和持续部署是现代应用开发的重要需求。然而,在传统的单体应用中,每次修改都需要重新部署整个应用,这不仅耗时,还会带来风险。同时,团队可能希望独立地迭代和部署自己的模块,以提高开发效率和灵活性。

    基于以上的背景和需求,[微前端架构]应运而生。它旨在解决大型前端应用开发中的复杂性、耦合性、技术栈多样性和独立部署等问题。微前端将应用拆分为更小的、独立的模块,每个模块可以由不同的团队开发和部署,具有独立的技术栈和生命周期。这种架构使得团队可以更好地协作、复用代码、独立部署和快速迭代,同时提高了应用的可扩展性和可维护性。

    公司项目应用拆分

    当前我们有一个迭代了3年的巨型前端项目,是一个三百多个页面的单页面SPA应用。原来是基于vue-cli2构建的,后面升级为[vue-cli3]项目。每次冷启动应用需要不下10分钟,热更新一次大概要5s,而打包输出的包体快100M了,称它为shit mountain code(屎山代码)也不为过。当然这也不能全怪前端,当时的开发也不知道项目会有N多个迭代,变得这么庞大笨重。在一个契机下,公司需要将项目做改造,大概是基于应用场景,将应用分割为多个云产品,大体如下图所示:

    image.png

    基于公司的业务发展,我们就将我们的前端应用进行了上图的应用拆分(基于公司业务拆分)。

    但是这里慎重提醒各位码友,在改造之前可以看一下这篇文章非必要不要改造

    于是我们本来是一个web应用,突然变更了十几个前端应用的部署,前端直接骂娘了,本来我一个应用开发切分支搞定,现在要开十几个项目,每个项目都有自己的master, dev, feature分支,每天切项目,切分支都搞死人了,还经常弄错项目。运维本来从一个前端应用的jenkens部署,突然变成了十几个,而且每个应用都有自己的线上环境,开发环境,测试环境,运维直接撂挑子不干了。

    一开始可谓是困难重重,微前端框架选型,我们使用的是@micro-zoe/micro-app方案。 @micro-zoe/micro-app 是一个基于 Custom Elements(Web Components)和 Shadow DOM 技术的[微前端框架]。它提供了一种轻量级的方式来实现前端应用的解耦和独立部署。

    该框架的实现原理:

    • Custom Elements(自定义元素):@micro-zoe/micro-app 利用浏览器原生的 Custom Elements API,将前端应用封装为独立的自定义元素(Custom Element)。自定义元素是一种浏览器支持的标准,允许开发者定义自己的 HTML 标签,具备类似于内置元素的行为和功能。
    • Shadow DOM(影子 DOM):每个自定义元素都有一个关联的 Shadow DOM,它提供了一种隔离的 DOM 环境。通过使用 Shadow DOM,@micro-zoe/micro-app 可以确保微前端应用的样式和 DOM 结构与其他应用或页面相互隔离,避免冲突。
    • 应用通信:@micro-zoe/micro-app 提供了一套通信机制,使得不同的微前端应用之间可以进行跨框架的通信。它使用 CustomEvent API 和全局事件总线,允许应用之间进行事件的发布和订阅,以实现应用间的消息传递。
    • 生命周期管理:@micro-zoe/micro-app 通过监听自定义元素的生命周期事件(如 connectedCallback 和 disconnectedCallback),可以在应用加载和卸载时执行相应的操作。这使得应用可以在正确的时机进行初始化、启动和销毁,保证了整体应用的稳定性。

    总的来说,@micro-zoe/micro-app 利用浏览器原生的 Custom Elements 和 Shadow DOM 技术,将前端应用封装为独立的自定义元素,并使用一套通信机制和生命周期管理来实现微前端应用之间的解耦和独立部署。它的轻量级设计和原生浏览器支持的特性使得它具有良好的性能和兼容性,并且可以与其他前端框架(如 React、Vue、Angular 等)进行集成。

    整体结构图如下:


    image.png

    经过内部的多轮讨论,最终没有选择[qiankun],而且选择了micro-app。但是真是改造,真可谓坑巨多。主要集中在以下问题:

    1. 各种跨域,静态资源加载失败(因为子应用是基于父应用的域名来访问的)
    2. 子应用内存溢出,父子应用莫名其妙加载失败。
    3. 还有一些开发规范问题,某些页面是history模式,有些是[hash模式],这就导致我们必须统一了。如果你的子应用是hash加载的,那你的子应用就不能有history模式的页面。
    4. 本地存储的问题,首先,拆分应用后,是共享当前基座应用下的本地存储的(localStorage,sessionStorage),那么子应用最好都用应用name来单独存储自己应用的key,不然会导致相互覆盖,主应用可以存一些公共的key,如:token,userInfo等。
    5. 基座应用样式影响子应用,子应用都是挂载到基座应用下面的,所以基座应用的全局样式会直接被子应用应用上。虽然官方提供了[样式隔离]的方案,但某些时候有缺陷。
    6. 子应用路由匹配不上,导致子应用进不去页面。

    总体来说:改造成本还是很高的,如果非必要,不要轻易尝试。最终经过2个月的不懈努力,项目终于稳定上线了。

    改造后无法忍受的坑

    因为应用拆分,子应用相互隔离,与基座应用的通讯是基于变量,函数级别的通讯,导致一个问题,我们所有应用的公共组件无法复用,开始是拷贝了[common-components]给每个子应用都有一份,没觉得什么问题,但是开发维护一段时间后就有问题了。

    1. 因为公共组件随着业务发展也是要更新的,只更新当前应用,那其他应用就无法更新。
    2. 某些业务组件只在某个业务模块有,其他业务模块如果有相同的业务组件需要引用,就无法引用了,只能再拷贝,拷贝的组件又引用其他文件,拷贝也变得困难。

    因为功能的组件,函数,我们都是copy的,导致我们的每个子应用还是臃肿不堪,极其冗余和不好维护。

    痛苦不堪后模块联邦来了

    Webpack 5 新增了一项功能--模块联邦(Module Federation),旨在解决前端[微服务架构]中的模块共享和应用集成问题。它使得不同的 Webpack 构建可以共享模块,从而实现了在不同的应用之间共享代码和资源的能力。

    模块联邦的实现原理如下:

    1. 主应用(Host)和远程应用(Remote):在模块联邦中,存在一个主应用和一个或多个远程应用。主应用是整个应用的入口,而远程应用是提供独立功能的应用。

    2. 远程容器(Remote Container):每个远程应用都会创建一个远程容器,它负责加载和管理远程应用的模块。远程容器是一个独立的 Webpack 构建,它包含了远程应用的代码和资源,并将它们封装为可以动态加载的模块。

    3. [共享模块](Shared Module):模块联邦允许不同的应用共享模块。主应用和远程应用可以声明它们希望共享的模块,以及模块的版本和名称。这样,在构建过程中,Webpack 会将共享模块提取到一个独立的文件中,并在主应用和远程应用之间共享使用。

    4. [动态远程加载](Dynamic Remote Loading):模块联邦使得主应用可以在运行时动态加载远程应用的模块。主应用可以根据需要动态地加载远程应用的代码和资源,并在主应用中使用这些模块。

    5. [共享上下文](Shared Context):为了确保共享模块的正确性和一致性,模块联邦提供了共享上下文的概念。通过共享上下文,主应用和远程应用可以共享一些全局的状态和配置,以便共享模块的正确执行。

    模块联邦使得不同的应用可以以独立的方式开发、构建和部署,同时可以共享和集成代码和资源。它提供了一种更灵活、松耦合的前端架构方式,有助于构建大型复杂应用和[微服务体系结构]。同时,模块联邦还提供了一些安全性措施,以确保远程应用和共享模块的安全性和可靠性。

    正如上面所说,我们需要[webpack5]的模块联邦功能,将公共组件,函数,样式,package包等共享出来,供其他应用使用。大体结构图如下:


    image.png

    在上述流程图中,展示了一个微前端架构应用的结构和关系。基座应用A作为主应用(业务中台),提供公共组件、工具函数、公共样式(表示为"C")给其他所有子应用(物业云、能源云、设备云、资管云)。某些子应用也会提供业务组件(表示为"D1"、"D2"、"D3"、"D4")给其他子应用使用。某些应用之间存在通讯关系(如B1和B2之间、B2和B3之间),但不能直接兄弟应用间通讯,而是先通讯给父组件(业务中台),再由业务中台发送事件给对应的子应用。


    image.png

    参考demo
    下面提供一个@micro-zoe/micro-app微前端架构和模块联邦的应用demo:

    v2-2e49af2902775078abf67eee3c3c491e_720w.gif

    主应用(业务中台):

    // 主应用的Webpack配置文件
    
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    const { ModuleFederationPlugin } = require('webpack').container;
    
    module.exports = {
      // 其他配置项...
      plugins: [
          new webpack.container.ModuleFederationPlugin({
            name: packageName,
            filename: 'remoteEntry.js',
            exposes: {
            // 暴露按钮组件,和工具函数
              './Button.vue': './src/components/Button.vue',
              './util.ts': './src/assets/util.ts'
            }
          })
        ]
    };
    

    sub-app1(物业云):

    // 远程应用1的Webpack配置文件
    
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    const { ModuleFederationPlugin } = require('webpack').container;
    
    module.exports = {
      // 其他配置项...
      plugins: [
          new webpack.container.ModuleFederationPlugin({
            name: 'sub-app1',
            filename: 'remoteEntry.js',
            // 使用useDynamicScript,好处是更改后不用重启服务,也可以更灵活,甚至选择不同的远程版本来加载
            remotes: {
            },
            shared: {}
          })
        ]
    };
    
    

    sub-app2(资管云):

    // 远程应用2的Webpack配置文件
    module.exports = function override(config, env) {
        // 添加模块联邦配置
        config.plugins.push(
            new ModuleFederationPlugin({
                name: 'sub-app2',
                filename: 'remoteEntry.js'
            })
        );
    
        return config;
    };
    
    

    通过以上配置,我们在主应用(base)中可以使用@micro-zoe/micro-app来实现微前端架构,同时使用模块联邦来实现子应用的组件共享。具体代码如下:

    // 主应用代码,启动微前端,注册子应用
    import microApp from '@micro-zoe/micro-app'
    microApp.start()
    

    子应用react应用使用主应用公共函数:

    // 加载远程模块
    import remoteRef from "./remoteConfig/remoteRef";
    useEffect(() => {
    (async () => {
          const { util } = await remoteRef();
          // 检测函数,和防抖函数
          const {isElementInViewport, debounce} = util
      })();
    })
    

    完整的代码放到GitHub上了,个人在开发过程中,微前端和模块联邦问题巨多,特别是结合用的时候,也会有一些莫名其妙的问题。不过大部分问题都能在找到解决办法,大家如果遇到问题,可以在下面提问题,我看到了会尽力收集问题和提供解决思路:issues

    相关文章

      网友评论

        本文标题:联邦模块之微前端

        本文链接:https://www.haomeiwen.com/subject/mmfrvdtx.html