本文是《前端大屏原理系列》第三篇:多组件联动。
本系列所有技术点,均经过本人开源项目 react-big-screen 实际应用,欢迎 star (*´▽`)ノノ.
一、效果演示
demo地址:点击访问
二、简介
多组件联动
就是指多个组件相互触发,从而完成某些行为逻辑。需要注意的是两个随机组件之间互不相识,因此每个组件只关心自己能做什么(暴露事件)、以及广播出去自己做了什么(触发事件)。我使用事件发布 - 订阅
模式来管理组件之间的触发逻辑,以此实现对多组件交互的解耦。
// 一个简单的事件发布-订阅模型
class Observer {
private eventMap = {};
// 监听事件
public on(id, callback) {
(this.eventMap[id] ||= []).push(callback);
return () => {
this.remove(id, callback);
}
}
// 触发事件
public notify(id, payload) {
this.eventMap[id]?.forEach?.(callback => {
callback?.(payload);
})
}
// 卸载事件
public remove(id, callback) {
if (!this.eventMap[id]?.length) return;
this.eventMap[id] = this.eventMap[id].filter(cb => {
return cb !== callback;
})
}
}
三、暴露事件
暴露事件
指的是组件告诉外界自己能做什么,主要用于修改组件内部逻辑。
由于组件是运行时动态创建的,所以在组件创建时才开始暴露事件。
3.1 简单实现
在组件渲染时注册内部事件,并在组件卸载时同时取消注册对应事件。
function renderComponentNode ({ componentNode }) {
...
useEffect(() => {
// 暴露事件(注册事件监听)
engine.events.on('[组件id]-[callbackId]', callback)
// 卸载事件
return () => {
engine.events.remove('[组件id]-[callbackId]', callback)
}
}, [])
...
}
3.2 封装运行时hook
前面的例子中,我们需要在每个自定义组件中都要去写一遍注册、卸载事件,并直接修改 engine.events
上的事件绑定。这样在后续的维护中可能存在问题:修改事件id的格式后「发布-订阅模型」无法触发、不再使用 engine.events 管理事件等等。所以,我们可以将其封装成一个hook,这样开发自定义组件时就只需要关心如何编写暴露事件本身了。
运行时创建 hook:
// ------------- react-big-screen 源码部分 -------------
// 获取约定格式的事件id
export function getEventId(componentNodeId: string, exposeId: string): string {
return `${componentNodeId}-${exposeId}`;
}
// 创建暴露事件 hook
function createUseExposeHook(componentNodeId: string) {
return function (exposes: Record<string, (payload: any) => void>) {
useEffect(() => {
for (const key in exposes) {
engine.events.on(getEventId(componentNodeId, key), exposes[key]);
}
return () => {
for (const key in exposes) {
engine.events.remove(getEventId(componentNodeId, key), exposes[key]);
}
};
}, [])
};
}
// 运行时生成 hook
function useCreateUseExposeHook(componentNodeId: string) {
return useMemo(() => createUseExposeHook(componentNodeId), []);
}
// ------------- 渲染通用组件模板 -------------
function renderComponentNode ({ componentNode }) {
const Component = ... // 自定义组件模板
...
// 创建运行时 hooks
const useExpose = useCreateUseExposeHook(componentNode?.id);
...
return (
<Component
...
useExpose={useExpose}
...
/>
)
}
使用方式(自定义组件内部暴露事件):
export default createComponent(({ useExpose }) => {
...
// 暴露事件
useExpose({
setValue: (payload) => {
// (内部操作)
// ...
},
refresh: (payload) => {
// (内部操作)
// ...
}
})
...
})
四. 触发事件
触发事件
就是组件向外界广播自己做了什么,一般用来主动发起一个行为逻辑。这个很简单,直接触发事件即可。
4.1 简单实现
engine.events.notify('[目标组件id]-[目标组件callbackId]', payload);
4.2 封装运行时函数
虽然只要触发事件就可以,但是我们怎么知道要触发哪个组件的哪个事件呢?每个组件可以同时触发多个组件,每个组件又有多个事件待触发。所以我们需要在当前组件保存 当前组件 ~ 多个目标组件 ~ 多个暴露事件
的事件关联关系,在运行时找到并触发所有关联的组件,就可以实现多组件联动了。
创建触发函数:
// ------------- react-big-screen 源码部分 -------------
function createHandleTrigger(componentNodeId: string) {
// triggerId:当前组件的触发事件id。
// payload:携带参数。
return function (triggerId: string, payload: any) {
...
// 当前组件
const origin = engine.componentNode.get(componentNodeId);
// 找到所有的待触发目标(因为可能同时触发多个组件,一个target对应一个待触发组件)
const targets = origin?.events?.find?.((event) => {
return event?.triggerId === triggerId;
})?.targets;
// 处理所有关联的待触发目标组件。
targets.forEach((optTarget: ComponentNodeEventTarget) => {
// 待触发的目标组件
const target: ComponentNodeType | undefined = engine.componentNode.get(
optTarget.id
);
if (!target) {
message.error("[bigScreen]: target componentNode not exist.");
return;
}
// 触发目标组件所有待执行的暴露事件。
optTarget.opts.forEach((opt: ComponentNodeEventTargetOpt) => {
// 处理待触发目标组件的事件配置数据(opt)
switch (opt.exposeId) {
// 显隐类型
case INIT_EXPOSES.visible:
handleVisibleOption(opt, target);
break;
// 请求类型
case INIT_EXPOSES.request:
handleRequestOption(opt, target, origin, payload);
break;
// 自定义操作类型
case INIT_EXPOSES.custom:
handleCustomOption(opt, target, origin, payload);
break;
// 【通用处理】自定义组件类型
default:
handleCommonOption(opt, target, origin, payload);
break;
}
});
});
...
};
}
// 创建触发函数
function useCreateHandleTrigger(componentNodeId: string) {
return useMemo(() => createHandleTrigger(componentNodeId), []);
}
// ------------- 渲染自定义组件 -------------
function renderComponentNode ({ componentNode }) {
const Component = ... // 自定义组件模板
...
// 创建运行时 hooks
const handleTrigger = useCreateHandleTrigger(componentNode?.id);
...
return (
<Component
...
handleTrigger={handleTrigger}
...
/>
)
}
使用方式(自定义组件内部触发事件):
export default createComponent(props => {
const { width, height, handleTrigger } = props;
return (
<div style={{ width, height }}>
<a onClick={e => handleTrigger('click', e)}>
点击我呀!
</a>
</div>
)
})
五、总结
我们之所以在运行时封装hook、封装触发函数
,都是为了屏蔽暴露事件、触发事件
的具体实现逻辑。这样后续如果更新事件处理方式时,不会影响所有组件。并且可以提高自定义组件开发时的效率,而不用关心事件注册、卸载以及事件id的命名问题。
【前端大屏原理系列】
react-big-screen 是一个从0到1使用React开发的前端拖拽大屏开源项目。此系列将对大屏的关键技术点一一解析。包含了:拖拽系统实现、自定义组件、收藏夹、快捷键、可撤销历史记录、加载远程组件/本地组件、自适应预览页、布局容器组件、多组件联动(基于事件机制)、成组/取消成组、多子页面切换、i18n国际化语言、鼠标范围框选、... ... 等等。
演示地址:点击访问