Skip to content

背景

前端时间总结了开发远程组件的介绍,其实也不算是远程组件, 就是通过install的形式进行安装依赖;

虽然通过拆分组件或者方法,通过install(不论公开还是私有)都是可以的,但是最近在新的项目中使用还是发现了一些问题;

  1. 使用npm, yarn出现一些依赖性问题: 版本冲突,打包问题等等; yarn peerDependencies 不生效,只有使用npm是可以的;因为项目中有autoimport.d.ts等文件,那么使用npm会导致依赖性重复写入autoimport.d.ts的警告;
  2. 使用pnpm安装可以避免这些问题,但前提是服务器上有pnpm

实践方向

近期在整理实现技术上,发现了俩个方法:

vite-lib 插件模式 + fetch 加载异步组件

就是将你编写好的组件,通过vite-lib的形式,将其打包成工具插件;代码如下:

ts
export default defineConfig({
	plugins: [vue()],
	define: {
		"process.env.NODE_ENV": '"production"',
	},
	build: {
		// css 拆分 压缩
		cssCodeSplit: true,
		cssMinify: true,
		lib: {
			entry: {
				// 组件的入口
				A: "./src/components/Test.vue",
			},
			// 打包的格式
			formats: ["es"],
		},
		rollupOptions: {
			// 这个不需要有,需要将vue打包进去
			// external: ["vue"],
			output: {
				dir: "dist",
				format: "es",
			},
		},
	},
});

打包完成之后执行pnpm preview,启动服务;

在主应用中编写如下:

ts
import { defineAsyncComponent } from "vue";

// url:就是你需要加载的js文件,例如:http://localhost:4173/a.js
export async function loadRemoteComponents(url: string, name = "default") {
	try {
		const response = await fetch(url);
		const code = await response.text();
		const blob = new Blob([code], { type: "text/javascript" });
		const blobUrl = URL.createObjectURL(blob);
		const module = await import(/* @vite-ignore */ blobUrl);
		URL.revokeObjectURL(blobUrl);
		const _component = module[name];
		// 将组件,组件名,以及scopeId暴露出去
		return {
			component: defineAsyncComponent(() => Promise.resolve(_component)),
			componentName: _component.name,
			scopeId: _component.__scopeId,
		};
	} catch (error) {
		console.error("加载远程组件失败:", error);
		throw error;
	}
}

index.html中引入css样式

html
<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8" />
		<link rel="icon" type="image/svg+xml" href="/vite.svg" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
		<title>Vite + Vue + TS</title>
		<!-- 在这里添加,你远程的css地址 -->
		<link rel="stylesheet" href="http://localhost:4173/a.css" />
	</head>
	<body>
		<div id="app"></div>
		<script type="module" src="/src/main.ts"></script>
	</body>
</html>

创建一个组件 Remote_1.vue

vue
<script setup lang="ts">
const { url } = defineProps<{
	url: string;
}>();
import { loadRemoteComponents } from "./loadRemoteComponents";
const { component, componentName, scopeId } = await loadRemoteComponents(url);
</script>

<template>
	<!-- scopeId: 必须要有,不然会导致样式丢失 -->
	<component :is="component" :key="componentName" :[scopeId]="scopeId" />
</template>

<style scoped></style>

APP.vue中:

vue
<script setup lang="ts">
import Remote_1 from "./Remote_1.vue";
</script>
<template>
	<div>
		<Suspense>
			<Remote_1 />
			<template #fallback>加载中</template>
		</Suspense>
	</div>
</template>

这样基本上就可以了,但是它也是有缺陷的:

如果远程组件没有接住任何的 ui 库,插件库等等,那么就可以参考这个做法,但如果你的主应用使用了element-plus,远程组件也使用了element-plus,那么就不可以使用这个做法了,可以参考下一个做法

借助插件 @originjs/vite-plugin-federation

这个插件相对于上一个做法的好处就是:模块共享;如果主应用和远程应用都使用了element-plus有单独的配置是可以使用的;

