⚔️ 跨微前端通信 📦

2025-05-25

⚔️ 跨微前端通信 📦

在本文中,我将解释一些在多个应用程序之间进行通信的方法以及我在当前项目和工作中选择使用的特定方法。

如果您不熟悉这个micro frontends概念和架构,我建议您看看这些精彩的文章:

选择微前端架构的原因有很多,也许您的应用程序已经增长太多,或者新团队正在同一个 repo/代码库上进行编码,但最常见的用例之一是应用程序某个领域的解耦逻辑。

按照这个逻辑,好的架构是微前端解耦并且不需要频繁通信的架构,但微前端可能会共享或通信一些东西,如功能、组件、某些逻辑或状态。

共享代码

绘制一个包裹

对于功能,组件和通用逻辑可以放在第三个包中并在每个应用程序上导入。

创建包的方法有很多种,我不会深入探讨,但我会给你举一些例子:

共享状态

但是共享状态呢?为什么需要在多个应用之间共享状态?

让我们用一个现实世界的例子,想象一下这个电子商务:
包含主应用程序、购物车、商品详情、广告和建议微前端的电子商务线框

每个方块代表一个具有特定域或功能的微前端,并且可以使用任何框架。

带有两个微前端的电子商务线框,箭头指向购物车微前端

添加一些内容时,我们注意到应用程序的某些部分可能需要共享一些数据或状态,例如:

  • 当商品添加后,商品详情和推荐商品可能需要进行通信并通知购物车
  • 推荐商品可以使用购物车中的当前商品,并根据一些复杂的算法来推荐另一件商品
  • 当当前商品已在购物车中时,商品详情可以显示一条消息

如果两个微前端频繁地相互传递状态,请考虑合并它们。当你的微前端不是独立的模块时,微前端的缺点会更加明显。这句话来自single-spa文档,很棒,也许建议的项目可以与项目详情合并,但如果它们需要成为独立的应用程序呢?

对于这些用例,我尝试了 5 种不同的模式:

  1. Web Workers
  2. Props 和回调
  3. 自定义事件
  4. Pub Sub 库(windowed-observable)
  5. 自定义实现

比较表

  • ✅ 一流、内置、简单
  • 💛 不错,但还可以更好
  • 🔶 棘手且容易搞砸
  • 🛑 复杂而困难
标准 Web 工作者 Props 和回调 自定义事件 窗口可观察 自定义实现
设置 🛑 🔶
API 🔶 💛 💛 🔶
框架无关 🔶
可定制 🔶

Web Workers

我创建了一个示例来说明使用虚拟 Web 工作者在两个微前端之间进行简单的通信workerize-loadercreate-micro-react-app也称为crma设置反应微前端。

此示例包含monorepo2 个微前端、1 个容器应用程序和一个公开工作者的共享库。

工人📦



let said = [];

export function say(message) {
  console.log({ message, said });

  said.push(message)

  // This postMessage communicates with everyone listening to this worker
  postMessage(message);
}


Enter fullscreen mode Exit fullscreen mode

容器应用

容器应用程序正在共享自定义workyWeb 工作器。



...
import worky from 'worky';

window.worky = worky;

...


Enter fullscreen mode Exit fullscreen mode

你应该想想🤔

但是为什么不在worky每个微前端上导入它呢?

当从 node_modules 导入库并在不同的应用程序中使用它时,worker.js捆绑后每个库都会有不同的哈希值。

工人调试

因此,每个应用程序都会有不同的工作程序,因为它们不一样,我使用窗口共享同一个实例,但有不同的方法。

微前端 1️⃣



const { worky } = window;

