😊一看就会的Vite模块联邦

为了更清晰地呈现Vite模块联邦的实践过程,本文花费了大量篇幅进行详细阐述,希望各位读者能耐心阅读,相信一定会有所收获。

同时,准备了相应的体验环境以做参考。

引子

这里提及低代码只是为了交代为什么会搞模块联邦,如果只是想了解如何实现Vite的模块联邦可以跳过【引子】

最近公司的低代码项目遇到了一些头疼的问题,表现出来的现象是编辑器加载缓慢应用打包慢、体积大等问题。

现象一:编辑器加载缓慢

首先说一下编辑器中的页面是通过iframe的方式加载的一个与运行时一样的页面,编辑器的工作区域看上去像是蒙在画布上的一个透明蒙层。

编辑器的工作区域归低代码平台管理,这部分的加载并不慢。主要慢的是通过iframe加载的应用页面,这个页面完全和应用独立运行时一样。

打开开发者工具就可以看到,主要原因是加载的包太多了,请求疯狂等待中。

话不多说,诸君看图。

上面这些密密麻麻的加载文件都是应用注册的组件和其他依赖包,通过上图可以看到光是依赖加载就用了24s。这还是经过vite打包优化后的结果,简直不可接受啊。

现象二:应用打包慢、体积大

当前低代码平台导出的应用主要包含DSL数据、静态资源和渲染器。其中DSL数据和静态资源对打包速度和体积的影响微乎其微,主要的影响因素在于渲染器部分。事实上,这个渲染器就是一个打包后的 Vue 项目。

话不多说,诸君看图。

为什么渲染器的打包速度如此慢呢?根本原因在于应用对许多组件的被动依赖。

所谓被动依赖,是因为不论应用的内容是什么,即使是一个空白的页面,应用依然会加载所有已注册的组件。

问题分析

现在的低代码平台应用结构大致是这样的

从图中可以看出根本原因在于组件总是全量注册和全量加载。随着组件数量的增加,出现了上述问题。

现在不是流行"减负"嘛,那我们可以给Script模块减减负。

解决这个问题的思路很明确:将组件包从应用中拆离出去,减少导出应用的体积 。同时,实现组件的按需加载,即应用页面需要什么组件就加载什么组件,以提高打包速度和编辑器加载速度。

解决这个问题的方案是组件的按需远程加载。这不仅包括远程加载组件,还要确保只加载应用页面所需的组件,以实现更高效的应用打包和编辑器加载。😁

模块联邦

拆解远程组件的方案经过筛选后,确定了模块联邦的方案。

为什么是模块联邦?

模块联邦是一个允许开发人员跨多个 JavaScript 应用程序或微前端共享代码和资源的概念。在传统的 Web 应用程序中,单个页面的所有代码通常包含在单个代码库中。这可能会导致难以维护和扩展的单体应用程序。

通过模块联邦,代码可以被分割成更小的、可独立部署的模块,这些模块可以在需要时按需加载。这使得微前端可以独立开发和部署,从而减少团队之间的协调并缩短开发周期。

模块联邦的核心是基于远程加载 JavaScript 模块的思想。这意味着,不是一次加载单个应用程序的所有代码,而是可以将代码分割成更小的、可独立部署的模块,这些模块可以在需要时按需加载

模块联邦的远程加载和按需加载,完美的匹配了我们的需求。

vite-plugin-federation

模块联邦的思路看起来挺不错的,但是我们的低代码技术栈采用的是 vue3vite,而问题在于 vite 并不原生支持模块联邦。好在,社区中提供了一个基于 vite 实现模块联邦的插件------@originjs/vite-plugin-federation

在使用 @originjs/vite-plugin-federation 时,有几个核心概念需要明确:

  • Vite 构建:通过 Vite 对独立项目进行打包,构建资源包。
  • Remote:是一个通过 Vite 构建的项目,它会将一些模块或代码暴露给其他使用 Vite 构建的项目消费。
  • Host:是一个使用 Vite 构建的项目,它会消费其他项目(Remote)暴露出来的模块或代码。

示例项目我用了Monorepo的形式搭建,项目结构如下:

Remote

这里我们首先配置 Remote 项目,即示例工程中的 remote-ui 项目。在初始化工程后,我们需要先配置一下 vite-plugin-federation 插件。

  • 安装插件
shell 复制代码
pnpm --filter remote-ui add -rD @originjs/vite-plugin-federation

# 如果是非npm安装请运行下面的命令

npm install @originjs/vite-plugin-federation --save-dev
  • vite.config.ts配置插件
