前言
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
}
}
- 抽离滚动加载Vant List逻辑
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 标签内代码量变少,并且抽离的功能方便给其他地方复用。
最后
原创不易!如果我的文章对你有帮助,你的👍就是对我的最大支持^_^。