基于 Webpack5 Module Federation 的业务解耦实践

前言

本文中会提到很多目前数栈中使用的特定名词,统一做下解释描述

  • dt-common:每个子产品都会引入的公共包(类似 NPM 包)

  • AppMenus:在子产品中快速进入到其他子产品的导航栏,统一维护在 dt-common 中,子产品从 dt-common 中引入

  • Portal:所有子产品的统一入口

  • APP_CONF:子产品的一些配置信息存放

背景

由于迭代中,我们有很多需求都是针对 AppMenus 的,这些需求的生效需要各个子产品的配合,进行统一变更。现在的数栈前端的项目当中, AppMenus 的相关逻辑存在于 dt-common 中,dt-common 又以独立的目录存在于每个子项目中, 所以当出现这种需求的时候,变更的分支就包含所有的子产品,这给前端以及测试同学都带来很多重复性的工作。

本文旨在通过 webpack5 Module Federation 的实践,实现 AppMenus 与各个子产品的解耦,即 AppMenus 作为共享资源,将其从 dt-common 中分离出来,保证其更新部署无需其他子产品的配合。

本地实现

Portal 项目

  1. 拆分 AppMenus 到 Portal 下的 Components 中,Portal 内部引用 AppMenus 就是正常组件内部的引用

  2. 配置 Module Federation 相关配置,方便其他项目进行远程依赖

    const federationConfig = {
    name: 'portal',
    filename: 'remoteEntry.js',
    // 当前组件需要暴露出去的组件
    exposes: {
    './AppMenus': './src/views/components/app-menus',
    },
    shared: {
    react: {
    singleton: true,
    eager: true,
    requiredVersion: deps.react
    },
    'react-dom': {
    singleton: true,
    eager: true,
    requiredVersion: deps['react-dom'],
    },
    },
    };

    复制代码
     const plugins = [
         ...baseKoConfig.plugins,
         {
             key: 'WebpackPlugin',
             action: 'add',
             opts: {
                 name: 'ModuleFederationPlugin',
                 fn: () => new ModuleFederationPlugin({ ...federationConfig }),
             },
         },
     ].filter(Boolean);	
  3. dt-common 中修改 Navigator 组件引用 AppMenus 的方式,通过 props.children 实现

子产品项目

  1. 配置 Module Federation config

    const federationConfig = {
    name: 'xxx',
    filename: 'remoteEntry.js',
    // 关联需要引入的其他应用
    remotes: {
    // 本地相互访问采取该方式
    portal: 'portal@http://127.0.0.1:8081/portal/remoteEntry.js',
    },
    shared: {
    antd: {
    singleton: true,
    eager: true,
    requiredVersion: deps.antd,
    },
    react: {
    singleton: true,
    eager: true,
    requiredVersion: deps.react,
    },
    'react-dom': {
    singleton: true,
    eager: true,
    requiredVersion: deps['react-dom'],
    },
    },
    };

  2. 修改 AppMenus 引用方式

    const AppMenus = React.lazy(() => import('portal/AppMenus'));
    <Navigator
    {...this.props}

    复制代码
     <React.Suspense fallback="loading">
         <AppMenus {...this.props} />
     </React.Suspense>
    </Navigator>

    // 需要 ts 定义
    // typings/app.d.ts 文件
    declare module 'portal/AppMenus' {
    const AppMenus: React.ComponentType<any>;
    export default AppMenus;
    }

  3. 注意本地调试的时候,子产品中需要代理 Portal 的访问路径到 Portal 服务的端口下,才能访问 Portal 暴露出来的组件的相关chunckjs

    module.exports = {
    proxy: {
    '/portal': {
    target: 'http://127.0.0.1:8081', // 本地
    //target: 'portal 对应的地址', 本地 -〉 devops 环境
    changeOrigin: true,
    secure: false,
    onProxyReq: ProxyReq,
    },
    }
    }

远程部署

部署到服务器上,由于 Portal 项目中的 AppMenus 相当于是远程组件,即共享依赖;子产品为宿主环境,所以部署的时候需要对应部署 Portal 项目与子产品。而在上述配置中,需要变更的是加载的地址。 Portal 项目中没有需要变更的,变更的是子产品中的相关逻辑。

复制代码
//remote.tsx 
import React from 'react';

function loadComponent(scope, module) {
    return async () => {
        // Initializes the share scope. This fills it with known provided modules from this build and all remotes
        await __webpack_init_sharing__('default');
        const container = window[scope]; // or get the container somewhere else
        // Initialize the container, it may provide shared modules
        await container.init(__webpack_share_scopes__.default);
        const factory = await window[scope].get(module);
        const Module = factory();
        return Module;
    };
}

const urlCache = new Set();
const useDynamicScript = (url) => {
    const [ready, setReady] = React.useState(false);
    const [errorLoading, setErrorLoading] = React.useState(false);

    React.useEffect(() => {
        if (!url) return;

        if (urlCache.has(url)) {
            setReady(true);
            setErrorLoading(false);
            return;
        }

        setReady(false);
        setErrorLoading(false);

        const element = document.createElement('script');

        element.src = url;
        element.type = 'text/javascript';
        element.async = true;

        element.onload = () => {
            console.log('onload');
            urlCache.add(url);
            setReady(true);
        };

        element.onerror = () => {
            console.log('error');
            setReady(false);
            setErrorLoading(true);
        };

        document.head.appendChild(element);

        return () => {
            urlCache.delete(url);
            document.head.removeChild(element);
        };
    }, [url]);

    return {
        errorLoading,
        ready,
    };
};

