⚔️ 跨微前端通信 📦
在本文中,我将解释一些在多个应用程序之间进行通信的方法以及我在当前项目和工作中选择使用的特定方法。
如果您不熟悉这个micro frontends
概念和架构,我建议您看看这些精彩的文章:
- https://microfrontends.com
- https://micro-frontends.org
- https://martinfowler.com/articles/micro-frontends.html
选择微前端架构的原因有很多,也许您的应用程序已经增长太多,或者新团队正在同一个 repo/代码库上进行编码,但最常见的用例之一是应用程序某个领域的解耦逻辑。
按照这个逻辑,好的架构是微前端解耦并且不需要频繁通信的架构,但微前端可能会共享或通信一些东西,如功能、组件、某些逻辑或状态。
共享代码
对于功能,组件和通用逻辑可以放在第三个包中并在每个应用程序上导入。
创建包的方法有很多种,我不会深入探讨,但我会给你举一些例子:
共享状态
但是共享状态呢?为什么需要在多个应用之间共享状态?
每个方块代表一个具有特定域或功能的微前端,并且可以使用任何框架。
添加一些内容时,我们注意到应用程序的某些部分可能需要共享一些数据或状态,例如:
- 当商品添加后,商品详情和推荐商品可能需要进行通信并通知购物车
- 推荐商品可以使用购物车中的当前商品,并根据一些复杂的算法来推荐另一件商品
- 当当前商品已在购物车中时,商品详情可以显示一条消息
如果两个微前端频繁地相互传递状态,请考虑合并它们。当你的微前端不是独立的模块时,微前端的缺点会更加明显。这句话来自single-spa文档,很棒,也许建议的项目可以与项目详情合并,但如果它们需要成为独立的应用程序呢?
对于这些用例,我尝试了 5 种不同的模式:
比较表
- ✅ 一流、内置、简单
- 💛 不错,但还可以更好
- 🔶 棘手且容易搞砸
- 🛑 复杂而困难
标准 | Web 工作者 | Props 和回调 | 自定义事件 | 窗口可观察 | 自定义实现 |
---|---|---|---|---|---|
设置 | 🛑 | ✅ | ✅ | ✅ | 🔶 |
API | 🔶 | 💛 | 💛 | ✅ | 🔶 |
框架无关 | ✅ | ✅ | ✅ | ✅ | 🔶 |
可定制 | ✅ | ✅ | ✅ | ✅ | 🔶 |
Web Workers
我创建了一个示例来说明使用虚拟 Web 工作者在两个微前端之间进行简单的通信workerize-loader
,create-micro-react-app
也称为crma
设置反应微前端。
此示例包含monorepo
2 个微前端、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);
}
容器应用
容器应用程序正在共享自定义worky
Web 工作器。
...
import worky from 'worky';
window.worky = worky;
...
你应该想想🤔
但是为什么不在
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>
);
}
微前端 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>
);
}
优点✅
- 根据MDN的 说法,这样做的好处是可以在单独的线程中执行繁琐的处理,从而使主线程(通常是 UI)能够运行而不会被阻塞/减慢。
缺点❌
- 复杂的设置
- 详细 API
- 如果不使用窗口,则很难在多个微前端之间共享同一个工作器
Props 和回调
当使用反应组件时,您可以随时使用 props 和回调来提升状态,这是一种在微前端之间共享小交互的绝佳方法。
crma
我创建了一个示例来说明如何设置反应微前端,从而实现两个微前端之间的简单通信。
此示例包含monorepo
2 个微前端和一个容器应用程序。
容器应用
我已将状态提升至容器应用程序并将其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>
);
}
微前端 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>
);
}
微前端 2️⃣
function App({ onNewMessage }) {
const handleSubmit = (e) => {
e.preventDefault();
const { target: form } = e;
const input = form?.elements?.something;
onNewMessage(input.value);
form.reset();
}
...
}
优点✅
- 简单的 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]);
...
}
微前端 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();
}
...
}
优点✅
- 简单设置
- 可定制
- 框架无关
- 微前端不需要了解其父母
缺点❌
- 详细的自定义事件 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 });
这个库还有更多功能,如检索发布的最新事件、获取每个事件的列表、清除每个事件等等!
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]);
...
}
微前端 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();
}
...
}
欢迎随意查看并使用它❤️
优点✅
- 简单的 API
- 简单设置
- 几乎可定制
- 命名空间事件隔离
- 检索调度事件的额外功能
- 开源❤️
缺点❌
自定义实现
在完成所有这些示例之后,您还可以合并其中一些示例并创建自定义实现,使用封装应用程序需求的抽象,但这些选项可能很棘手且容易混淆。
结论
没有完美或最好的解决方案,我的建议是避免草率的抽象,并尝试使用最简单的解决方案,如道具和回调,如果它不适合你的需求,请尝试另一个,直到感觉良好!
您可以深入了解此存储库中的这些示例。
在下面评论您更喜欢哪一个以及原因🚀
文章来源:https://dev.to/luistak/cross-micro-frontends-communication-30m3