背景
前端时间总结了开发远程组件的介绍
,其实也不算是远程组件, 就是通过install
的形式进行安装依赖;
虽然通过拆分组件或者方法,通过install
(不论公开还是私有)都是可以的,但是最近在新的项目中使用还是发现了一些问题;
- 使用
npm
,yarn
出现一些依赖性问题: 版本冲突,打包问题等等;yarn peerDependencies
不生效,只有使用npm
是可以的;因为项目中有autoimport.d.ts
等文件,那么使用npm
会导致依赖性重复写入autoimport.d.ts
的警告; - 使用
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 的情况,其他情况暂时先不考虑