微前端-模块联邦方式使用总结

背景

  1. 项目比较庞大,需要把几大功能模块当成独立应用拎出来管理,减轻整体项目压力,实现微前端效果。
  2. 使用webpack5的模块联邦可以实现独立项目(模块)之间的组件共用,A项目可以使用B项目抛出来的组件,实现项目之间的代码共享。

模块联邦参数介绍

ModuleFederationPlugin

  • name: 应用名称,需要唯一性;
  • filename: 入口文件名称,用于对外提供模块时候的入口文件名;
  • exposes: 暴露出去的文件名称,被引用的;
  • remotes: 依赖的远程模块,用于引入外部其他模块;
  • shared: 配置共享的组件,一般是对第三方库做共享使用;
new ModuleFederationPlugin({
    // 应用名称
    name: "myApp",
    // 暴露出去生成的文件名称,被引用的
    filename: "remoteEntry.js",
    // 暴露出去的模块,被其他应用引用
    exposes: {
        './Bootstrap': './src/App.tsx',
    },
    // 远程模块,引用外部的模块
    remotes: {
        app1: "myAppChild@http://localhost:3009/remoteEntry.js",
    },
    shared: {
        react: { singleton: true, eager: true, requiredVersion: deps.react },
        "react-dom": {
          singleton: true,
          eager: true,
          requiredVersion: deps["react-dom"],
        },
        "react-router-dom": {
          singleton: true,
          eager: true,
          requiredVersion: deps["react-router-dom"],
        }
    }
})
  • 模块联邦 shared
  // 公共依赖shared的配置项
  Shared = string[] | {
    [string]: {
      eager?: boolean; // 是否立即加载模块而不是异步加载
      import?: false | SharedItem; // 应该提供给共享作用域的模块。如果在共享范围中没有发现共享模块或版本无效,还充当回退模块。默认为属性名
      packageName?: string; // 设置包名称以查找所需的版本。只有当包名不能根据请求自动确定时,才需要这样做(如要禁用自动推断,请将requiredVersion设置为false)。
      requiredVersion?: false | string; // 共享范围内模块的版本要求
      shareKey?: string; // 用这个名称在共享范围中查找模块
      shareScope?: string; // 共享范围名称
      singleton?: boolean; // 是否在共享作用域中只允许共享模块的一个版本 (单例模式).
      strictVersion?: boolean; // 如果版本无效则不接受共享模块(默认为true,如果本地回退模块可用且共享模块不是一个单例,否则为false,如果没有指定所需的版本则无效)
      version?: false | string; // 所提供模块的版本,将替换较低的匹配版本
    }[]
  }

主应用

  1. modulefederation配置
const moduleName = 'myApp'
const fileName = "remoteEntry.js"
const deps = require("./package.json").dependencies;

// 静态导入远程模块
const remotesModules = {
	app1: "myAppChild@http://localhost:3009/remoteEntry.js",
	app2: "myAppChild2@http://localhost:3010/remoteEntry.js"
}

module.exports = {
	// 模块名代表整个子应用
	name: moduleName,
	// 暴露出去的文件名称
	filename: fileName,
	// 暴露出去的模块
	// exposes: exportModules,
	// 引入外部模块
	remotes: remotesModules,
	shared: {
		...deps,
        react: { singleton: true, eager: true, requiredVersion: deps.react },
        "react-dom": {
          singleton: true,
          eager: true,
          requiredVersion: deps["react-dom"],
        },
        "react-router-dom": {
          singleton: true,
          eager: true,
          requiredVersion: deps["react-router-dom"],
        },
	}
}
  1. 动态链接远程容器

App.tsx

// 加载组件
function loadComponent(scope: any, module: string) {
  return async () => {
    // 初始化共享作用域。这将使用此构建中提供的已知模块和所有远程
    await __webpack_init_sharing__('default');
    const container = window[scope]; // or get the container somewhere else
    // 初始化容器,它可以提供共享模块
    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: string) => {
  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 = () => {
      urlCache.add(url);
      setReady(true);
    };

    element.onerror = () => {
      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: string, scope: string, module: string) => {
  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 as any);
    }
    // key includes all dependencies (scope/module)
  }, [Component, ready, key]);

  return { errorLoading, Component };
};

// 加载模块hook
// 切换url, scope, module状态进行动态导入FederatedComponent模块
const { Component: FederatedComponent, errorLoading }: any = useFederatedComponent(url, scope, module);

子应用

  1. app1: modulefederation配置
const moduleName = 'myAppChild'
const fileName = "remoteEntry.js"
const exportModules = {
	'./Bootstrap': './src/App.tsx',
}
const deps = require("./package.json").dependencies;

module.exports = {
	// 模块名代表整个子应用
	name: moduleName,
	// 暴露出去的文件名称
	filename: fileName,
	// 暴露出去的模块
	exposes: exportModules,
	// 引入外部模块
	// remotes: remotesModules,
	shared: {
		...deps,
        react: { singleton: true, eager: true, requiredVersion: deps.react },
        "react-dom": {
          singleton: true,
          eager: true,
          requiredVersion: deps["react-dom"],
        },
        "react-router-dom": {
          singleton: true,
          eager: true,
          requiredVersion: deps["react-router-dom"],
        },
	}
}
  1. app2: modulefederation配置
const moduleName = 'myAppChild2'
const fileName = "remoteEntry.js"
const exportModules = {
	'./Bootstrap': './src/App.tsx',
}
const deps = require("./package.json").dependencies;

module.exports = {
	// 模块名代表整个子应用
	name: moduleName,
	// 暴露出去的文件名称
	filename: fileName,
	// 暴露出去的模块
	exposes: exportModules,
	// 引入外部模块
	// remotes: remotesModules,
	shared: {
		...deps,
        react: { singleton: true, eager: true, requiredVersion: deps.react },
        "react-dom": {
          singleton: true,
          eager: true,
          requiredVersion: deps["react-dom"],
        },
        "react-router-dom": {
          singleton: true,
          eager: true,
          requiredVersion: deps["react-router-dom"],
        },
	}

}

微前端的好处

1.支持独立打包,独立升级,对比单体项目的部署更方便,粒度更小,独立打包构建的速度也快,维护更方便。

微前端的缺点

1.要搞几套项目代码进行配置,比单体项目略微复杂。

遇到的坑

  1. Uncaught Error: createRoot(…): Target container is not a DOM element.
// 由于react18后使用createRoot
// 子应用expose出去时要注意,不是把root.render组件暴露出去,而是把内容组件export default出去,支持export class或者function
const exportModules = {
	'./Bootstrap': './src/App.tsx',
}

demo地址

demo用的lerna进行依赖包整合,按照lerna文档操作即可,如果觉得繁琐,可以直接把my-app\my-app-child\my-app-child2独立安装使用即可

参考:

webpack Module Federation

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值