ts 复制代码
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import federation from '@originjs/vite-plugin-federation';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    federation({
      // 作为远程模块的模块名称,必填
      name: 'remote-ui',
      // 作为远程模块的入口文件,非必填,默认为`remoteEntry.js`
      filename: 'remoteEntry.js',
      // 这里我们暴露出两个vue组件,当然也可以是其他js/ts模块
      exposes: {
        './hello-world': './src/components/HelloWorld.vue',
        './i-button': './src/components/IButton.vue',
      },
      // 本地模块和远程模块共享的依赖。可根据需要调整。
      // 本地模块需配置所有使用到的远端模块的依赖;远端模块需要配置对外提供的组件的依赖。
      shared: ['vue'],
    }),
  ],
});

到这里Remote端的模块联邦配置就完成了,接下来是 Remote 项目的vite打包配置。

  • vite.config.ts打包配置

vite-plugin-federation的官方文档中并没有对vite打包的目标进行说明,如果只按照官方文档,你在打包时可能会遇到下面的提示。

这个错误是因为vite-plugin-federation中使用了顶层的await,默认的目标环境是['es2020', 'edge88', 'firefox78', 'chrome87', 'safari14'],然而这些目标环境并不支持在模块的顶层使用await关键字。

目前浏览器对顶层await的支持还是可以满足生产的,只要不是兼容特别老的浏览器

这里需要配置一下vite的打包配置,主要是配置build.targetesnext来解决上述报错。

ts 复制代码
...
// https://vitejs.dev/config/
export default defineConfig({
  ...
  build: {
    // 假设有原生动态导入支持,并且将会转译得尽可能小
    target: 'esnext',
    // 启用混淆,减少模块体积
    minify: true,
    // 小于4096KB得引用资源将转为Base64,减少额外得HTTP请求
    assetsInlineLimit: 4096,
  },
  // 用于调试时提供服务给 Host 端
  preview: {
    host: '0.0.0.0',
    port: 5001,
  },
});
  • 打包&预览
shell 复制代码
pnpm --filter remote-ui build

打包产物如下

这里我们关心的文件有 remoteEntry.js 入口文件,__federation_shared_vue-UTNxSTI4.js 共享依赖文件,以及 __federation_expose_Hello-world-pEgJDYxf.js 暴露模块文件。

接下来启动预览,对外提供导出模块的服务,服务端口为5001

shell 复制代码
pnpm --filter remote-ui preview

到此Remote端的配置就完成了。

Host

这里我们首先配置 Host 项目,即示例工程中的 vue3-host 项目。跟 Remote 项目一项,工程初始化后先安装vite-plugin-federation 插件。

  • vite.config.ts配置插件

这里的插件配置和 Remote 项目有所区别,我们的vue3-host作为一个纯消费项目,所以配置和 Remote 项目有所不同。

ts 复制代码
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import federation from '@originjs/vite-plugin-federation';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    federation({
      name: 'vue3-host',
      // 作为本地模块,引用的远端模块入口文件
      remotes: {
        // 这里的remote-ui会作为Remote项目的入口文件的代理
        // 详细配置可查看 https://github.com/originjs/vite-plugin-federation/blob/main/README-zh.md
        'remote-ui': 'http://localhost:5001/assets/remoteEntry.js',
      },
      shared: ['vue'],
    }),
  ],
});
  • 使用远程模块

vue3-host项目的App.vue文件中使用remote-ui暴露的两个组件HelloWorldIButton

html 复制代码
<script setup lang="ts">
import HelloWorld from 'remote-ui/hello-world';
import IButton from 'remote-ui/i-button';
</script>

<template>
  <div>
    <h1>Vue3-host</h1>
    <IButton text="remote button"></IButton>
    <HelloWorld></HelloWorld>
  </div>
</template>

启动vue3-host项目后,可以看到引用的 Remote 组件已经被完整的渲染在页面。

动态加载

上面的配置,我们解决了组件的远程加载问题 ,但是别忘了我们还需要解决组件的按需加载的问题。

为了确保 <component> 在 DSL 中能够成功加载到组件,我们之前的做法是在 vue 中全局注册所有组件。这样一来,<component is="xxx"> 总是能够成功渲染。

main.ts中全局注册逻辑像下面这样:

ts 复制代码
import { createApp } from 'vue';
import HelloWorld from 'remote-ui/hello-world';
import IButton from 'remote-ui/i-button';
import App from './App.vue';

const app = createApp(App);
app.component('hello-world', HelloWorld);
app.component('i-button', IButton);
app.mount('#app');

<IComopnent>组件中,通过 DSL 的type字段来确定当前要渲染的组件,<IComponent>代码如下:

html 复制代码
<script setup lang="ts">
defineProps<{
  dsl: Record<string, any>;
}>();
</script>

<template>
  <component :is="dsl.type"></component>
</template>

我们在页面中使用<IComonent>效果如下:

html 复制代码
<script setup lang="ts">
import IComponent from './components/IComponent.vue';
</script>

<template>
  <div>
    <h1>Vue3-host</h1>
    <IComponent :dsl="{ type: 'i-button' }" />
  </div>
</template>

从截图可以看出我们只需要i-button组件,但是请求的组件除了i-button还有hello-world

