vite-plugin-federation

1,451 阅读7分钟

背景

随着业务量的扩展、业务复杂度的逐渐提高,我们的系统也在高速成长的过程中出现了工程膨胀、开发维护困难、开发团队间协同困难等相关问题。随之而来,微前端的概念也被不断提出。vite作为一个新兴的前端开发与构建工具,也需要在这方面进行探索与尝试。

概述

vite-plugin-federation是一款为vite提供,用于支持多个独立构建的应用可以将自己的部分能力作为组件提供出来,组成一个应用程序。他们之间不存在相互依赖,可以进行独立的开发和部署。灵感来源于webpack 5提供的Module Federation特性。

虽然这款插件还在告诉成长的过程中,也已经过得了vite社区的关注,并收录进了其框架插件

什么是Module Federation

Module Federation是希望引用动态引用其他应用的模块,这里的模块与通常意义上的应用级微前端相比,要小。比如我们通常要公共使用的头部导航,侧边栏等。各个应用独立构建、打包、部署,按需引用、组合成一个新的应用。通常各个引用之间是通过统一入口文件中,对外暴露模块及其相关信息,通过异步引用的的手段,加载共享出来的模块,供自己使用。

我们为什么要使用Vite

  • 💡 极速的服务启动 使用原生 ESM 文件,无需打包!
  • ⚡️ 轻量快速的热重载 无论应用程序大小如何,都始终极快的模块热重载(HMR)
  • 🛠️ 丰富的功能 对TypeScript、JSX、CSS 等支持开箱即用。
  • 📦 优化的构建 可选 “多页应用” 或 “库” 模式的预配置 Rollup 构建
  • 🔩 通用的插件 在开发和构建之间共享 Rollup-superset 插件接口。
  • 🔑 完全类型化的API 灵活的 API 和完整 TypeScript 类型。

基于以上vite给我们提供了这么多的理由,我们将vite作为前端开发与构建工具的最优选择之一也是理所应当的,特别是在我们的前端工程日益庞大的今天,快速的修改即所得是一件多么美妙的事情。

module federation项目构建的基本逻辑

vite-plugin-federation 通过viterollup提供的hock,对构建文件进行干预,从而将所有远程模块的组件,汇总到remoteEntry.js中。本地模块通过remoteEntry.js入口,从而调用加载三方组件以及组件编译后的jscss等文件。

federation.png

  • Remote: 远程模块,不属于当前构建,在使用时从容器(远程模块)加载编译好的文件。
  • Host:消费其他Remote提供的组件的本地模块。
  • Shared:Host使用Remote提供的组件时,需要依赖的第三方库,例如vuereact等。

更具体的架构,可以参考一下社区的架构文档

如何上手

基于vite的优势,社区贡献了vite-plugin-federation,可以让我们在本地模块使用远程模块的纯js组件,用以抽取一些公共组件、小团队开发一些相对独立的功能,完成独立部署并提供给其他本地模块使用。

接下来以vue3为例,看一下如何开发、配置和使用vite-plugin-federation来达成我们的想法。

安装vite并创建vue3项目

vite-plugin-federation既然是vite的组件,我们要先安装vite并建立两个vue3的项目,这些准备工作就不在这里赘述了,有兴趣的朋友可以先去了解一下,也比较简单:

搭建第一个 Vite 项目

当然,如果你的项目现在使用的是webpack,想要体验一下vite前端开发与构建工具,也可以使用webpack-to-vite工具,来转换一下已经存在项目的,看看vite是不是趁手。

安装vite-plugin-federation

npm i @originjs/vite-plugin-federation

基本结构

projects
└───home // 对应 remote 远端模块
│   │   Content.vue // 子组件
│   │   button.js   // 子组件
│   │   vite.config.ts   // 相对应配置
│   
└───layout // 对应 host 本地模块
│   │   main.js // 异步引入远端模块
│   │   vite.config.ts   // 相对应配置

创建Remote远程模块

1. 创建一个vue3的组件

你可以使用h函数进行绘制

Button.js

import {h} from "vue";
​
const button = {
    name: "btn-component",
    render() {
        return h(
            "button",
            {
                id: "btn-remote",
                style: {
                    'background-color': 'red',
                    'border': 'none',
                    'color': 'white',
                    'padding': '15px 32px',
                    'text-align': 'center',
                    'text-decoration': 'none',
                    'display': 'inline-block',
                    'font-size': '16px'
                },
                onClick: () => {
                    this.$store.state.cartItems++
                }
            },
            "Hello Remote Button"
        );
    },
};
​
export default button;
​

也可以使用vue3的组件文件

Content.vue

<template>
  <div :class="$style.red">{{ title }}</div>
  <p id='cart-item' :class="$style.red">cartItems from vuex: {{cartCount}}</p>
  <div :class="[$style.red, $style.bold]">Red and bold</div>
</template>
<script>
export default {
  data() {
    return {
      title: 'Remote Component in Action..'
    }
  },
  computed:{
    cartCount() {
      return this.$store.state.cartItems
    }
  }
}
</script>
<style module>
.red {
  color: red;
}
.bold {
  font-weight: bold;
}
</style>

注意:

  1. 为了尽量防止远程模块与本地模块css样式之间相互污染,可以使用css-module等,相关css隔离方案或者行内样式。

2. 在vite.config.ts中,配置federation选项

