写了一个字典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 更简单啊!

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

相关推荐
桂月二二4 小时前
探索前端开发中的 Web Vitals —— 提升用户体验的关键技术
前端·ux
hunter2062065 小时前
ubuntu向一个pc主机通过web发送数据,pc端通过工具直接查看收到的数据
linux·前端·ubuntu
qzhqbb5 小时前
web服务器 网站部署的架构
服务器·前端·架构
刻刻帝的海角5 小时前
CSS 颜色
前端·css
浪浪山小白兔6 小时前
HTML5 新表单属性详解
前端·html·html5
lee5767 小时前
npm run dev 时直接打开Chrome浏览器
前端·chrome·npm
2401_897579657 小时前
AI赋能Flutter开发:ScriptEcho助你高效构建跨端应用
前端·人工智能·flutter
limit for me7 小时前
react上增加错误边界 当存在错误时 不会显示白屏
前端·react.js·前端框架
浏览器爱好者7 小时前
如何构建一个简单的React应用?
前端·react.js·前端框架
qq_392794488 小时前
前端缓存策略:强缓存与协商缓存深度剖析
前端·缓存