前言
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的缺點