写了一个字典hook,瞬间让组员开发效率提高20%!!!

1、引言

在项目的开发中,发现有很多地方(如:选择器、数据回显等)都需要用到字典数据,而且一个页面通常需要请求多个字典接口,如果每次打开同一个页面都需要重新去请求相同的数据,不但浪费网络资源、也给开发人员造成一定的工作负担。最近在用 taro + react 开发一个小程序,所以就写一个字典 hook 方便大家开发。

2、实现过程

首先,字典接口返回的数据类型如下图所示:

其次,在没有实现字典 hook 之前,是这样使用 选择器 组件的:

ts 复制代码
  const [unitOptions, setUnitOptions] = useState([])

  useEffect(() => {
    dictAppGetOptionsList(['DEV_TYPE']).then((res: any) => {
      let _data = res.rows.map(item => {
        return {
          label: item.fullName,
          value: item.id
        }
      })
      setUnitOptions(_data)
    })
  }, [])

  const popup = (
    <PickSelect
      defaultValue=""
      open={unitOpen}
      options={unitOptions}
      onCancel={() => setUnitOpen(false)}
      onClose={() => setUnitOpen(false)}
    />
  )

每次都需要在页面组件中请求到字典数据提供给 PickSelect 组件的 options 属性,如果有多个 PickSelect 组件,那就需要请求多次接口,非常麻烦!!!!!

既然字典接口返回的数据格式是一样的,那能不能写一个 hook 接收不同属性,返回不同字典数据呢,而且还能 缓存 请求过的字典数据?

当然是可以的!!!

预想一下如何使用这个字典 hook?

ts 复制代码
const { list } = useDictionary('DEV_TYPE')

const { label } = useDictionary('DEV_TYPE', 1)

const { label } = useDictionary('DEV_TYPE', 1, '、')

从上面代码中可以看到,第一个参数接收字典名称,第二个参数接收字典对应的值,第三个参数接收分隔符,而且后两个参数是可选的,因此根据上面的用法来写我们的字典 hook 代码。

ts 复制代码
interface dictOptionsProps {
  label: string | number;
  value: string | number | boolean | object;
  disabled?: boolean;
}

interface DictResponse {
  value: string;
  list: dictOptionsProps[];
  getDictValue: (value: string) => string
}

let timer = null;
const types: string[] = [];
const dict: Record<string, dictOptionsProps[]> = {}; // 字典缓存

// 因为接收不同参数,很适合用函数重载
function useDictionary(type: string): DictResponse;
function useDictionary(
  type: string | dictOptionsProps[],
  value: number | string | Array<number | string>,
  separator?: string
): DictResponse;
function useDictionary(
  type: string | dictOptionsProps[],
  value?: number | string | Array<string | number>,
  separator = ","
): DictResponse {
    const [options, setOptions] = useState<dictOptionsProps[]>([]); // 字典数组
    const [dictValue, setDictValue] = useState(""); // 字典对应值
    
    const init = () => {
      if (!dict[type] || !dict[type].length) {
        dict[type] = [];

        types.push(type);

        // 当多次调用hook时,获取所有参数,合成数组,再去请求,这样可以避免多次调用接口。
        timer && clearTimeout(timer);
        timer = setTimeout(() => {
          dictAppGetOptionsList(types.slice()).then((res) => {
            for (const key in dictRes.data) {
              const dictList = dictRes.data[key].map((v) => ({
                label: v.description,
                value: v.subtype,
              }));
              dict[type] = dictList
              setOptions(dictList) // 注意这里会有bug,后面有说明的
            }
          });
        }, 10);
      } else {
        typeof type === "string" ? setOptions(dict[type]) : setOptions(type);
      }
    };
    
    // 获取字典对应值的中文名称
    const getLabel = useCallback(
        (value) => {
          if (value === undefined || value === null || !options.length) return "";
          const values = Array.isArray(value) ? value : [value];

          const items = values.map((v) => {
            if (typeof v === "number") v = v.toString();
            return options.find((item) => item.value === v) || { label: value };
          });
          return items.map((v) => v.label).join(separator);
        },
        [options]
     )
    
    useEffect(() => init(), [])
    useEffect(() => setDictValue(getLabel(value)), [options, value])
    
    return { value: dictValue, list: options, getDictValue: getLabel };
}