OK,既然这条路走的下去,那么按需加载无非就是去掉全局的组件注册,在<IComponent>组件中,通过type来确定要加载的远程组件,动态的加载组件。

html 复制代码
<script setup lang="ts">
import { defineAsyncComponent } from 'vue';

const props = defineProps<{
  dsl: Record<string, any>;
}>();

const com = defineAsyncComponent(() => import(`remote-ui/${props.dsl.type}`));
</script>

<template>
  <component :is="com"></component>
</template>

然而这时候并没有渲染出i-button组件。

vite的坑

改造后的 <IComponent> 看起来逻辑一切正常,但事实上页面无法加载远程的 i-button 组件。回头查看控制台就会发现,控制台抛出了异常。

这表明 Vite 不支持 'remote-ui/${props.dsl.type}' 这种写法。

具体原因查看文档

此时,内心简直一万匹艸鲵🐎跑过,简直头大。

查看文档后,尝试了rollup的插件@rollup/plugin-dynamic-import-vars,然而并没有什么用。

解决方案

vite-plugin-federationissue中深入研究了一段时间后,终于发现了其隐藏的用法。我们可以摆脱对import的依赖,实现对远程模块的动态加载。

vite-plugin-federation提供了__federation_method_setRemote__federation_method_getRemote__federation_method_unwrapDefault方法。

  • __federation_method_setRemote:设置远程模块的入口地址。
  • __federation_method_getRemote:获取远程模块。
  • __federation_method_unwrapDefault:解析模块抛出内容。

接着,我们对main.ts<IComponent>组件进行了相应的改造。

main.ts

ts 复制代码
import { createApp } from 'vue';
// 'virtual:__federation__'并不是一个真实导入的模块,这个模块是`vite-plugin-federation`动态导出的
// 所以ts检查不到'virtual:__federation__'会抛出错误,这里我们忽略ts检查即可
// @ts-ignore
import { __federation_method_setRemote } from 'virtual:__federation__';

import App from './App.vue';

__federation_method_setRemote('remote-ui', {
  url: () => Promise.resolve('http://localhost:5001/assets/remoteEntry.js'),
  format: 'esm',
  from: 'vite',
});

const app = createApp(App);
app.mount('#app');

IComponent.vue

html 复制代码
<script setup lang="ts">
import { defineAsyncComponent } from 'vue';
import {
  __federation_method_getRemote,
  __federation_method_unwrapDefault,
  // @ts-ignore
} from 'virtual:__federation__';

const props = defineProps<{
  dsl: Record<string, any>;
}>();

const com = defineAsyncComponent(async () => {
  const module = await __federation_method_getRemote(
    'remote-ui',
    `./${props.dsl.type}`
  );
  return __federation_method_unwrapDefault(module);
});
</script>

<template>
  <component :is="com"></component>
</template>

同时,确保在 vite.config.ts 中保留 federation 插件的配置,因为 virtual:__federation__ 是插件动态抛出的一个模块,我们需要维持 vite 中的 federation 插件配置。不过,可以将 remote 选项留空,就像下面这样配置。

ts 复制代码
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    federation({
      name: 'vue3-host-dynamic',
      remotes: {},
      shared: ['vue'],
    }),
  ],
});

最后启动项目就可以看到我们按需加载的远程组件啦!

最后

🎉🎉🎉 恭喜,你成功地搭建了一个远程加载按需加载的vite模块联邦。希望这篇文章为你的项目带来更多的可能性和便利。

体验环境

如果你觉得这篇文章对你在开发中有所帮助,麻烦多点赞评论收藏😊

如果这篇文章对你实现某些业务有所启发,麻烦多点赞评论收藏😊

如果...,麻烦多点赞评论收藏😊

如果大家有其他模块联邦方案,欢迎留言交流哦!

相关推荐
咬人喵喵3 小时前
18 类年终总结核心 SVG 交互方案拆解
前端·css·编辑器·交互·svg
不想秃头的程序员3 小时前
JS继承方式详解
前端·面试
Mapmost3 小时前
【高斯泼溅】从“看清”到“看懂”,3DGS语义化让数字孪生“会说话”
前端
指尖跳动的光4 小时前
防止前端页面重复请求
前端·javascript
luquinn4 小时前
用canvas切图展示及标记在原图片中的位置
开发语言·前端·javascript
巧克力芋泥包4 小时前
前端vue3调取阿里的oss存储
前端
AAA阿giao4 小时前
React Hooks 详解:从 useState 到 useEffect,彻底掌握函数组件的“灵魂”
前端·javascript·react.js
RedHeartWWW4 小时前
Next.js Middleware 极简教程
前端
饼饼饼4 小时前
从 0 到 1:前端 CI/CD 实战 ( 第一篇: 云服务器环境搭建)
运维·前端·自动化运维
用户47949283569154 小时前
给前端明星开源项目Biome提 PR,被 Snapshot 测试坑了一把
前端·后端·测试