😊一看就会的Vite模块联邦

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

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

引子

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

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

现象一:编辑器加载缓慢

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

😊一看就会的Vite模块联邦

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

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

话不多说,诸君看图。

😊一看就会的Vite模块联邦

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

😊一看就会的Vite模块联邦

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

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

话不多说,诸君看图。

😊一看就会的Vite模块联邦

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

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

问题分析

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

😊一看就会的Vite模块联邦

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

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

😊一看就会的Vite模块联邦

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

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

😊一看就会的Vite模块联邦

模块联邦

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

为什么是模块联邦?

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

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

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

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

vite-plugin-federation

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

😊一看就会的Vite模块联邦

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

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

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

😊一看就会的Vite模块联邦

Remote

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

  • 安装插件
pnpm --filter remote-ui add -rD @originjs/vite-plugin-federation

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

npm install @originjs/vite-plugin-federation --save-dev
  • vite.config.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模块联邦

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

目前浏览器对顶层await的支持还是可以满足生产的,只要不是兼容特别老的浏览器
😊一看就会的Vite模块联邦

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

...
// 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,
  },
});
  • 打包&预览
pnpm --filter remote-ui build

打包产物如下

😊一看就会的Vite模块联邦

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

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

pnpm --filter remote-ui preview

😊一看就会的Vite模块联邦

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

😊一看就会的Vite模块联邦

Host

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

  • vite.config.ts配置插件

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

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

<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 组件已经被完整的渲染在页面。

😊一看就会的Vite模块联邦

动态加载

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

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

main.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>代码如下:

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

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

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

<script setup lang="ts">
import IComponent from './components/IComponent.vue';
</script>

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

😊一看就会的Vite模块联邦

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

😊一看就会的Vite模块联邦

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

<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模块联邦

vite的坑

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

😊一看就会的Vite模块联邦

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

具体原因查看文档

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

😊一看就会的Vite模块联邦

查看文档后,尝试了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

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

<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 选项留空,就像下面这样配置。

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

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

😊一看就会的Vite模块联邦

😊一看就会的Vite模块联邦

最后

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

体验环境

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

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

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

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

😊一看就会的Vite模块联邦

原文链接:https://juejin.cn/post/7313380227160670219 作者:youth君

(0)
上一篇 2023年12月18日 上午10:18
下一篇 2023年12月18日 上午10:28

相关推荐

发表回复

登录后才能评论