Home
avatar

.Wang

了解模块联邦四

前三篇博文描述了模块联邦的基本概念、使用场景以及项目如何使用,这一篇作为完结篇,将会提到如何部署上线。

share 配置

// ASSET_PREFIX 换成你的域名, 开发环境需要和你项目的端口一致,生产环境需要和你部署的域名一致
// 这里建议将其改为环境变量
{
		format: "mf",
		output: {
			// 输出的目录
			distPath: `${DIST_PATH}/mf`,
			assetPrefix: `${ASSET_PREFIX}/mf`,
		},
		dev: { assetPrefix: `${ASSET_PREFIX}/mf` }, // 开发环境使用
		plugins: [
			PluginModuleFederation({
				filename: "remoteEntry.js", // 输出的文件名
				name: "shared_remote", // 模块联邦的名称
				exposes: {
					// 暴露的模块, /src/index.ts 是我存放的  utils. hooks. enums
					".": "./src/index.ts",
					// 暴露的组件模块, 例如button, table 等
					"./components": "./src/components/index.ts",
				},
				shared: sharedLib, // 共享的库
				shareStrategy: "version-first", // 优先使用版本匹配的共享模块
			}),
		],
	},

构建脚本,package.json

// dev 是启动模块联邦的开发服务器, 打包 iife 格式的文件
// build 打包生产环境
"scripts": {
    "build": "rslib build && vue-tsc ",
    "dev": "rslib mf-dev & rslib build --watch --env-mode development",
  },

打包完成之后,将其部署到对应的域名下,例如 https://www.example.com/mf/remoteEntry.js

vite 项目如何引入模块联邦

// remoteUrl 就是你部署的域名, 例如 https://www.example.com/mf/remoteEntry.js
federation({
	name: "host-vite",
	remotes: {
		remote_shared_app: remoteUrl,
	},
	shared: ["vue", "vue-router", "element-plus"],
	exposes: {},
});

不过在 vite项目中可能存在以下问题:

  1. hmr 热更新问题
  2. shared 配置问题,开发环境正常使用,生产环境出现报错
  3. 因为外部依赖的页面,会二次刷新页面

hmr 热更新问题解决

// vite.config.ts 中添加以下配置
define: {
  // 为模块联邦环境定义 __VUE_HMR_RUNTIME__,避免在远程模块中报错.
  // 但是这样的缺点:就是会导致远程模块无法使用 HMR,因为远程模块无法访问 __VUE_HMR_RUNTIME__。
  __VUE_HMR_RUNTIME__:
    mode === "development"
      ? `({createRecord:function(){},rerender:function(){},reload:function(){}})`
      : "undefined",
}

shared 生产环境配置问题解决

为什么开发环境正常?

因为开发环境下,文件的名字是正常的,而在生产环境打包的名字就会被改变,导致生产环境下的文件无法被找到。

# 名字应该是这样的, 而不是 index.js, 123.js之类的。
"host_mf_2_vite__mf_v__runtimeInit__mf_v__.js",
"host_mf_2_vite__prebuild__element_mf_2_plus__prebuild__.js",
"host_mf_2_vite__prebuild__vue__prebuild__.js",
"host_mf_2_vite__prebuild__vue_mf_2_router__prebuild__.js"

如何解决:就需要打包的时候,将 filename 配置为 [name].js, 而不是默认的 index.js

我是这样进行chunk命名的:

function generateChunkFileName(chunkInfo: ChunkInfo): string {
	const { name, facadeModuleId, moduleIds } = chunkInfo;
	let _name = name;

	// 处理 node_modules 中的包
	if (facadeModuleId?.includes("node_modules")) {
		const chunkName = getChunkName(facadeModuleId);
		if (chunkName) {
			_name = chunkName;
		}
	}

	// 处理 index 文件
	if (name?.includes("index")) {
		const n = moduleIds[0]?.split("/").filter(Boolean) || [];
		if (n.length >= 2) {
			_name = `${n[n.length - 2]}-${n[n.length - 1]
				.split("?")
				.filter(Boolean)[0]
				.replace(".", "-")}`;
		}
	}

	return `js/${_name}-[hash].js`;
}

