发布于 2026-01-06 4 阅读
0

微型前端简化:使用模块联合实现微型前端之间的状态共享,将宿主页面与微型前端状态连接起来,提供原生JS兼容性,下一步该怎么做?

微型FEs简化

一些准备工作

使用模块联邦进行微型前端开发

微型FE之间的状态共享

将主机页面与微型FE状态连接

提供原生JS兼容性

接下来该何去何从

微前端技术允许将大型 UI 组件实时共享到不同的应用程序,它潜力巨大,但实现起来却可能成为实现其价值的阻碍。本文及其配套视频将探讨如何使用 Webpack 5 内置的模块联合功能来简化微前端代码的共享。

事实上,模块联合使得组件共享变得如此简单,以至于我们可以思考微型前端带来的两个后续问题:

  1. 如何让来自同一来源的多个微型前端共享状态,而无需它们所在的页面实现状态共享。
  2. 如何让所有主机页面订阅或修改支持页面上实例化的微型前端的数据存储。

如果您想完整了解包含三个应用程序的 Micro-FE 演示设置示例(其中 React 组件与另一个 React 应用程序以及一个原生 JS 应用程序共享),请查看相关视频。

本文将重点更详细地解释视频中提出的三个核心概念。

一些准备工作

已完成的 Micro-FEs Simplified 项目包含三个与销售啤酒桶相关的不同应用程序。

啤酒瓶是太平洋西北地区的传统饮品。它们是可重复使用的大瓶子,我们用它们来装啤酒、汽水、咖啡、康普茶等等。

growlers应用程序包含三个共享的 Micro-FE 组件。其中一个Taps组件显示所有可倒入啤酒桶的不同饮品。另一个组件Search允许用户搜索可用饮品,搜索结果会立即显示在该Taps组件中。还有一个Cart组件会在用户按下按钮时显示所选饮品的列表Add to Cart。应用程序如下所示growlers

Growlers 应用,配备三个 Micro-FE

左边是Search组件,中间是Taps组件,右边是Cart组件。

这些组件随后在两个不同的应用程序中使用;host-react一个使用 React,host-vanilla另一个则仅在页面上使用原生 JavaScript。如下所示host-react

主机 React 应用中使用的微型前端

该应用程序以不同的布局和不同的Chakra-UIhost-react深色主题展示了三个微型前端。此外,左侧还有一个额外的用户界面,它连接到微型前端商店,并以更简洁的方式展示符合当前搜索参数的饮品。这种主机页面与微型前端之间的深度集成得益于模块联合。host-react

现在我们对演示应用程序有了更深入的了解,让我们来深入了解一下它的运行机制。

使用模块联邦进行微型前端开发

用模块联合(Module Federation)的术语来说,Growlers 应用程序正在暴露模块。你可以在项目文件中找到相关的机制webpack.config.js。使用 Webpack 5,只需导入 ModuleFederationPlugin 并进行配置即可。