初步的字典hook已经开发完成,在 Input 组件中添加 dict 属性,去试试看效果如何。

ts 复制代码
export interface IProps extends taroInputProps {
  value?: any;
  dict?: string; // 字典名称
}

const CnInput = ({
  dict,
  value,
  ...props
}: IProps) => {
  const { value: _value } = dict ? useDictionary(dict, value) : { value };
  
  return <Input value={_value} {...props} />
}

添加完成,然后去调用 Input 组件

ts 复制代码
<CnInput
    readonly
    dict="DEV_ACCES_TYPE"
    value={formData?.accesType}
/>

<CnInput
    readonly
    dict="DEV_SOURCE"
    value={formData?.devSource}
/>

没想到,翻车了

会发现,在一个页面组件中,多次调用 Input 组件,只有最后一个 Input 组件才会回显数据

这个bug是怎么出现的呢?原来是 setTimeout 搞的鬼,在 useDictionary hook 中,当多次调用 useDictionary hook 的时候,为了能拿到全部的 type 值,请求一次接口拿到所有字典的数据,就把字典接口放在 setTimeout 里,弄成异步的逻辑。但是每次调用都会清除上一次的 setTimeout,只保存了最后一次调用 useDictionary 的 setTimeout ,所以就会出现上面的bug了。

既然知道问题所在,那就知道怎么去解决了。

解决方案: 因为只有调用 setOptions 才会引起页面刷新,为了不让 setTimeout 清除掉 setOptions,就把 setOptions 添加到一个更新队列中,等字典接口数据回来再去执行更新队列就可以了。

ts 复制代码
let timer = null;
const queue = []; // 更新队列
const types: string[] = [];
const dict: Record<string, dictOptionsProps[]> = {};

function useDictionary2(type: string): DictResponse;
function useDictionary2(
  type: string | dictOptionsProps[],
  value: number | string | Array<number | string>,
  separator?: string
): DictResponse;
function useDictionary2(
  type: string | dictOptionsProps[],
  value?: number | string | Array<string | number>,
  separator = ","
): DictResponse {
  const [options, setOptions] = useState<dictOptionsProps[]>([]);
  const [dictValue, setDictValue] = useState("");

  const getLabel = useCallback(
    (value) => {
      if (value === undefined || value === null || !options.length) return "";
      const values = Array.isArray(value) ? value : [value];

      const items = values.map((v) => {
        if (typeof v === "number") v = v.toString();
        return options.find((item) => item.value === v) || { label: value };
      });
      return items.map((v) => v.label).join(separator);
    },
    [options]
  );

  const init = () => {
    if (typeof type === "string") {
      if (!dict[type] || !dict[type].length) {
        dict[type] = [];

        const item = {
          key: type,
          exeFunc: () => {
            if (typeof type === "string") {
              setOptions(dict[type]);
            } else {
              setOptions(type);
            }
          },
        };
        queue.push(item); // 把 setOptions 推到 更新队列(queue)中

        types.push(type);

        timer && clearTimeout(timer);
        timer = setTimeout(async () => {
          const params = types.slice();

          types.length = 0;

          try {
              let dictRes = await dictAppGetOptionsList(params);
              for (const key in dictRes.data) {
                dict[key] = dictRes.data[key].map((v) => ({
                  label: v.description,
                  value: v.subtype,
                }));
              }
              
              queue.forEach((item) => item.exeFunc()); // 接口回来了再执行更新队列
              queue.length = 0; // 清空更新队列
          } catch (error) {
            queue.length = 0;
          }
        }, 10);
      } else {
        typeof type === "string" ? setOptions(dict[type]) : setOptions(type);
      }
    }
  };

  useEffect(() => init(), []);

  useEffect(() => setDictValue(getLabel(value)), [options, value]);

  return { value: dictValue, list: options, getDictValue: getLabel };
}

export default useDictionary;

修复完成,再去试试看~

