了解模块联邦四
前三篇博文描述了模块联邦的基本概念、使用场景以及项目如何使用,这一篇作为完结篇,将会提到如何部署上线。
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项目中可能存在以下问题:
hmr热更新问题shared配置问题,开发环境正常使用,生产环境出现报错- 因为外部依赖的页面,会二次刷新页面
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 [];
}
}