Vue Coder拥抱Hooks铲屎

270 阅读3分钟

前言

2019年6月,尤雨溪在 vue/issue里提出了关于 vue3 composittion API 的提案, vue3+版本中已具备类似react hook的能力。vuejs/composition-api是一个提供组合式API的vue2插件,它能帮助开发者在vue2版本中体验到vue3+版本的composittion API

本文分享的是笔者维护公司vue2+ 版本项目中,使用hook思想解耦代码,提高代码可复用性、可读性的实践

未优化的代码

先说明下产品想要的效果:

说到产品就狠得心痒痒😩

由于公司项目,不便透露。这里使用Vant Cascader组件dome演示下。

需求:级联选择部门、人员,人员选择有关键字搜索功能。

需求麻烦点:人员列表数据量多,后端做了分页请求,但Vant Cascader组件并没有提供像滚动加载Vant List的功能,所以Vant Cascader不能直接用,级联选择需要笔者自己去实现。

整体代码量多,这里只粘贴出script 标签内代码:

<template>...</template>
<script>
import isZeroLen from "medash/lib/isZeroLen";
import { ZERO } from "@/tool/constant";
import Toast from "@/tool/utils";
import {
  defineComponent,
  reactive,
  ref,
  toRefs,
  watch,
  watchEffect,
} from "@vue/composition-api";
import isEmpty from "medash/lib/isEmpty";
import { getUserList } from "@/api/getUserList";
import listLoad from "../js/listLoad";
import debounce from "lodash/debounce";
import createUser from "../js/createUser";
import getTitle from "../js/getTitle";
import eq from "medash/lib/eq";
import deleteUser from "../js/deleteUser";

export default defineComponent({
  props: {
    title: {
      type: String,
      default: "",
    },
    lists: {
      type: Array,
      default: () => {
        return [];
      },
    },

    selected: {
      type: Array,
      default: () => [],
    },

    saveValue: {
      type: String,
      default: "",
    },

    endTime: {
      type: Number,
      default: 0,
    },
  },
  setup(props, { emit }) {
    const query = {
      page: 1,
      size: 18,
    };
    const { saveValue, lists, endTime } = toRefs(props);
    const selectValue = ref("");
    const show = ref(false);
    const active = ref("");
    const radioValue = ref(ZERO);
    const extUserId = ref(-1);
    const name = ref("");
    //用于暂存级联数据
    const saveQuery = reactive({
      title1: "",
      title2: "",
      value1: ZERO,
      value2: -1,
    });

    const searchValue = ref("");
    const listLoadingQueryRef = reactive({
      loading: false,
      finished: false,
      lists: createUser(isEmpty(endTime.value)),
      error: false,
    });

    const onLoad = () => {
      query.extOrgId = saveQuery.value1;
      listLoad(getUserList, { listLoadingQueryRef, query });
    };

    const reset = () => {
      query.page = 1;
      listLoadingQueryRef.lists = createUser(isEmpty(endTime.value));
      listLoadingQueryRef.finished = false;
      return onLoad();
    };

    const searchFn = debounce(() => {
      query.name = searchValue.value;
      reset();
    }, 1000);

    const resetShow = () => {
      name.value = "";
      selectValue.value = "";
      radioValue.value = ZERO;
      extUserId.value = ZERO;
    };

    const resetSaveQuery = () => {
      active.value = "tab1";
      saveQuery.value1 = ZERO;
      saveQuery.title1 = "";
      saveQuery.title2 = "";
      saveQuery.value2 = -1;
    };

    const emitFn = () =>
      emit("trigger", {
        extUserId: extUserId.value,
        extOrgId: radioValue.value,
      });

    const selectOrg = (list) => {
      saveQuery.value1 = list.key;
      saveQuery.title1 = list.value;
      saveQuery.title2 = "";
      saveQuery.value2 = -1;
      reset();
      active.value = "tab2";
    };

    const selectUser = (list) => {
      if (isEmpty(list)) {
        resetShow();
        resetSaveQuery();
        emitFn();
        return;
      }

      saveQuery.title2 = getTitle(list);
      saveQuery.value2 = list.extUserId;

      selectValue.value = saveQuery.title1;
      radioValue.value = saveQuery.value1;
      extUserId.value = saveQuery.value2;
      name.value = saveQuery.title2;
      emitFn();
      show.value = false;
    };

    const onClick = () => {
      if (isZeroLen(lists.value)) {
        Toast("error", "未选择厂区或该厂区未配置可呼叫的部门");
        return;
      }
      show.value = true;
    };

    watch(
      () => saveQuery.title1,
      () => {
        searchValue.value = "";
      }
    );

    watch(searchValue, () => {
      searchFn();
    });

    watch(endTime, (value) => {
      if (isEmpty(value)) {
        listLoadingQueryRef.lists.unshift(...createUser(isEmpty(value)));
        return;
      }
      if (eq(extUserId.value, -1) || isEmpty(extUserId.value)) {
        selectUser();
      } else {
        deleteUser(listLoadingQueryRef.lists);
      }
    });

    watchEffect(() => {
      selectValue.value = saveValue.value;
    });

    return {
      eq,
      show,
      active,
      onClick,
      selectValue,
      radioValue,
      isEmpty,
      onLoad,
      selectUser,
      name,
      selectOrg,
      extUserId,
      listLoadingQueryRef,
      searchValue,
      getTitle,
      saveQuery,
    };
  },
});
</script>
<style>...</style>