首先主应用和远程应用都需要安装 pnpm add -D @originjs/vite-plugin-federation,配置如下:

ts
// 远程应用的vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
import federation from "@originjs/vite-plugin-federation";

// https://vite.dev/config/
export default defineConfig({
	plugins: [
		vue(),
		AutoImport({
			dts: true,
			imports: ["vue", "vue-router"],
			resolvers: [ElementPlusResolver()],
		}),
		Components({
			dts: true,
			resolvers: [ElementPlusResolver()],
		}),
		federation({
			name: "remote",
			// 打包的应用文件,也是入口文件
			filename: "remoteEntry.js",
			// 导出的路径
			exposes: {
				// 自己写一个简单的按钮组件
				"./re-button": "./src/components/ReButton.vue",
			},
			// 共享的模块
			shared: ["vue", "element-plus"],
		}),
	],
	build: {
		target: "esnext",
		minify: false,
	},
});

远程应用配置完成之后,打包完成之后执行pnpm preview,主应用会用到这个链接

ts
// 主应用的vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
import federation from "@originjs/vite-plugin-federation";

// https://vite.dev/config/
export default defineConfig({
	plugins: [
		vue(),
		AutoImport({
			dts: true,
			imports: ["vue", "vue-router"],
			resolvers: [ElementPlusResolver()],
		}),
		Components({
			dts: true,
			resolvers: [ElementPlusResolver()],
		}),
		federation({
			name: "host",
			remotes: {
				// 这个地址就是 你远程应用的入口文件地址
				remote: "http://localhost:4173/assets/remoteEntry.js",
			},
			shared: ["vue", "element-plus"],
		}),
	],
	build: {
		target: "esnext",
		minify: false,
	},
});

App.vue中:

vue
<script setup lang="ts">
import { defineAsyncComponent } from "vue";
// @ts-ignore
const ReButton = defineAsyncComponent(() => import("remote/re-button"));
</script>

<template>
	<div>
		<h1>加载远程组件</h1>

		<ReButton title="远程按钮" />
	</div>
</template>

<style scoped></style>

在主应用打包之后上传nginx或者服务器上可以正常运行的;

rsbuild 的配置

ts
// 远程应用
import { defineConfig } from "@rsbuild/core";
import { pluginVue } from "@rsbuild/plugin-vue";

export default defineConfig({
	plugins: [pluginVue()],
	source: {
		entry: {
			index: "./src/main.ts",
		},
	},
	moduleFederation: {
		name: "remote",
		filename: "remoteEntry.js",
		exposes: {
			"./Button": {
				import: "./src/components/Button.vue",
				name: "Button",
			},
		},
		shared: {
			vue: {
				singleton: true,
				requiredVersion: "^3.3.0",
				eager: true,
			},
		},
	},
	server: {
		port: 3001,
		cors: true,
	},
	html: {
		template: "./index.html",
	},
});
ts
// 主应用
import { defineConfig } from "@rsbuild/core";
import { pluginVue } from "@rsbuild/plugin-vue";

export default defineConfig({
	plugins: [pluginVue()],
	source: {
		entry: {
			index: "./src/main.ts",
		},
	},
	moduleFederation: {
		name: "host",
		remotes: {
			remote: {
				external: "remote@http://localhost:3001/remoteEntry.js",
				format: "esm",
				from: "vite",
				type: "module",
			},
		},
		shared: {
			vue: {
				singleton: true,
				requiredVersion: "^3.3.0",
				eager: true,
			},
		},
	},
	html: {
		template: "./index.html",
	},
});
ts
const remoteComponent = defineAsyncComponent({
	loader: () => import("remote/Button") as Promise<typeof import("*.vue")>,
	onError(error) {
		console.error("远程组件加载失败:", error);
	},
});

只测试了没有引入任何插件,ui 的情况,其他情况暂时先不考虑

wangxiaoze | MIT License.