function getChunkName(
	facadeModuleId: string | null | undefined
): string | null {
	if (!facadeModuleId?.includes("node_modules")) {
		return null;
	}
	// Vue 核心
	if (
		facadeModuleId.includes("vue-router") ||
		facadeModuleId.includes("pinia")
	) {
		return "vue-vendor";
	}
	// 默认 vendor
	return "vendor";
}

二次刷新页面 解决

不论是在开发还是生产环境, 某些页面安装了第三方的依赖,导致该页面去加载这些依赖代码,导致页面重新刷新了页面的问题:

当然项目的终端也是会有对应的提示信息

// vite.config.ts 配置
optimizeDeps: createOptimizeDepsConfig(process.cwd()),

export function createOptimizeDepsConfig(rootDir?: string) {
	const elementPlusStyles = getElementPlusStylePaths(rootDir);
	// 动态获取模块联邦虚拟模块
	const virtualModules = getModuleFederationVirtualModules(rootDir);

	return {
    // BASE_DEPENDENCIES 是我项目中使用的一些库, 例如 vue, vue-router, pinia 等
		include: [...BASE_DEPENDENCIES, ...virtualModules, ...elementPlusStyles],
	};
}

/**
 * 获取所有 Element Plus 组件样式路径
 * @param rootDir - 项目根目录(可选,默认使用 process.cwd())
 * @returns 样式路径数组
 */
export function getElementPlusStylePaths(rootDir?: string): string[] {
	const stylePaths: string[] = ["element-plus/es"];
	const projectRoot = rootDir || process.cwd();
	const componentsDir = path.resolve(
		projectRoot,
		"node_modules/element-plus/es/components"
	);

	// 检查目录是否存在
	if (!fs.existsSync(componentsDir)) {
		console.warn(
			`Element Plus components directory not found: ${componentsDir}`
		);
		return stylePaths;
	}

	try {
		const files = fs.readdirSync(componentsDir);

		files.forEach(dirname => {
			const cssPath = path.join(componentsDir, dirname, "style", "css.mjs");

			// 使用同步方法检查文件是否存在
			if (fs.existsSync(cssPath)) {
				stylePaths.push(`element-plus/es/components/${dirname}/style/css`);
			}
		});
	} catch (error) {
		console.error("Error reading Element Plus components directory:", error);
	}

	return stylePaths;
}

/**
 * 从 node_modules/__mf__virtual 目录获取模块联邦虚拟模块列表
 * @param rootDir - 项目根目录(可选,默认使用 process.cwd())
 * @returns 虚拟模块路径数组
 */
export function getModuleFederationVirtualModules(rootDir?: string): string[] {
	const projectRoot = rootDir || process.cwd();
	const virtualModulesDir = path.resolve(
		projectRoot,
		"node_modules/__mf__virtual"
	);

	// 检查目录是否存在
	if (!fs.existsSync(virtualModulesDir)) {
		return [];
	}

	try {
		const files = fs.readdirSync(virtualModulesDir);

		// 只匹配特定的模块联邦虚拟模块文件
		const targetFiles = [
			"host_mf_2_vite__mf_v__runtimeInit__mf_v__.js",
			"host_mf_2_vite__prebuild__element_mf_2_plus__prebuild__.js",
			"host_mf_2_vite__prebuild__vue__prebuild__.js",
			"host_mf_2_vite__prebuild__vue_mf_2_router__prebuild__.js",
		];

		// 过滤出目标文件,并添加 __mf__virtual/ 前缀
		return files
			.filter(file => targetFiles.includes(file))
			.map(file => `__mf__virtual/${file}`);
	} catch (error) {
		console.warn(
			`Failed to read module federation virtual modules directory: ${error}`
		);
		return [];
	}
}
总结 技术调研

同系列的博文

了解模块联邦一
总结技术调研

了解模块联邦一

了解模块联邦二
总结技术调研

了解模块联邦二

了解模块联邦五
总结技术调研

了解模块联邦五

设置