逻辑全都挤在一个script 标签内,当其他地方也需要相似功能后,基本就是又复制粘贴一份过去。

Hook思维优化

首先,思考要如何拆分功能点?功能最小化抽离。

上述实现的级联选择,它可以拆分以下几个功能点:

  • 人员列表关键字搜索;

  • 选择部门后,切换人员列表选择,交互与Vant Cascader一样;

  • 增加滚动加载Vant List的功能。

功能代码抽离:

  • 抽离人员列表关键字搜索逻辑

useSearch.js:

import { ref, watch } from "@vue/composition-api"
import debounce from "lodash/debounce";
export default (trigger) => {
    const searchValue = ref('')
    const onChange = debounce(() => {
        trigger(searchValue.value)
    }, 1000);
    watch(searchValue, () => {
        onChange()
    })
    return [searchValue,trigger]
}
  • 抽离部门人员级联选择交互逻辑

useCascader.js:

import { reactive, ref } from "@vue/composition-api"
import { ZERO } from "@/tool/constant";
import getTitle from "./getTitle";
import isEmpty from "medash/lib/isEmpty";
import eq from "medash/lib/eq"

export default (finishFn = () => { }, changeActiveFn = () => { }) => {
    //用于暂存级联数据
    const saveQuery = reactive({
        title1: "",
        title2: "",
        value1: ZERO,
        value2: -1,
    });
    const selectValue = ref("");
    const show = ref(false);
    const active = ref("");
    const extUserId = ref(-1);
    const name = ref("");

    //重置第一负责人
    const resetUser = () => {
        name.value = "";
        extUserId.value = ZERO;
    }

    //重置部门&第一负责人
    const resetShow = () => {
        resetUser()
        selectValue.value = "";
    };

    const resetSaveQuery = () => {
        active.value = "tab1";
        saveQuery.value1 = ZERO;
        saveQuery.title1 = "";
        saveQuery.title2 = "";
        saveQuery.value2 = -1;
    };

    const selectOrg = (title, value) => {
        saveQuery.title1 = title;
        saveQuery.value1 = value;
        saveQuery.title2 = "";
        saveQuery.value2 = -1;
        changeActiveFn();
        active.value = "tab2";
    };

    const resetUserFn = () => {
        if (eq(extUserId.value, -1)) {
            resetUser();
            finishFn({ extUserId: ZERO, extOrgId: saveQuery.value1 });
        }
    }

    const selectUser = (value, list) => {
        if (isEmpty(list)) {
            resetShow();
            resetSaveQuery();
            //主责部门&主责第一负责人重置
            finishFn({ extUserId: ZERO, extOrgId: ZERO });
            return;
        }

        saveQuery.title2 = getTitle(list);
        saveQuery.value2 = value;

        selectValue.value = saveQuery.title1;
        extUserId.value = saveQuery.value2;
        name.value = saveQuery.title2
        finishFn({ extUserId: extUserId.value, extOrgId: saveQuery.value1 });
        show.value = false
    };

    return {
        show,
        name,
        active,
        selectValue,
        extUserId,
        saveQuery,
        selectOrg,
        selectUser,
        resetUserFn
    }
}

useList.js:

import { reactive } from "@vue/composition-api";
import isObject from "medash/lib/isObject"
import isEmptyObj from "medash/lib/isEmptyObj"
import or from "medash/lib/or"

export const onLoadList =  async (callBack, { listLoadingQueryRef: listLoadingQueryRef, query }) => {
    listLoadingQueryRef.loading = true
    const errorFn = () => {
        listLoadingQueryRef.error = true
        listLoadingQueryRef.loading = false
    }
    callBack(query).then(result => {
        //边界处理
        if (or(!isObject(result), isEmptyObj(result))) {
            errorFn()
            return
        }
        const { data, hasMore } = result;
        listLoadingQueryRef.lists.push(...data)
        if (hasMore) {
            query.page++
        } else {
            listLoadingQueryRef.finished = true
        }
    }, () => {
        errorFn()
    }).finally(() => {
        listLoadingQueryRef.pullRefresh = false
        listLoadingQueryRef.loading = false
    })
}
export default () => {
    const listLoadingQueryRef = reactive({
        loading: false,
        finished: false,
        lists:[],
        error: false,
    });
    return [listLoadingQueryRef,onLoadList]
}
  • 抽离其他逻辑