new ModuleFederationPlugin({
  name: "growlers",
  filename: "remoteEntry.js",
  remotes: {},
  exposes: {
    "./DataComponent": "./src/components/DataComponent",
    "./Cart": "./src/components/Cart",
    "./Search": "./src/components/Search",
    "./Taps": "./src/components/Taps",
    "./store": "./src/store",
    "./VanillaCart": "./src/vanilla/VanillaCart",
    "./VanillaSearch": "./src/vanilla/VanillaSearch",
    "./VanillaTaps": "./src/vanilla/VanillaTaps",
  },
  ...
Enter fullscreen mode Exit fullscreen mode

这里最重要的字段是联合模块容器的名称,我们将其指定为 `<federated modules container> growlers`。接下来是已公开模块的列表。一开始,我们只公开 `<modules>` Cart、` <module>`SearchTaps`<module>` 组件,以及用于指定要显示的客户端数据的存储。

演示应用程序随后会公开一个DataComponentReact 宿主程序可以用来显示商店当前状态的组件。此外,它还提供了 Micro-FE 组件的原生版本,这些组件可以管理每个组件在指定选择器上的挂载(这使得原生 JS 应用程序可以轻松地使用看起来像函数一样的 React 组件)。

然后,在宿主应用程序中,我们再次使用 ModuleFederationPlugin 来使用远程的 growler:

new ModuleFederationPlugin({
  name: "hostreact",
  filename: "remoteEntry.js",
  remotes: {
    growlers: "growlers@http://localhost:8080/remoteEntry.js",
  },
  exposes: {},
  ...
Enter fullscreen mode Exit fullscreen mode

在这种情况下,host-react应用程序指定存在一个远程服务器,其 URL 为growlers

之后,使用这些组件就和使用导入语句一样简单:

import Search from "growlers/Search";
import Cart from "growlers/Cart";
import Taps from "growlers/Taps";
import DataComponent from "growlers/DataComponent";
import { load } from "growlers/store";
load("hv-taplist");
Enter fullscreen mode Exit fullscreen mode

这段代码内部host-react导入了 React Micro-FE 组件,就像导入其他 React 组件一样。同时,我们也使用客户 ID 初始化了 store,以便 Micro-FE 知道要处理哪些饮料数据。

这一切之所以有效,是因为模块联合(Module Federation)提供的是真正的 JavaScript React 代码,而不是封装在 Micro-FE 容器中的代码。模块联合支持任何可以 Web 化的代码类型:React、Vue、Angular、原生 JavaScript、JSON、转译后的 TypeScript 等等,应有尽有。

这里有三个关键区别:

  1. 您的 Micro-FE 代码无需从托管它的应用程序中提取和单独部署。
  2. 您的 Micro-FE 代码不需要以任何方式进行封装或打包。
  3. 你可以公开任何类型的代码,而不仅仅是视觉组件。

不过,所有这些都有一个很大的限制:模块联合并不提供与视图平台无关的兼容层。它无法帮助你将 React 组件嵌入到 Vue 应用中,反之亦然。如果你需要这种功能,可以考虑使用SingleSPA之类的工具(它也推荐使用模块联合作为代码传输层)。但如果你的所有应用都是 React 应用,或者你可以接受像本例中展示的那种轻量级原生 JS 组件,那么就可以放心使用了。

微型FE之间的状态共享

由于使用模块联合在应用程序之间共享代码非常容易,因此我们的示例设置接下来要考虑的是如何在不同 Micro-FE 之间共享状态,即使它们位于宿主页面的不同部分。

为了增加趣味性,我坚持一个限制条件:宿主页面无需实现任何类型的全局状态提供程序即可正常工作。宿主应用程序应该能够直接导入组件并将其添加到页面中,并且它应该能够正常工作(前提是已指定客户端存储)。
为了实现这一点,我将使用一款名为Valtio 的革命性新型微状态管理器,原因有二:首先,它极其易用;其次,它不需要任何状态提供程序。

要在应用程序中设置存储,growlers我们只需proxy从 Valtio 导入,然后创建一个具有初始状态的存储

import { proxy, ... } from "valtio";
import { Beverage } from "./types";
export interface TapStore {
  taps: Beverage[];
  searchText: string;
  alcoholLimit: number;
  filteredTaps: Beverage[];
  cart: Beverage[];
}
const store = proxy<TapStore>({
  taps: [],
  searchText: "",
  alcoholLimit: 5,
  filteredTaps: [],
  cart: [],
});
Enter fullscreen mode Exit fullscreen mode

该状态包含所有可用饮料的数组、搜索参数、符合这些筛选条件的饮料(或水龙头)以及购物车。

要使用 store ,我们可以useProxy在任何组件中使用 hook

import React from "react";
import { useProxy } from "valtio";
import store from "../store";
const Cart = () => {
  const snapshot = useProxy(store);
  return (
    <Box border={MFE_BORDER}>
      {snapshot.cart.map((beverage) => (
        ...
      ))}
      ...
   </Box>
  );
};
export default Cart;
Enter fullscreen mode Exit fullscreen mode

您无需在视图层次结构的顶部指定任何类型的提供程序。您只需proxy在共享文件中创建一个,然后使用它即可useProxy

设置值同样简单,我们可以回到商店查看其实现,setSearchText它非常简单:

export const setSearchText = (text: string) => {
  store.searchText = text;
  store.filteredTaps = filter();
};
Enter fullscreen mode Exit fullscreen mode

要给商店设置一个值,你只需要设置它就行了。没有比这更简单的办法了。

将主机页面与微型FE状态连接

由于 Valtio 非常易于使用,我们可以做更多酷炫的事情,拓展微型前端及其与宿主页面连接的边界。例如,我们可以创建一个全新的DataProvider组件

import React, { ReactElement } from "react";
import { useProxy } from "valtio";
import store, { TapStore } from "../store";
const DataComponent: React.FC<{
  children: (state: TapStore) => ReactElement<any, any>;
}> = ({ children }) => {
  const state = useProxy(store);
  return children(state);
};
export default DataComponent;
Enter fullscreen mode Exit fullscreen mode

使用 React 的宿主页面可以提供一个子函数,该子函数可以按照宿主页面需要的任何方式渲染 store 的状态。例如,演示中host-react就使用此功能来显示更小的饮料卡片:

<DataComponent>
  {({ filteredTaps }) =>
    filteredTaps.slice(0, 5).map((beverage) => (
      <SimpleGrid ...>
         ...
      </SimpleGrid>
     ))
  }
</DataComponent>
Enter fullscreen mode Exit fullscreen mode

从微型前端客户的角度来看,这非常棒。我不仅可以使用现成的微型前端组件,无需借助任何供应商即可将其放置在页面的任何位置。而且,如果我不喜欢一个或多个微型前端提供的用户界面,我还可以利用所有必要的扩展点来创建自己的组件,这些组件可以与微型前端使用的同一个商店兼容。

提供原生JS兼容性

我们在视频中讨论的另一个问题是如何在 VanillaJS 页面上显示这些组件,这其实很简单,只需为React-DOM 提供函数包装器即可:

import React from "react";
import ReactDOM from "react-dom";
import { ChakraProvider } from "@chakra-ui/react";
import Cart from "../components/Cart";
const App = () => (
  <ChakraProvider>
    <Cart />
  </ChakraProvider>
);
export default (selector: string): void => {
  ReactDOM.render(<App />, document.querySelector(selector));
};
Enter fullscreen mode Exit fullscreen mode

不要被它ChakraProvider的外观所迷惑,它只是用来为组件提供 CSS 的。

然后,在 VanillaJS 端,我们可以直接导入这些函数,然后通过选择器调用它们:

import "./index.css";
import createTaps from "growlers/VanillaTaps";
import createCart from "growlers/VanillaCart";
import createSearch from "growlers/VanillaSearch";
import { load, subscribe } from "growlers/store";
// load("growlers-tap-station");
load("hv-taplist");

...

createTaps(".taps");
createCart(".cart");
createSearch(".search");
Enter fullscreen mode Exit fullscreen mode

这些微型前端是如何实现的?谁也说不准。从原生 JavaScript 应用的角度来看,它们是被调用的函数,而 UI 则根据这些选择器显示。

在这种情况下,模块联合不仅负责将微型前端代码加载到页面上,还react负责react-dom确保代码能够运行。更棒的是,如果您采用的是微型前端的延迟加载,它也能完美运行。模块联合会将文件加载remoteEntry.js到页面上,但该文件仅包含对所需代码块的引用,只有在您决定导入和调用它们时才会生效。因此,该系统本身就支持延迟加载。

接下来该何去何从

这个例子以及更广泛的模块联合技术,其内容远不止我在这里提到的这些。你可以我的 YouTube 频道上找到关于模块联合的播放列表。或者,你也可以看看《实用模块联合》(Practical Module Federation)这本书,这是我和 Zack Jackson 合著的,书中涵盖了这项用于共享代码的全新技术的实用方法和内部机制。

文章来源:https://dev.to/jherr/micro-fes-simplified-5ac6