前言
Hooks是React等函數(shù)式編程框架中非常受歡迎的工具,隨著VUE3 Composition API 函數(shù)式編程風(fēng)格的推出,現(xiàn)在也受到越來越多VUE3開發(fā)者的青睞,它讓開發(fā)者的代碼具有更高的復(fù)用度且更加清晰、易于維護。
本文將通過CRMEB商城商品詳情sku選擇功能了解Hooks的使用基礎(chǔ)以及自定義HOOK開發(fā)相關(guān)的要點,快速入門。
Hook簡介
1.什么是hook
Hooks并不是VUE特有的概念,實際上它原本被用于指代一些特定時間點會觸發(fā)的勾子。而在React16之后,它被賦予了新的意義:
一系列以 use 作為開頭的方法,它們提供了讓你可以完全避開 class式寫法,在函數(shù)式組件中完成生命周期、狀態(tài)管理、邏輯復(fù)用等幾乎全部組件開發(fā)工作的能力
在VUE3中,Hooks的概念結(jié)合了VUE的響應(yīng)式系統(tǒng),被稱為組合函數(shù)。組合函數(shù)是VUE3組合式API中提供的新的邏輯復(fù)用的方案,是一類利用 Vue 的組合式 API 來封裝和復(fù)用有狀態(tài)邏輯的函數(shù),簡單來說,它就是一個創(chuàng)建工具的工具.
2.Hooks與composition Api
Hooks是一種基于閉包的函數(shù)式編程思維產(chǎn)物,所以通常我們會在函數(shù)式風(fēng)格的框架或組件中使用Hook,比如VUE的組合式API(Composition Api)。Hooks在VUE2所使用的選項式風(fēng)格API中也不是不可以使用,畢竟Hook本質(zhì)只是一個函數(shù),只要hook內(nèi)部所使用的api能夠得到支持,我們可以在任何地方使用它們,只是可能需要額外的支持以及效果沒有函數(shù)式組件中那么好,因為仍會被選項分割。
VUE3推出時為開發(fā)者帶來了全新的Composition API即組合式API。它是一種通過函數(shù)來描述組件邏輯的開發(fā)模式。組合式API為開發(fā)者帶來了更好的邏輯復(fù)用能力,通過組合函數(shù)來實現(xiàn)更加簡潔高效的邏輯復(fù)用。
為什么要使用Hooks
在以往VUE2的選項式API中,主要通過Mixin或是Class繼承來實現(xiàn)邏輯復(fù)用,但這種方式有三個明顯的短板:
1.不清晰的數(shù)據(jù)來源:當使用了多個mixin/class時,哪個數(shù)據(jù)是哪個模塊提供的將變得難以追尋,這將提高維護難度
2.命名空間沖突:來自多個class/mixin的開發(fā)者可能會注冊同樣的屬性名,造成沖突
3.隱性的跨模塊交流:不同的mixin/class之間可能存在某種相互作用,產(chǎn)生未知的后果
以上三種主要的缺點導(dǎo)致在大型項目的開發(fā)中,多mixin/class的組合將導(dǎo)致邏輯的混亂以及維護難度的提升,因而在VUE3的官方文檔中不再繼續(xù)推薦使用,保留mixin也只是為了遷移的需求或方便VUE2用戶熟悉。
mixin的缺點其實就是Hooks的優(yōu)點:
1.清晰一目了然的源頭
2.沒有命名沖突的問題
3.精簡邏輯
怎么開始玩Hooks
Hooks的各類規(guī)范
1.通常來講,一個Hook的命名需要以use開頭,比如useTimeOut,這是約定俗成的,開發(fā)者看到useXXX即可明白這是一個Hook。Hook的名稱需要清楚地表明其功能。
2.只在當前關(guān)注的最頂級作用域使用Hook,而不要在嵌套函數(shù)、循環(huán)中調(diào)用Hook
3.函數(shù)必須是純函數(shù),沒有副作用
4.返回值是一個函數(shù)或數(shù)據(jù),供外部使用
5.Hook內(nèi)部可以使用其他的Hook,組合功能
6.數(shù)據(jù)必須依賴于輸入,不依賴于外部狀態(tài),保持數(shù)據(jù)流的明確性
7.在Hook內(nèi)部處理錯誤,不要把錯誤拋出到外部,否則會增加hook的使用成本
8.Hook是單一功能的,不要給一個Hook設(shè)計過多功能。單個Hook只負責(zé)做一件事,復(fù)雜的功能可以使用多個Hook互相組合實現(xiàn),如果給單個Hook增加過多功能,又會陷入過于臃腫、使用成本高、難維護的問題中
下面通過一個簡單的hooks感受一下它的魅力:
這是一個控制頁面彈窗或者抽屜顯示或隱藏的hook,在以往vue2中,我們實現(xiàn)這樣一個功能,需要在data中定義一個變量,在methods中大概率會寫兩個方法分別控制彈窗的顯示和隱藏,如果頁面有多個這樣的顯隱組件,我們的代碼簡直是災(zāi)難,糟糕的事,我們的代碼中這樣的案例實在是太多了,有了hooks就完全不一樣了.
這是一個useBoolean的hooks,可以看到它拋出了一個響應(yīng)式的布爾值和四個方法.在使用的組件內(nèi)就可以多次使用該方法,從而簡化代碼
import { ref } from 'vue';
/**
* boolean組合式函數(shù)
* @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,
};
}
通過這個例子發(fā)現(xiàn),我們在vue2中大概率要寫6個方法和定義三個變量的工作在vue3配合Hooks的情況下,三行代碼就實現(xiàn)了.
下面進入我們本文的重點,通過hooks的方式實現(xiàn)sku選擇器的功能.
在CRMEB各個項目中,加購功能并不是只有在商品詳情頁使用,還有很多頁面也有使用,比如商品分類的幾個模板,購物車頁面,搭配購等,都會需要到打開sku選擇商品規(guī)格的功能,改功能包含選擇商品規(guī)格,價格,庫存,規(guī)格圖跟隨切換實時變化,還有加購數(shù)量的操作,對庫存為0的規(guī)格做不可操作的限制等等,所以這段代碼在前端是非常臃腫龐大的一部分代碼,牽扯的業(yè)務(wù)復(fù)雜,功能廣泛,若是在需要的組件內(nèi)每次復(fù)制粘貼,代碼量就會非常龐大,所以若是可以將這部分功能單獨抽離出來整理為一個可調(diào)用的方法就非常適合我們的使用場景.
先截圖看看以前vue2的方式書寫的該段代碼.
下面是我用vue3+ts+hooks的方式實現(xiàn)一下,代碼如下:
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選擇器傳遞的數(shù)據(jù)
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選擇器內(nèi)商品規(guī)格選擇的Hook,在使用時只需傳入該商品的詳情數(shù)據(jù)以及一些配置項即可快默認選中,節(jié)省了大量重復(fù)的控制代碼,使用該Hook后只需調(diào)用useSkuSelect即可實現(xiàn)規(guī)格的切換,加購數(shù)量的控制等等,且繼承原接口的類型.因為本人其實也是hooks小白,處于學(xué)習(xí)階段,書寫的該hook和ts代碼有可能并不規(guī)范,歡迎讀者交流指正.
總結(jié)
Hooks是VUE3中利用組合式API響應(yīng)式的特性的,實現(xiàn)簡單高效的邏輯復(fù)用、提高開發(fā)效率、提高VUE模塊可維護性的工具。Hooks的組合可以讓組件低代價、高效率地實現(xiàn)高復(fù)雜度業(yè)務(wù),Hooks之間通常相互獨立,沒有過度耦合,降低后期陷入維護地獄的風(fēng)險,而且可以使得功能模塊更加易于測試.使用開源的Hook將為開發(fā)帶來很多方便,而開發(fā)自定義Hook則需要花費一些時間,但在實現(xiàn)后,高度的定制化將為項目開發(fā)帶來巨大的便利.Hooks的出現(xiàn)不意味著拋棄Class,Hooks也有自己的缺點比如內(nèi)存泄漏和可能的性能問題。Class更加易于上手,在經(jīng)驗豐富、技術(shù)深厚的開發(fā)者手中也可以一定程度上避開Class的缺點