未优化代码中,有一段这样的逻辑,它在其他地方也需要用到。我们也把它抽离出去

 watch(endTime, (value) => {
    if (isEmpty(value)) {
      listLoadingQueryRef.lists.unshift(...createUser(isEmpty(value)));
      return;
    }
    if (eq(extUserId.value, -1) || isEmpty(extUserId.value)) {
      selectUser();
    } else {
      deleteUser(listLoadingQueryRef.lists);
    }
  });

watchEndTime.js

import deleteUser from "./deleteUser";
import createUser from "./createUser";
import or from "medash/lib/or"
import isEmpty from "medash/lib/isEmpty";
import eq from "medash/lib/eq";
import isObject from "medash/lib/isObject"
import some from "medash/lib/some"

//是否已含有系统找人  -1值
const isHaveNegativeOne = (lists) => {
    const isEmptyArr = isEmpty(lists)
    if (isEmptyArr) {
        return !isEmptyArr
    }
    const list = lists[0]
    if (isObject(list)) {
        return some(list, (key, value) => {
            return eq(value, -1)
        })
    }
    return false
}

export default (endTime, { lists, extUserId, resetFn }) => {
    if (isEmpty(endTime)) {
        isHaveNegativeOne(lists) ? null : lists.unshift(...createUser(isEmpty(endTime)));
        return;
    }
    if (or(eq(extUserId, -1), isEmpty(extUserId))) {
        resetFn();
    } else {
        deleteUser(lists);
    }
}

好了!可复用的功能均被抽离,现在将它们全部重新组合:

<template>...</template>
<script>
import {
    defineComponent,
    toRefs,
    watch,
    watchEffect,
    onBeforeMount
} from "@vue/composition-api";
import isZeroLen from "medash/lib/isZeroLen"
import Toast from "@/tool/utils";
import isEmpty from "medash/lib/isEmpty";
import { getUserList } from "@/api/getUserList";
import createUser from "./createUser";
import getTitle from "./getTitle";
import eq from "medash/lib/eq";
import watchEndTime from "./watchEndTime"
import useList from "./useList"
import useSearch from "./useSearch";
import useCascader from "./useCascader";

export default defineComponent({
    props: {
        title: {
            type: String,
            default: "",
        },
        lists: {
            type: Array,
            default: () => {
                return [];
            },
        },
        selected: {
            type: Array,
            default: () => [],
        },
        saveValue: {
            type: String,
            default: "",
        },
        endTime: {
            type: Number,
            default: 0,
        },
    },
    setup(props, { emit }) {
        const query = {page: 1,size: 18};
        const { saveValue, lists, endTime } = toRefs(props)
        const cascaderQuery = useCascader((result) => emit("trigger", result), reset)
        const { show, name, active, selectValue, extUserId, saveQuery, selectOrg, selectUser ,resetUserFn} = cascaderQuery
        const [listLoadingQueryRef, onLoadList] = useList()

        function onLoad() {
            query.extOrgId = saveQuery.value1;
            onLoadList(getUserList, { listLoadingQueryRef, query });
        };

        function reset() {
            query.page = 1;
            listLoadingQueryRef.lists = createUser(isEmpty(endTime.value));
            listLoadingQueryRef.finished = false;
            return onLoad();
        };

        const [searchValue] = useSearch((value) => {
            query.name = value;
            reset()
        })

        const onClick = () => {
            if (isZeroLen(lists.value)) {
                Toast("error", "未选择厂区或该厂区未配置可呼叫的部门");
                return;
            }
            show.value = true;
        };

        watch(() => saveQuery.title1, () => searchValue.value = "");

        watch(endTime, (value) => watchEndTime(value, { lists: listLoadingQueryRef.lists, extUserId: extUserId.value, resetFn:resetUserFn }));

        watchEffect(() => selectValue.value = saveValue.value);

        onBeforeMount(() => {
            const isEmptyEndTime = isEmpty(endTime.value)
            if (isEmptyEndTime) {
                listLoadingQueryRef.lists = createUser(isEmptyEndTime)
            }
        })

        return {
            eq,
            show,
            active,
            onClick,
            selectValue,
            isEmpty,
            onLoad,
            selectUser,
            name,
            selectOrg,
            extUserId,
            listLoadingQueryRef,
            searchValue,
            getTitle,
            saveQuery,
        };
    },
});
</script>
<style>...</style>

优化后,script 标签内代码量变少,并且抽离的功能方便给其他地方复用。

最后

原创不易!如果我的文章对你有帮助,你的👍就是对我的最大支持^_^。

参考

浅谈:为啥vue和react都选择了Hooks🏂?