react: harmony-events + use-command 组件模式
一种原创的 react 组件开发模式
Why
目的: 为了实现业务对通用组件内部方法的
命令式
调用。
Why not ref
- 耦合度高了,而且,涉及多组件跨页面调用,ref 就不方便了
- 天然的状态管理
核心思想
- 完全解耦合:组件没了,也只是消息没有处理,不会报错
- 命令式调用:天然的状态处理
- 孤岛: 每个组件是一个孤岛,配合由组件名来统一调度,由 name 来识别同一类型,不同的组件
- 适用: 比较复杂的“黑盒子”组件
- 设计模式: 简化版命令模式?
实现步骤
- 定义类组件
- 注册 harmony-events
- 实现 use-command 的 hook
架构图
类组件
因为我自己的通用组件,都使用类组件来完成,所以,我只实现这个版本。
import { Component } from 'react';
import type { EventMittNamespace } from '@jswork/event-mitt';
import { ReactHarmonyEvents } from '@jswork/harmony-events';
class ReactInteractiveList extends Component {
private harmonyEvents: ReactHarmonyEvents | null = null;
static event: EventMittNamespace.EventMitt;
static events = ["add", "remove", "set"];
static defaultProps = {
name: "@",
};
constructor(props) {
super(props);
this.harmonyEvents = ReactHarmonyEvents.create(this);
}
/* ----- public eventBus methods ----- */
add = () => {};
remove = (inIndex: number) => {};
set = (items: any[]) => {};
componentWillUnmount() {
this.harmonyEvents?.destroy();
}
render() {
// todo ...
}
}
export default ReactInteractiveList;
use-command 部分
方便业务进行命令式调用,只考虑业务为 FC 组件。
注意,这个
RcComponent.event
实际上是在 constructor 阶段注册的,所以,并依赖于 useEffect/useState 等,其实并不是 hook,比 hook 使用更加的自由。
import RcComponent from '.';
const useCommand = (inName?: string) => {
const name = inName || '@';
const execute = (command: string, ...args: any[]) =>
RcComponent.event.emit(`${name}:${command}`, ...args);
// the command repository:
const add = () => execute('add');
const remove = (index: number) => execute('remove', index);
const set = (items: any[]) => execute('set', items);
return {
add,
remove,
set
};
};
export default useCommand;
业务上使用
使用特别简单,因为有可能同样的组件有多个,所以需要 useCommand 传入 name
function App() {
const [items, setItems] = useState(messages);
const ref1 = useRef(null);
const { add, remove, clear, up, down, top, bottom } = useCommand('i1');
const template = ({ item, index }) => {
const idx = index + 1;
return (
<div className="bg-gray-100 p-2 rounded-md hover:bg-gray-300 transition-all cursor-pointer" key={item.id}>
<nav className="x-2">
<button className="btn2" onClick={() => top(index)}>ToTop</button>
<button className="btn2" onClick={() => bottom(index)}>ToBottom</button>
<button className="btn1" onClick={() => remove(index)}>DELETE</button>
<button className="btn1" disabled={index === 0} onClick={() => up(index)}>Up</button>
<button className="btn1" disabled={index === items.length - 1} onClick={() => down(index)}>Down</button>
</nav>
<span>{idx}.{item.message}</span>
</div>
);
};
return (
<ReactInteractiveListUI
name="i1"
ref={ref1}
listProps={{ className: "react-list-x", as: "section" }}
value={items}
template={template}
defaults={defaults}
onChange={handleChange}
/>
)
}