const componentCache = new Map();

export const useFederatedComponent = (remoteUrl, scope, module) => {
    const key = `${remoteUrl}-${scope}-${module}`;
    const [Component, setComponent] = React.useState(null);

    const { ready, errorLoading } = useDynamicScript(remoteUrl);
    React.useEffect(() => {
        if (Component) setComponent(null);
        // Only recalculate when key changes
    }, [key]);

    React.useEffect(() => {
        if (ready && !Component) {
            const Comp = React.lazy(loadComponent(scope, module));
            componentCache.set(key, Comp);
            setComponent(Comp);
        }
        // key includes all dependencies (scope/module)
    }, [Component, ready, key]);

    return { errorLoading, Component };
};

//layout header.tsx
const Header = () => {
	....
	const url = `${window.APP_CONF?.remoteApp}/portal/remoteEntry.js`;
  const scope = 'portal';
  const module = './AppMenus'
	const { Component: FederatedComponent, errorLoading } = useFederatedComponent(
      url,
      scope,
      module
  );
	return (
        <Navigator logo={<Logo />} menuItems={menuItems} licenseApps={licenseApps} {...props}>
            {errorLoading ? (
                <WarningOutlined />
            ) : (
                FederatedComponent && (
                    <React.Suspense fallback={<Spin />}>
                        {<FederatedComponent {...props} top={64} showBackPortal />}
                    </React.Suspense>
                )
            )}
        </Navigator>
    );
}

如何调试

子产品本地 → Portal 本地

Portal 与某个资产同时在不同的端口上运行Portal 无需变更,子产品需要以下相关的文件在这种情况下 remoteApp 为本地启动的 portal 项目本地环境;同时当我们启动项目的时候需要将 /partal 的请求代理到本地环境

复制代码
// proxy -> 代理修改
// header 引用的远程地址 -> 修改

window.APP_CONF?.remoteApp = 'http://127.0.0.1:8081'

proxy: {
    '/portal': {
        target: 'http://127.0.0.1:8081'
    }
}

子产品本地 → Portal 的服务器环境

本地起 console 的服务服务器上部署 Portal 对应的 Module Ferderation 分支同上,只不过此时 Portal 已经部署了,remote 和代理地址只需要改成部署后的 Portal 地址即可

复制代码
// proxy -> 代理修改
// header 引用的远程地址 -> 修改

window.APP_CONF?.remoteApp = 'xxx'

proxy: {
    '/portal': {
        target: 'xxx'
    }
}

子产品服务器环境 → Portal 的服务器环境

子产品 && Portal 分别部署到服务器环境上修改子产品的 config 的配置 ,添加 window.APP_CONF.remoteApp 到 Portal 的服务器环境

异常处理

  1. 当 Portal 部署不当,或者是版本不对应的时候,没有 AppMenus 远程暴露出来的话, 做了异常处理思路是: 当请求 remoteEntry.js 出现 error 的时候,是不会展示 AppMenus 相关组件的

  2. 当 Portal 已经部署,其他子产品未接入 Module Federation, 是不会影响到子产品的正常展示的;子产品当下使用的 应是 dt-common 中的 AppMenus

如何开发 AppMenus

问题记录

依赖版本不一致

【Error】Could not find "store" in either the context or props of "Connect(N)". Either wrap the root component in a <Provider>, or explicitly pass "store" as a prop to "Connect(N)".

发现报错路径为 portal/xxx,可以定位到是 AppMunes 发生了问题,导致原因子产品 React-Redux 和 Portal React-Redux 版本不一致导致的,需要在对应子产品 federationConfig 处理 react-redux 为共享

总结

本文主要从业务层面结合 webpack 5 Module Federation ,实现 AppMenus 的解耦问题。主要涉及 dt-common 、Portal、子产品的变更。通过解耦能够发现我们对 AppMenus 的开发流程减少了不少,有效的提高了我们的效率。

文章转载自: ++袋鼠云数栈前端++

原文链接: https://www.cnblogs.com/dtux/p/17898758.html

体验地址: 引迈 - JNPF快速开发平台_低代码开发平台_零代码开发平台_流程设计器_表单引擎_工作流引擎_软件架构

相关推荐
阿里云云原生8 分钟前
阿里云 FunctionAI 技术详解:基于 Serverless 的企业级 AI 原生应用基础设施构建
人工智能·阿里云·serverless
竹君子9 分钟前
研发管理知识库(13)阿里云的DevOps工具介绍
阿里云·云计算·devops
感智教育10 分钟前
2025 年世界职业院校技能大赛汽车制造与维修赛道备赛方案
人工智能·汽车·制造
8Qi817 分钟前
Stable Diffusion详解
人工智能·深度学习·stable diffusion·图像生成
激动的小非17 分钟前
电商数据分析报告
大数据·人工智能·数据分析
carver w26 分钟前
transformer 手写数字识别
人工智能·深度学习·transformer
新智元1 小时前
GPT-5.1发布当天,文心5.0杀回来了
人工智能·openai
月下倩影时1 小时前
视觉学习篇——机器学习模型评价指标
人工智能·学习·机器学习
小毅&Nora1 小时前
【云计算】【Kubernetes】 ① K8S的架构、应用及源码解析 - 核心架构与组件全景图
架构·kubernetes·云计算
领航猿1号1 小时前
如何通过神经网络看模型参数量?
人工智能·python·神经网络·大模型参数量