react: harmony-events + use-command 组件模式

一种原创的 react 组件开发模式
更新于: 2024-07-22 20:58:26

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}
    />
  )
}