function App() {
  const [messages, setMessages] = useState([]);

  const handleNewMessage = (message) => {
    if (message.data.type) {
      return;
    }

    setMessages((currentMessages) => currentMessages.concat(message.data));
  };

  useEffect(() => {
    worky.addEventListener('message', handleNewMessage);

    return () => {
      worky.removeEventListener('message', handleNewMessage)
    }
  }, [handleNewMessage]);

  return (
    <div className="MF">
      <h3>Microfrontend 1️⃣</h3>
      <p>New messages will be displayed below 👇</p>
      <div className="MF__messages">
        {messages.map((something, i) => <p key={something + i}>{something}</p>)}
      </div>
    </div>
  );
}


Enter fullscreen mode Exit fullscreen mode

微前端 2️⃣



const { worky } = window;

function App() {
  const handleSubmit = (e) => {
    e.preventDefault();

    const { target: form } = e;
    const input = form?.elements?.something;

    worky.say(input.value);
    form.reset();
  }

  return (
    <div className="MF">
      <h3>Microfrontend 2️⃣</h3>
      <p>⌨️ Use this form to communicate with the other microfrontend</p>
      <form onSubmit={handleSubmit}>
        <input type="text" name="something" placeholder="Type something in here"/>
        <button type="submit">Communicate!</button>
      </form>
    </div>
  );
}


Enter fullscreen mode Exit fullscreen mode

优点✅

  • 根据MDN的 说法,这样做的好处是可以在单独的线程中执行繁琐的处理,从而使主线程(通常是 UI)能够运行而不会被阻塞/减慢。

缺点❌

  • 复杂的设置
  • 详细 API
  • 如果不使用窗口,则很难在多个微前端之间共享同一个工作器

Props 和回调

当使用反应组件时,您可以随时使用 props 和回调来提升状态,这是一种在微前端之间共享小交互的绝佳方法。

crma我创建了一个示例来说明如何设置反应微前端,从而实现两个微前端之间的简单通信。

此示例包含monorepo2 个微前端和一个容器应用程序。

容器应用

我已将状态提升至容器应用程序并将其messages作为道具和handleNewMessage回调传递。



const App = ({ microfrontends }) => {
  const [messages, setMessages] = useState([]);

  const handleNewMessage = (message) => {
    setMessages((currentMessages) => currentMessages.concat(message));
  };

  return (
    <main className="App">
      <div className="App__header">
        <h1>⚔️ Cross microfrontend communication 📦</h1>
        <p>Workerized example</p>
      </div>
      <div className="App__content">
        <div className="App__content-container">
          {
            Object.keys(microfrontends).map(microfrontend => (
              <Microfrontend
                key={microfrontend}
                microfrontend={microfrontends[microfrontend]}
                customProps={{
                  messages,
                  onNewMessage: handleNewMessage,
                }}
              />
            ))
          }
        </div>
      </div>
    </main>
  );
}


Enter fullscreen mode Exit fullscreen mode

微前端 1️⃣



function App({ messages = [] }) {
  return (
    <div className="MF">
      <h3>Microfrontend 1️⃣</h3>
      <p>New messages will be displayed below 👇</p>
      <div className="MF__messages">
        {messages.map((something, i) => <p key={something + i}>{something}</p>)}
      </div>
    </div>
  );
}


Enter fullscreen mode Exit fullscreen mode

微前端 2️⃣



function App({ onNewMessage }) {
  const handleSubmit = (e) => {
    e.preventDefault();

    const { target: form } = e;
    const input = form?.elements?.something;

    onNewMessage(input.value);
    form.reset();
  }

  ...
}


Enter fullscreen mode Exit fullscreen mode

优点✅

  • 简单的 API
  • 简单设置
  • 可定制

缺点❌

  • 当有多个框架(Vue、angular、react、svelte)时设置困难
  • 每当属性发生变化时,整个微前端都会重新渲染

自定义事件

eventListeners使用合成事件是使用和进行通信的最常见方式之一CustomEvent

我创建了一个示例来说明两个微前端之间的简单通信,该示例monorepo使用 2 个微前端和 1 个容器应用程序crma来设置反应微前端。

微前端 1️⃣