vite.config.ts

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue' // 引入 vite 针对 vue 支持的插件
import federation from "@originjs/vite-plugin-federation"; // 引入 vite-plugin-federation// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    federation({
      name: 'home', // 远程模块名称
      filename: 'remoteEntry.js', // 远程模块入口文件,与本地模块中`remotes`配置相对应
      exposes: {
        './Content': './src/components/Content.vue', // 组件名称及其对应文件
        './Button': './src/components/Button.js'
      },
      shared: ["vue","vuex"] // 对外提供的组件所依赖的第三方依赖,这个例子使用了`vue`,`vuex`,此处还可以配置依赖版本,参考`Readme.md`
    })
  ],
  build: {
    target:'es2020', // 针对非行内样式,需要构建规格为 es2020,否则样式会失效,控制台给出提示
    minify: false,
    cssCodeSplit: false,
    rollupOptions: {
      output: {
        minifyInternalExports: false
      }
    }
  }
})
​

3. 设置federation.shared选项,用于在远程模块与本地模块之间共享第三方依赖

根据需要设置federation.shared选项,具体可以参考

vite.config.ts

export default defineConfig({
  ...
  plugins: [
    ...
    federation({
      ...
      shared: ["vue","vuex"] // 这里是简易配置,详细配置可以点击参考,查看官方 Readme 文档
    })
  ],
})

Host本地模块中,配置并使用远程模块中的组件

至此,我们构建的远端模块已经提供了,Content、Button 两个子组件,可以让本地模块进行使用。

1. 在vite.config.ts中配置远程模块的入口文件

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({
  server:{
    // host: "192.168.56.1", // 在 dev 场景下尽量显示声明 ip、port,防止`vite`启动时ip、port自动获取机制导致不准确的问题
    // port: 5100
  },
  cacheDir: "node_modules/.cacheDir", // 存储缓存文件的目录,非关键配置项
  plugins: [
    vue(),
    federation({
      name: "layout", // 远程模块名称,一个服务既可以作为本地模块使用远程模块组件,可以作为远程模块,对外提供组件
      filename: "remoteEntry.js", // 远程模块入口文件,与本地模块中`remotes`配置相对应
      remotes: {
        home: "http://localhost:5001/remoteEntry.js", // 远程模块入口文件的网络地址,用于获取远程模块的`remoteEntry.js`来加载组件
        "common-lib": "http://localhost:5002/remoteEntry.js",
        "css-modules":"http://localhost:5003/remoteEntry.js"
      },
      shared: ["vue","vuex"] // 远程模块组件使用的第三方依赖,如果本地有可以优先使用本地;在 dev 模式下尽量在本地引用这些第三方依赖,防止第三方组件在 dev 和打包模式下不同导致的问题。
    })
  ],
  build: {
    target:'es2020',
    minify: false,
    cssCodeSplit: true,
    rollupOptions:{
      output:{
        minifyInternalExports:false
      }
    }
  },
});

2. 异步加载远程组件

main.js

import { createApp, defineAsyncComponent } from "vue";
import store from './store';
import Layout from "./Layout.vue";
​
const HomeContent = defineAsyncComponent(() => import("home/Content")); // 创建一个只有在需要时才会加载的异步组件, 远程模块名/组件名
const HomeButton = defineAsyncComponent(() => import("home/Button"));
​
const app = createApp(Layout); // 返回一个提供应用上下文的应用实例。应用实例挂载的整个组件树共享同一个上下文。
​
app.component("home-content", HomeContent); // 将远程模块的组件,注册为组件
app.component("home-button", HomeButton);
​
app.use(store); // 安装 Vue.js 插件,这里是`vuex`
app.mount("#root");
​

声明vuex插件,不是本文主要涉及到的内容,按需便携即可

store.js

import { createStore } from 'vuex';
​
export default createStore({
    state() {
        return {
            cartItems: 5
        }
    }
});

3. 使用组件

<template>
  <Content />
  <Button />
  <hr />
  <!-- 使用远程模块, -->
  <home-content />
  <home-button />
</template><script>
import Content from "./components/Content.vue"; // 此处引用的是本地模块自己的组件
import Button from "./components/Button.js";
export default {  components: {
    Content,
    Button,
    UnusedButton,
  },};
</script><style scoped>
img {
  width: 200px;
}
.h1 {
  border: 5px solid red !important;
  padding: 1px !important;
}
.section {
  border: 1px solid black;
  padding: 10px;
}
</style>

远程模块静态资源的支持

vite默认支持将导入或引用资源将内联为 base64 编码,这里和webpack是比较相似的,不过设置阈值为4kb,比较小,可能会让我们不经意间引入的某些静态资源无法在本地模块正常显示。当然,这也是各个编译、构建工具在权衡多请求和单个请求文件大小之间给出的一个权衡,大家可以根据自己的需要进行修改。

这里根据Remote需要对外提供的组件包含的静态资源大小,调整该阈值即可保证静态资源的对外提供。

针对问题:本地模块使用远程模块时,图片请求地址为本地模块地址,导致 404 资源请求失败。

参考文档:build.assetsInlineLimit

vite.config.ts

export default defineConfig({
  ...
  build: {
    assetsInlineLimit: 40960, // 40kb
    ...
  }
})

最后,再来一波宣传,VUE Shenzhen Meetup第二期来啦~ 10月28日举办哟,欢迎大家报名参与啊~~~~~快点动动手指报名吧~!

报名链接:Anthony Fu 线上交流访谈

第二期.JPG