项目实战:自定义监听器

223 阅读5分钟

前言:

整个项目是基于Antd Pro 来实现的一个中后台管理项目,在实现一个列表搜索查询的功能时具体需求是这样的:列表中保存好搜索的参数,点击某条列表数据详情时跳转到详情页,点击浏览器返回时也要保持上一次的状态,例如:我查看第三页的某条数据详情时再返回列表也要是第三页的数据,点击切换左边菜单栏时要将上一次的列表查询的参数全部清除掉~,我一想这不挺简单的吗?直接在左边菜单栏时点击事件中清除models的数据不就行了(我用的是antd pro 自带的useModels 存储我搜索的参数的),后面我发现左边的菜单栏都是通过路由数组来配置的,然后antd pro 内部获取路由数组然后自动渲染成Menu的,没法加点击事件。。。,

1.png

后面查看官方api文档发现可以在运行时配置有个方法:patchRoutes,可以在这里面对路由中每个菜单项注入一个点击事件去做处理,点击菜单项的时候就调用useModel重置的方法去重置上次列表搜索的参数,但我发现这个根本用不了useModel的方法,因为antd pro 中app.tsx是运行时的一个配置文件,里面是用不了hooks函数的,后面就想到了写一个事件监听,在点击的时候触发一个‘reset-model’,然后再Header组件中监听这个事件然后重置useModel的数据,下面是具体步骤。。。

1、定义我们的Model数据

model我们用了ts去写,主要是用的时候方便有提示,其实useModel写法就跟自定义hooks基本一致的,我也在里面具体也是用了一个自己自定义的useReducer,还有就是三个方法:更新、重置、重置所有 ,下面是具体代码,一些初始值的ts类型定义我就不放出来了,主要是看下自定义的一个reducer。

2.png

import { useReducer } from "react";
import {
  initJinJianParams,
  initialBaobeiParams,
  initJiaofeiParams,
  initTuikuanParams,
  ActionTypes,
  liuchengTreeData,
  initPigaiParams,
  initTuibaoParams,
  initBillAnjianParams,
} from "./initstate";

  interface UpdateQueryParamsAction {
    type: typeof ActionTypes.UpdateQueryParams;
    identifier: keyof CombinedState;
    payload: Partial<JinJianParamsType | BaobeiParamsType | JiaofeiParamsType | TuikuanParamsType | FukuandanParamsType | GuaZhangParamsType>;
  }

  interface ResetQueryParamsAction {
    type: typeof ActionTypes.ResetQueryParams;
    identifier: keyof CombinedState;
    payload: Partial<JinJianParamsType | BaobeiParamsType | JiaofeiParamsType | TuikuanParamsType | FukuandanParamsType | GuaZhangParamsType>;
  }

  interface ResetAllQueryParamsAction {
    type: typeof ActionTypes.ResetAllParams;
    identifier: keyof CombinedState;
    payload: Partial<JinJianParamsType | BaobeiParamsType | JiaofeiParamsType | TuikuanParamsType | FukuandanParamsType | GuaZhangParamsType>;
  }

  type CombinedActions = UpdateQueryParamsAction | ResetQueryParamsAction | ResetAllQueryParamsAction;
}

interface CombinedState {
  jinJianParams: QUERY.JinJianParamsType;
  baobeiParams: QUERY.BaobeiParamsType;
  jiaofeiParams: QUERY.JiaofeiParamsType;
  tuikuanParams: QUERY.TuikuanParamsType;
  pigaiParams: QUERY.PigaiParamsType;
  tuibaoParams: QUERY.TuibaoParamsType;
  billAnjianParams: QUERY.BillAnjianParamsType;
}

const initialState: CombinedState = {
  jinJianParams: initJinJianParams,
  baobeiParams: initialBaobeiParams,
  jiaofeiParams: initJiaofeiParams,
  tuikuanParams: initTuikuanParams,
  pigaiParams: initPigaiParams,
  tuibaoParams: initTuibaoParams,
  billAnjianParams: initBillAnjianParams,
};

// 自定义reducer
function combinedReducer(state: CombinedState, action: QUERY.CombinedActions): CombinedState {
  switch (action.type) {
    case ActionTypes.UpdateQueryParams:
      return {
        ...state,
        [action.identifier]: {
          ...state[action.identifier],
          ...action.payload,
        },
      };

    case ActionTypes.ResetQueryParams:
      return {
        ...state,
        [action.identifier]: action.payload,
      };
    case ActionTypes.ResetAllParams:
      return initialState;

    default:
      return state;
  }
}