function App() {
  const [messages, setMessages] = useState([]);

  const handleNewMessage = (event) => {
    setMessages((currentMessages) => currentMessages.concat(event.detail));
  };

  useEffect(() => {  
    window.addEventListener('message', handleNewMessage);

    return () => {
      window.removeEventListener('message', handleNewMessage)
    }
  }, [handleNewMessage]);

  ...
}


Enter fullscreen mode Exit fullscreen mode

微前端 2️⃣



function App({ onNewMessage }) {
  const handleSubmit = (e) => {
    e.preventDefault();

    const { target: form } = e;
    const input = form?.elements?.something;

    const customEvent = new CustomEvent('message', { detail: input.value });
    window.dispatchEvent(customEvent)
    form.reset();
  }

  ...
}


Enter fullscreen mode Exit fullscreen mode

优点✅

  • 简单设置
  • 可定制
  • 框架无关
  • 微前端不需要了解其父母

缺点❌

  • 详细的自定义事件 API

窗口可观察对象

在这个“微”服务、应用和前端的新时代,有一个共同点:分布式系统。
纵观微服务环境,一种非常流行的通信模式是发布/订阅队列,就像 AWS SQS 和 SNS 服务一样。
由于每个微前端和容器都在window,我决定使用 ,window通过发布/订阅实现进行全局通信,因此我创建了这个库,它融合了发布/订阅队列和可观察对象这两个概念,称为windowed-observable

公开附加到主题的 Observable 以发布、检索和监听其主题上的新事件。

常见用法



import { Observable } from 'windowed-observable';

// Define a specific context namespace
const observable = new Observable('cart-items');

const observer = (item) => console.log(item);

// Add an observer subscribing to new events on this observable
observable.subscribe(observer)

// Unsubscribing
observable.unsubscribe(observer);

...

// On the publisher part of the app
const observable = new Observable('cart-items');
observable.publish({ id: 1234, name: 'Mouse Gamer XyZ', quantity: 1 });


Enter fullscreen mode Exit fullscreen mode

这个库还有更多功能,如检索发布的最新事件、获取每个事件的列表、清除每个事件等等!

windowed-observable在同一个应用程序上使用示例:

微前端 1️⃣



import { Observable } from 'windowed-observable';

const observable = new Observable('messages');

function App() {
  const [messages, setMessages] = useState([]);

  const handleNewMessage = (newMessage) => {
    setMessages((currentMessages) => currentMessages.concat(newMessage));
  };

  useEffect(() => {  
    observable.subscribe(handleNewMessage);

    return () => {
      observable.unsubscribe(handleNewMessage)
    }
  }, [handleNewMessage]);

  ...
}


Enter fullscreen mode Exit fullscreen mode

微前端 2️⃣



import { Observable } from 'windowed-observable';

const observable = new Observable('messages');

function App() {
  const handleSubmit = (e) => {
    e.preventDefault();

    const { target: form } = e;
    const input = form?.elements?.something;
    observable.publish(input.value);
    form.reset();
  }

  ...
}


Enter fullscreen mode Exit fullscreen mode

欢迎随意查看并使用它❤️

优点✅

  • 简单的 API
  • 简单设置
  • 几乎可定制
  • 命名空间事件隔离
  • 检索调度事件的额外功能
  • 开源❤️

缺点❌

自定义实现

在完成所有这些示例之后,您还可以合并其中一些示例并创建自定义实现,使用封装应用程序需求的抽象,但这些选项可能很棘手且容易混淆。

结论

没有完美或最好的解决方案,我的建议是避免草率的抽象,并尝试使用最简单的解决方案,如道具和回调,如果它不适合你的需求,请尝试另一个,直到感觉良好!

您可以深入了解此存储库中的这些示例。

在下面评论您更喜欢哪一个以及原因🚀

文章来源:https://dev.to/luistak/cross-micro-frontends-communication-30m3
PREV
每个开发者必读的一本书!超级能力:代码整洁之道,鲍勃大叔终于来了
NEXT
前端面试题