大多数情况下,开发微前端的通常任务实际上是将现有的单体式 React 应用程序转换为其模块化形式,从而使这些部分可以独立使用。
问题在于这里,我们不能指望消费者依赖我们的框架选择,这限制了我们的选择范围,只能使用JavaScript自带的功能。
本文提出了一种使用消息队列的解决方案,也称为事件驱动方式,用于在不同微前端组件之间传输数据。
如果只想了解架构,可以直接跳到示例部分
十条原则这里的任务是开发一个让各组件能够相互沟通的“共同语言”。
但是——有个条件,我们希望尽可能地优化,减少容器层级的重新渲染的次数(我们所说的容器是指容纳所有微前端的组件)。
因此,将容器作为集中式数据存储使用是不可能的。一个被过度使用的架构模式。任何子组件中的分发或突变至少会触发每个子组件的重新渲染过程(需要注意的是,重新渲染并不等同于重新绘制,特别是在基于 React 的应用程序中,虽然前者比后者更省资源,但仍然是不必要的资源消耗)。
我们希望这种通信模式能够扩展,因此如果某个组件需要增加更多的“共享”数据输出,应该能够以尽可能小的影响范围来实现这一点。
这意味着我们要尽可能地减少对无关组件的代码改动。因此,任何基于 prop 的解决方案,尤其是容器持有状态(无论是 context、useState 等形式)的,都不适合,因为任何子组件的更改,开发人员都需要添加额外的回调,修改全局状态定义,或向其他组件添加额外的 prop。后者尤其不利,因为这些 prop 的列表可能会不断增长,使得维护变得越来越复杂。
解决办法自定义事件(CustomEvent) 来救场!
JavaScript 执行顺序及 CustomEvent (自定义事件)如果你想直接了解架构,可以直接跳到示例部分
- 创建自定义事件对象
我们定义一个自定义事件如下所示
new CustomEvent('eventName', { detail: 自定义数据对象 })
执行时:
- 调用了 CustomEvent
构造器。
- 创建了一个新的 CustomEvent
对象,使用指定的事件类型(eventName)和自定义数据(detail)。
- 用提供的 customData
初始化了 detail
属性。
2. 触发事件
- 当调用
dispatchEvent
方法时,它会在目标元素或窗口上触发自定义事件。 - 浏览器的事件系统会接管并处理这个分发过程
document分派了自定义事件对象
3. 事件传播:
捕获阶段:
- 事件从DOM的根节点开始,向下传播到目标元素。
- 设置为
{ capture: true }
的事件监听器会在这一阶段被触发。
目标阶段:
事件到达目标元素后,触发了该元素上直接注册的所有事件监听器。
泡沫阶段:
- 事件从目标元素向上冒泡到 DOM 树的根元素。
- 在此阶段,设置为
{ capture: false }
的事件监听器会被触发。
document.addEventListener('myEvent', (event) => {
console.log(event.detail); // 输出:{ key: 'value' }
});
// 在接收到'myEvent'事件时,打印出事件详情。
事件管理
- 自定义事件的监听器会按照添加的顺序依次执行。
- 事件对象会被传递给监听器,使它们可以通过
detail
属性获取自定义数据。
活动执行
- JavaScript引擎运行事件监听回调。
- 在事件监听回调中,
event
对象中的detail
属性包含自定义数据。
设想一个简单的 CRUD 应用程序(创建、读取、更新、删除),它包含 3 个组件。
一个纯粹用于调度的动作栏,不需要关注任何变化,但会发起变化。
我们还有一个组件,它是一个纯粹的监听者,它会“响应”任何事件。
最后我们终于得到了这张表,它不仅能生成事件,还能监听来自操作栏的事件。
心智模型——
软件架构本质上是一种关于逻辑如何布局和内部如何交互的思维模型。在我们这个生态系统中,一些人熟悉的方法是基于动作-调度-减少器的方法。
我们可以依靠这个经过测试的模型,然后为我们的测试应用搭建一个框架。
我们首先编写一系列可以执行的‘动作’——
注意:这些操作是你的微前端模块之间的“协议”。协议的性质要求它必须对所有相关方可见,因此在定义这些操作的位置时请特别注意这一点(它可能存在于一个单独的小型共享包里,所有微前端都使用这个包)。
export const MFE_ACTIONS = {
DELETE_ROW: "DELETE_ROW",
UPDATE_ROW: "UPDATE_ROW",
...
}
因为我们在这里没有传统意义上的 reducers,我们定义了一些返回该类型对象的函数
type 事件对象 = {
类型: 字符串;
选项?: {
详细信息?: 自定义事件的详细信息
}
}
那么每个函数都有它特定的任务:例如:
const deleteRow = (recordId: number) => {
return new CustomEvent({
type: MFE_ACTIONS.DELETE_ROW,
选项: { detail: { recordId: recordId }, bubbles: true, cancelable: true }
})
}
...
const updatedRows = (rows: Rows[]) => {
return new CustomEvent({
type: MFE_ACTIONS.UPDATE_ROWS,
detail: { rows: rows },
bubbles: true,
cancelable: true
})
}
这有助于我们定义一个协议,要求每个微前端在删除一条记录时输入一个 recordId
。
让我们看看调用栈的实际操作。
在我们之前的例子中,比如说某人点击了 t1 操作,这会删除所在行的内容。
点击这里时,该组件触发一个事件
const MyCustomTableComponent = () => {
function deleteARecord(recordId: number) {
const deleteRowEvent = deleteRow(recordId); //之前的删除函数
window.dispatchEvent(deleteRowEvent);
}
return (
<React.Fragment>
//一些JSX代码
</React.Fragment>
);
}
从消费者的角度来看,我们可以设置一个 API 监听器,它会监听并响应任何删除事件。
//useAPIListener.ts
function useAPIListener() {
useEffect(() => {
const listener = async ({ detail }) => {
const updatedRecords = await makeAPICallToDeleteRecord(detail.recordId);
window.dispatchEvent(updatedRows(updatedRecords));
};
window.addEventListener(MFE_ACTIONS.DELETE_ROW, listener);
return () => {
window.removeEventListener(MFE_ACTIONS.DELETE_ROW, listener);
};
}, []);
}
这样做是,当监听到删除行的事件时,它会从后端调用API删除该行,然后请求新的行,并发出事件通知大家行的变化。
现在我们的图表可以开始监听这个事件 ‘MFE_ACTIONS.UPDATE_ROWS’(多前端操作更新行事件),然后根据需要更新数据。
这将是端到端数据流传输过程的样子。
一个不常被提及的优点——测试!呢!当我们的所有组件都仅仅依赖于事件时,这些事件可以在任何测试框架中简单生成,甚至可以使用像 Jest 这样的单元测试框架来检查内部是否正常工作,因为我们只需要生成一个事件就够了!
如果所有组件都经过充分测试,那么
- 它们仅发出合法的事件
- 这些系统消费与其相应载荷类型匹配的事件
那么我们就可以成功地测试组件对所有可能突变的反应。
这变得非常方便,因为不需要单独为诸如 QueryClient 包装器之类的组件编写测试代码,也不需要模拟正确的 API 响应或模拟存储——所有这些工作都由事件触发,只需测试 useAPIListener 是否正确处理了 API 调用,大大降低了测试的惰性。
还可以玩玩的其他东西……
一个事件也具有 cancelable
, composed
, bubbles
。这三个可以调整和使用,以便进一步提供对我们这里观察者模式的细粒度的访问。
查看文档以了解更多详情。