export default () => {
  const [state, dispatch] = useReducer(combinedReducer, initialState);

  // 更新参数
  const updateQueryParams = <T extends keyof CombinedState>(identifier: T, newParams: Partial<CombinedState[T]>) => {
    dispatch({
      type: ActionTypes.UpdateQueryParams,
      identifier,
      payload: newParams,
    });
  };

  // 重置
  const resetQueryParams = <T extends keyof CombinedState>(identifier: T, newParams: Partial<CombinedState[T]>) => {
    dispatch({
      type: ActionTypes.ResetQueryParams,
      identifier,
      payload: newParams,
    });
  };

  // 重置所有的参数
  const resetAllQueryParams = () => {
    dispatch({
      type: ActionTypes.ResetAllParams,
      identifier: "jinJianParams",
      payload: {},
    });
  };

  return {
    state,
    initJinJianParams,
    initJiaofeiParams,
    initialBaobeiParams,
    initTuikuanParams,
    initPigaiParams,
    initTuibaoParams,
    liuchengTreeData,
    updateQueryParams,
    resetQueryParams,
    resetAllQueryParams,
    initBillAnjianParams,
  };
};

在list组件中我们这样引入就行了,所有的数据都存在state中了,使用的时候可以从state中取出来:state.jinJianParams;

const { initJinJianParams, state, liuchengTreeData, updateQueryParams, resetQueryParams } =  useModel("list.ywlist");

2、自定义事件监听器

因为每次点击菜单栏时都会触发一个事件监听,所以我们监听的时候提供一个方法来绑定只执行一次的事件监听器,首先我们在运行时配置文件app.tsx中的patchRoutes方法中菜单栏点击触发事件:

export function patchRoutes({ routes }: { routes: IRoute[] }): void {
  routes.forEach(function addTitleClick(route: IRoute) {
    if (route.routes) {
      route.onTitleClick = ({ key }: { key: string }): void => {
        // 处理菜单项点击事件
        if(key != '' && key != undefined){
          eventBus.emit('reset-modal');
        }
      };
      route.routes.forEach(addTitleClick);
    }
  });
}

3.png

在header组件中监听这个事件:

4.png

自定义监听代码:

// 定义一个事件监听器类型,它接受任意数量的参数
type EventListener = (...args: any[]) => void;

// 定义一个事件总线类
class EventBus {
  // 定义一个私有属性,用于存储事件及其对应的监听器数组
  private events: Map<string, EventListener[]>;

  constructor() {
    // 在构造函数中初始化事件存储为一个空的Map
    this.events = new Map();
  }

  // 提供一个方法来绑定事件监听器
  public on(event: string, listener: EventListener): void {
    // 先尝试获取已经注册的监听器数组,如果没有则使用空数组
    const listeners = this.events.get(event) ?? [];
    // 将新的监听器添加到数组中
    listeners.push(listener);
    // 更新存储中的监听器数组
    this.events.set(event, listeners);
  }

  // 提供一个方法来绑定只执行一次的事件监听器
  public once(event: string, listener: EventListener): void {
    // 定义一个包装器函数,它首先解除绑定,然后执行原始监听器
    const onceWrapper: EventListener = (...args) => {
      this.off(event, onceWrapper);
      listener.apply(this, args);
    };

    // 使用包装器函数作为监听器,绑定到事件上
    this.on(event, onceWrapper);
  }

  // 提供一个方法来移除某个事件的一个指定监听器
  public off(event: string, listener: EventListener): void {
    // 尝试获取事件对应的监听器数组
    const listeners = this.events.get(event);
    if (listeners) {
      // 找到要移除的监听器在数组中的索引
      const index = listeners.indexOf(listener);
      if (index >= 0) {
        // 移除该监听器
        listeners.splice(index, 1);
        // 如果数组为空,则从存储中移除此事件的记录
        if (listeners.length === 0) {
          this.events.delete(event);
        } else {
          // 更新存储中的监听器数组
          this.events.set(event, listeners);
        }
      }
    }
  }

  // 提供一个方法来触发某个事件,调用所有绑定的监听器,并传递参数
  public emit(event: string, ...args: any[]): void {
    // 获取事件对应的监听器数组
    const listeners = this.events.get(event);
    if (listeners) {
      // 创建监听器数组的一个浅拷贝,以防在执行监听器时修改原数组
      const toCall = listeners.slice();
      // 遍历并执行每个监听器,传递参数
      toCall.forEach(listener => listener.apply(this, args));
    }
  }
}

// 创建事件总线的一个实例
const eventBus = new EventBus();
// 导出这个实例,以便在其他文件中使用
export default eventBus;

总结:

后面应用到项目上暂时没发现什么问题,不过我觉得这个场景还是比较少见的,应该大部分都用不到这种,自己记录一下,免得下次用到又忘记了