不错不错,已经修复,嘿嘿~

这样就可以愉快的使用 字典 hook 啦,去改造一下 PickSelect 组件

ts 复制代码
export interface IProps extends PickerProps {
  open: boolean;
  dict?: string;
  options?: dictOptionsProps[];
  onClose: () => void;
}
const Base = ({
  dict,
  open = false,
  options = [],
  onClose = () => { },
  ...props
}: Partial<IProps>) => {
  // 如果不传 dict ,就拿 options 
  const { list: _options } = dict ? useDictionary(dict) : { list: options };

  return <Picker.Column>
          {_options.map((item) => {
            return (
              <Picker.Option
                value={item.value}
                key={item.value as string | number}
              >
                {item.label}
              </Picker.Option>
            );
          })}
        </Picker.Column>

在页面组件调用 PickSelect 组件

效果:

这样就只需要传入 dict 值,就可以轻轻松松获取到字典数据啦。不用再手动去调用字典接口啦,省下来的时间又可以愉快的摸鱼咯,哈哈哈

最近也在写 vue3 的项目,用 vue3 也实现一个吧。

ts 复制代码
// 定时器
let timer = 0
const timeout = 10
// 字典类型缓存
const types: string[] = []
// 响应式的字典对象
const dict: Record<string, Ref<CnPage.OptionProps[]>> = {}

// 请求字典选项
function useDictionary(type: string): Ref<CnPage.OptionProps[]>
// 解析字典选项,可以传入已有字典解析
function useDictionary(
  type: string | CnPage.OptionProps[],
  value: number | string | Array<number | string>,
  separator?: string
): ComputedRef<string>
function useDictionary(
  type: string | CnPage.OptionProps[],
  value?: number | string | Array<number | string>,
  separator = ','
): Ref<CnPage.OptionProps[]> | ComputedRef<string> {
  // 请求接口,获取字典
  if (typeof type === 'string') {
    if (!dict[type]) {
      dict[type] = ref<CnPage.OptionProps[]>([])

      if (type === 'UNIT_LIST') {
        // 单位列表调单独接口获取
        getUnitListDict()
      } else if (type === 'UNIT_TYPE') {
        // 单位类型调单独接口获取
        getUnitTypeDict()
      } else {
        types.push(type)
      }
    }

    // 等一下,人齐了才发车
    timer && clearTimeout(timer)
    timer = setTimeout(() => {
      if (types.length === 0) return
      const newTypes = types.slice()
      types.length = 0
      getDictionary(newTypes).then((res) => {
        for (const key in res.data) {
          dict[key].value = res.data[key].map((v) => ({
            label: v.description,
            value: v.subtype
          }))
        }
      })
    }, timeout)
  }

  const options = typeof type === 'string' ? dict[type] : ref(type)
  const label = computed(() => {
    if (value === undefined || value === null) return ''
    const values = Array.isArray(value) ? value : [value]
    const items = values.map(
      (value) => {
        if (typeof value === 'number') value = value.toString()
        return options.value.find((v) => v.value === value) || { label: value }
      }
    )
    return items.map((v) => v.label).join(separator)
  })

  return value === undefined ? options : label
}

export default useDictionary

感觉 vue3 更简单啊!

到此结束!如果有错误,欢迎大佬指正~

相关推荐
Fan_web14 分钟前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
安冬的码畜日常16 分钟前
【CSS in Depth 2 精译_044】第七章 响应式设计概述
前端·css·css3·html5·响应式设计·响应式
莹雨潇潇1 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
Jiaberrr1 小时前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
Tiffany_Ho2 小时前
【TypeScript】知识点梳理(三)
前端·typescript
安冬的码畜日常3 小时前
【D3.js in Action 3 精译_029】3.5 给 D3 条形图加注图表标签(上)
开发语言·前端·javascript·信息可视化·数据可视化·d3.js
小白学习日记4 小时前
【复习】HTML常用标签<table>
前端·html
丁总学Java4 小时前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js
yanlele4 小时前
前瞻 - 盘点 ES2025 已经定稿的语法规范
前端·javascript·代码规范