前言
Hooks是React等函数式编程框架中非常受欢迎的工具,随着VUE3 Composition API 函数式编程风格的推出,现在也受到越来越多VUE3开发者的青睐,它让开发者的代码具有更高的复用度且更加清晰、易于维护。
本文将通过CRMEB商城商品详情sku选择功能了解Hooks的使用基础以及自定义HOOK开发相关的要点,快速入门。
Hook简介
1.什么是hook
Hooks并不是VUE特有的概念,实际上它原本被用于指代一些特定时间点会触发的勾子。而在React16之后,它被赋予了新的意义:
一系列以 use 作为开头的方法,它们提供了让你可以完全避开 class式写法,在函数式组件中完成生命周期、状态管理、逻辑复用等几乎全部组件开发工作的能力
在VUE3中,Hooks的概念结合了VUE的响应式系统,被称为组合函数。组合函数是VUE3组合式API中提供的新的逻辑复用的方案,是一类利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数,简单来说,它就是一个创建工具的工具.
2.Hooks与composition Api
Hooks是一种基于闭包的函数式编程思维产物,所以通常我们会在函数式风格的框架或组件中使用Hook,比如VUE的组合式API(Composition Api)。Hooks在VUE2所使用的选项式风格API中也不是不可以使用,毕竟Hook本质只是一个函数,只要hook内部所使用的api能够得到支持,我们可以在任何地方使用它们,只是可能需要额外的支持以及效果没有函数式组件中那么好,因为仍会被选项分割。
VUE3推出时为开发者带来了全新的Composition API即组合式API。它是一种通过函数来描述组件逻辑的开发模式。组合式API为开发者带来了更好的逻辑复用能力,通过组合函数来实现更加简洁高效的逻辑复用。
为什么要使用Hooks
在以往VUE2的选项式API中,主要通过Mixin或是Class继承来实现逻辑复用,但这种方式有三个明显的短板:
1.不清晰的数据来源:当使用了多个mixin/class时,哪个数据是哪个模块提供的将变得难以追寻,这将提高维护难度
2.命名空间冲突:来自多个class/mixin的开发者可能会注册同样的属性名,造成冲突
3.隐性的跨模块交流:不同的mixin/class之间可能存在某种相互作用,产生未知的后果
以上三种主要的缺点导致在大型项目的开发中,多mixin/class的组合将导致逻辑的混乱以及维护难度的提升,因而在VUE3的官方文档中不再继续推荐使用,保留mixin也只是为了迁移的需求或方便VUE2用户熟悉。
mixin的缺点其实就是Hooks的优点:
1.清晰一目了然的源头
2.没有命名冲突的问题
3.精简逻辑
怎么开始玩Hooks
Hooks的各类规范
1.通常来讲,一个Hook的命名需要以use开头,比如useTimeOut,这是约定俗成的,开发者看到useXXX即可明白这是一个Hook。Hook的名称需要清楚地表明其功能。
2.只在当前关注的最顶级作用域使用Hook,而不要在嵌套函数、循环中调用Hook
3.函数必须是纯函数,没有副作用
4.返回值是一个函数或数据,供外部使用
5.Hook内部可以使用其他的Hook,组合功能
6.数据必须依赖于输入,不依赖于外部状态,保持数据流的明确性
7.在Hook内部处理错误,不要把错误抛出到外部,否则会增加hook的使用成本
8.Hook是单一功能的,不要给一个Hook设计过多功能。单个Hook只负责做一件事,复杂的功能可以使用多个Hook互相组合实现,如果给单个Hook增加过多功能,又会陷入过于臃肿、使用成本高、难维护的问题中
下面通过一个简单的hooks感受一下它的魅力:
这是一个控制页面弹窗或者抽屉显示或隐藏的hook,在以往vue2中,我们实现这样一个功能,需要在data中定义一个变量,在methods中大概率会写两个方法分别控制弹窗的显示和隐藏,如果页面有多个这样的显隐组件,我们的代码简直是灾难,糟糕的事,我们的代码中这样的案例实在是太多了,有了hooks就完全不一样了.
这是一个useBoolean的hooks,可以看到它抛出了一个响应式的布尔值和四个方法.在使用的组件内就可以多次使用该方法,从而简化代码
import { ref } from 'vue';
/**
* boolean组合式函数
* @param initValue 初始值
*/
export default function useBoolean(initValue = false) {
const bool = ref(initValue);
function setBool(value: boolean) {
bool.value = value;
}
function setTrue() {
setBool(true);
}
function setFalse() {
setBool(false);
}
function toggle() {
setBool(!bool.value);
}
return {
bool,
setBool,
setTrue,
setFalse,
toggle,
};
}
通过这个例子发现,我们在vue2中大概率要写6个方法和定义三个变量的工作在vue3配合Hooks的情况下,三行代码就实现了.
下面进入我们本文的重点,通过hooks的方式实现sku选择器的功能.
在CRMEB各个项目中,加购功能并不是只有在商品详情页使用,还有很多页面也有使用,比如商品分类的几个模板,购物车页面,搭配购等,都会需要到打开sku选择商品规格的功能,改功能包含选择商品规格,价格,库存,规格图跟随切换实时变化,还有加购数量的操作,对库存为0的规格做不可操作的限制等等,所以这段代码在前端是非常臃肿庞大的一部分代码,牵扯的业务复杂,功能广泛,若是在需要的组件内每次复制粘贴,代码量就会非常庞大,所以若是可以将这部分功能单独抽离出来整理为一个可调用的方法就非常适合我们的使用场景.
先截图看看以前vue2的方式书写的该段代码.
下面是我用vue3+ts+hooks的方式实现一下,代码如下:
import { ref, reactive, watch, unref } from 'vue';
import { cloneDeep } from 'lodash-es';
export default function useSkuSelect(productInfo: Product.Details) {
watch(productInfo, () => {
attr.productAttr = cloneDeep(productInfo.productAttr);
DefaultSelect();
});
// 向sku选择器传递的数据
const attr = reactive({
productAttr: [],
productSelect: createDefaultModel(),
});
const attrTxt = ref('请选择');
const attrValue = ref('');
attr.productAttr = productInfo.productAttr;
function DefaultSelect() {
let productAttr = attr.productAttr;
let valueObj: Array = [];
let value: Array = [];
let productValue = productInfo.productValue;
for (const key in productValue) {
if (Object.prototype.hasOwnProperty.call(productValue, key)) {
const element = productValue[key];
if (element.stock > 0) {
valueObj = attr.productAttr.length ? key.split(',') : [];
break;
}
}
}
// 处理已售罄时默认选中第一个
if (!valueObj.length && productAttr.length) {
// value = Object.keys(productValue)[0].split(',');
} else {
value = valueObj;
}
for (let index = 0; index < productAttr.length; index++) {
productAttr[index]!.index = value[index];
}
// 排序
type selectPro = Pick;
let productSelect: selectPro = productValue[value.join(',')];
if (productSelect && productAttr.length) {
attr.productSelect = createProductSelect(1, productSelect);
attrValue.value = value.join(',');
attrTxt.value = '已选择';
} else if (!productSelect && productAttr.length) {
attr.productSelect = createProductSelect(2, productSelect);
attrValue.value = '';
attrTxt.value = '请选择';
} else if (!productSelect && !productAttr.length) {
attr.productSelect = createProductSelect(3, productSelect);
attrValue.value = '';
attrTxt.value = '请选择';
}
}
function attrVal(val: Product.AttrVal) {
const { index, indexn } = val;
const attrValue = attr.productAttr[index]!.attr_values[indexn];
attr.productAttr[index]!.index = attrValue;
}
function ChangeAttr(res: any) {
let productSelect = productInfo.productValue[res];
if (productSelect && productSelect.stock >= 0) {
attr.productSelect = createProductSelect(1, productSelect);
attrValue.value = res;
attrTxt.value = '已选择';
} else {
attr.productSelect = createProductSelect(2, productSelect);
attrValue.value = '';
attrTxt.value = '请选择';
}
}
/**
*
* @param type
* true 加
* false 减
*/
function changeCartNum(type: boolean) {
// 获取当前变动属性
let proSelect = productInfo.productValue[unref(attrValue)];
//无属性值即库存为0;不存在加减;
if (!proSelect) return;
let stock = proSelect.stock || 0;
if (attr.productSelect.cart_num) {
if (type) {
attr.productSelect.cart_num++;
if (attr.productSelect.cart_num > stock) {
attr.productSelect.cart_num = stock ? stock : 1;
}
} else {
if (attr.productSelect.cart_num <= 1) {
attr.productSelect.cart_num = 1;
} else {
attr.productSelect.cart_num--;
}
}
}
}
function createProductSelect(type: number, productSelect: any): Product.selectPro {
let proSelect: Product.selectPro = createDefaultModel();
if (type === 1) {
proSelect = {
store_name: productInfo.storeInfo.store_name,
image: productSelect.image,
price: productSelect.price,
stock: productSelect.stock,
unique: productSelect.unique,
cart_num: 1,
vip_price: productSelect.vip_price,
};
} else if (type === 2) {
proSelect = {
store_name: productInfo.storeInfo.store_name,
image: productInfo.storeInfo.image,
price: productInfo.storeInfo.price,
stock: 0,
unique: '',
cart_num: 0,
vip_price: productInfo.storeInfo.vip_price,
};
} else if (type === 3) {
proSelect = {
store_name: productInfo.storeInfo.store_name,
image: productInfo.storeInfo.image,
price: productInfo.storeInfo.price,
stock: productInfo.storeInfo.stock,
unique: '',
cart_num: 1,
vip_price: productInfo.storeInfo.vip_price,
};
}
return proSelect;
}
function createDefaultModel(): Product.selectPro {
return {
store_name: '',
image: '',
price: '',
stock: 0,
vip_price: '',
unique: '',
cart_num: 0,
};
}
return {
ChangeAttr,
attrVal,
changeCartNum,
attrValue,
attrTxt,
attr,
};
}
在使用sku选择器组件的页面上使用:
这是一个管理sku选择器内商品规格选择的Hook,在使用时只需传入该商品的详情数据以及一些配置项即可快默认选中,节省了大量重复的控制代码,使用该Hook后只需调用useSkuSelect即可实现规格的切换,加购数量的控制等等,且继承原接口的类型.因为本人其实也是hooks小白,处于学习阶段,书写的该hook和ts代码有可能并不规范,欢迎读者交流指正.
总结
Hooks是VUE3中利用组合式API响应式的特性的,实现简单高效的逻辑复用、提高开发效率、提高VUE模块可维护性的工具。Hooks的组合可以让组件低代价、高效率地实现高复杂度业务,Hooks之间通常相互独立,没有过度耦合,降低后期陷入维护地狱的风险,而且可以使得功能模块更加易于测试.使用开源的Hook将为开发带来很多方便,而开发自定义Hook则需要花费一些时间,但在实现后,高度的定制化将为项目开发带来巨大的便利.Hooks的出现不意味着抛弃Class,Hooks也有自己的缺点比如内存泄漏和可能的性能问题。Class更加易于上手,在经验丰富、技术深厚的开发者手中也可以一定程度上避开Class的缺点