前言
Hooks是React等函數(shù)式編程框架中非常受歡迎的工具,隨著VUE3 Composition API 函數(shù)式編程風(fēng)格的推出,現(xiàn)在也受到越來越多VUE3開發(fā)者的青睞,它讓開發(fā)者的代碼具有更高的復(fù)用度且更加清晰、易于維護(hù)。
本文將通過CRMEB商城商品詳情sku選擇功能了解Hooks的使用基礎(chǔ)以及自定義HOOK開發(fā)相關(guān)的要點(diǎn),快速入門。
Hook簡(jiǎn)介
1.什么是hook
Hooks并不是VUE特有的概念,實(shí)際上它原本被用于指代一些特定時(shí)間點(diǎn)會(huì)觸發(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ù),簡(jiǎn)單來說,它就是一個(gè)創(chuàng)建工具的工具.
2.Hooks與composition Api
Hooks是一種基于閉包的函數(shù)式編程思維產(chǎn)物,所以通常我們會(huì)在函數(shù)式風(fēng)格的框架或組件中使用Hook,比如VUE的組合式API(Composition Api)。Hooks在VUE2所使用的選項(xiàng)式風(fēng)格API中也不是不可以使用,畢竟Hook本質(zhì)只是一個(gè)函數(shù),只要hook內(nèi)部所使用的api能夠得到支持,我們可以在任何地方使用它們,只是可能需要額外的支持以及效果沒有函數(shù)式組件中那么好,因?yàn)槿詴?huì)被選項(xiàng)分割。
VUE3推出時(shí)為開發(fā)者帶來了全新的Composition API即組合式API。它是一種通過函數(shù)來描述組件邏輯的開發(fā)模式。組合式API為開發(fā)者帶來了更好的邏輯復(fù)用能力,通過組合函數(shù)來實(shí)現(xiàn)更加簡(jiǎn)潔高效的邏輯復(fù)用。
為什么要使用Hooks
在以往VUE2的選項(xiàng)式API中,主要通過Mixin或是Class繼承來實(shí)現(xiàn)邏輯復(fù)用,但這種方式有三個(gè)明顯的短板:
1.不清晰的數(shù)據(jù)來源:當(dāng)使用了多個(gè)mixin/class時(shí),哪個(gè)數(shù)據(jù)是哪個(gè)模塊提供的將變得難以追尋,這將提高維護(hù)難度
2.命名空間沖突:來自多個(gè)class/mixin的開發(fā)者可能會(huì)注冊(cè)同樣的屬性名,造成沖突
3.隱性的跨模塊交流:不同的mixin/class之間可能存在某種相互作用,產(chǎn)生未知的后果
以上三種主要的缺點(diǎn)導(dǎo)致在大型項(xiàng)目的開發(fā)中,多mixin/class的組合將導(dǎo)致邏輯的混亂以及維護(hù)難度的提升,因而在VUE3的官方文檔中不再繼續(xù)推薦使用,保留mixin也只是為了遷移的需求或方便VUE2用戶熟悉。
mixin的缺點(diǎn)其實(shí)就是Hooks的優(yōu)點(diǎn):
1.清晰一目了然的源頭
2.沒有命名沖突的問題
3.精簡(jiǎn)邏輯
怎么開始玩Hooks
Hooks的各類規(guī)范
1.通常來講,一個(gè)Hook的命名需要以u(píng)se開頭,比如useTimeOut,這是約定俗成的,開發(fā)者看到useXXX即可明白這是一個(gè)Hook。Hook的名稱需要清楚地表明其功能。
2.只在當(dāng)前關(guān)注的最頂級(jí)作用域使用Hook,而不要在嵌套函數(shù)、循環(huán)中調(diào)用Hook
3.函數(shù)必須是純函數(shù),沒有副作用
4.返回值是一個(gè)函數(shù)或數(shù)據(jù),供外部使用
5.Hook內(nèi)部可以使用其他的Hook,組合功能
6.數(shù)據(jù)必須依賴于輸入,不依賴于外部狀態(tài),保持?jǐn)?shù)據(jù)流的明確性
7.在Hook內(nèi)部處理錯(cuò)誤,不要把錯(cuò)誤拋出到外部,否則會(huì)增加hook的使用成本
8.Hook是單一功能的,不要給一個(gè)Hook設(shè)計(jì)過多功能。單個(gè)Hook只負(fù)責(zé)做一件事,復(fù)雜的功能可以使用多個(gè)Hook互相組合實(shí)現(xiàn),如果給單個(gè)Hook增加過多功能,又會(huì)陷入過于臃腫、使用成本高、難維護(hù)的問題中
下面通過一個(gè)簡(jiǎn)單的hooks感受一下它的魅力:
這是一個(gè)控制頁面彈窗或者抽屜顯示或隱藏的hook,在以往vue2中,我們實(shí)現(xiàn)這樣一個(gè)功能,需要在data中定義一個(gè)變量,在methods中大概率會(huì)寫兩個(gè)方法分別控制彈窗的顯示和隱藏,如果頁面有多個(gè)這樣的顯隱組件,我們的代碼簡(jiǎn)直是災(zāi)難,糟糕的事,我們的代碼中這樣的案例實(shí)在是太多了,有了hooks就完全不一樣了.


通過這個(gè)例子發(fā)現(xiàn),我們?cè)趘ue2中大概率要寫6個(gè)方法和定義三個(gè)變量的工作在vue3配合Hooks的情況下,三行代碼就實(shí)現(xiàn)了.
下面進(jìn)入我們本文的重點(diǎn),通過hooks的方式實(shí)現(xiàn)sku選擇器的功能.

在CRMEB各個(gè)項(xiàng)目中,加購功能并不是只有在商品詳情頁使用,還有很多頁面也有使用,比如商品分類的幾個(gè)模板,購物車頁面,搭配購等,都會(huì)需要到打開sku選擇商品規(guī)格的功能,改功能包含選擇商品規(guī)格,價(jià)格,庫存,規(guī)格圖跟隨切換實(shí)時(shí)變化,還有加購數(shù)量的操作,對(duì)庫存為0的規(guī)格做不可操作的限制等等,所以這段代碼在前端是非常臃腫龐大的一部分代碼,牽扯的業(yè)務(wù)復(fù)雜,功能廣泛,若是在需要的組件內(nèi)每次復(fù)制粘貼,代碼量就會(huì)非常龐大,所以若是可以將這部分功能單獨(dú)抽離出來整理為一個(gè)可調(diào)用的方法就非常適合我們的使用場(chǎng)景.
先截圖看看以前vue2的方式書寫的該段代碼.


下面是我用vue3+ts+hooks的方式實(shí)現(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('請(qǐng)選擇');
? 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;
? ? ? ? }
? ? ? }
? ? }
? ? // 處理已售罄時(shí)默認(rèn)選中第一個(gè)
? ? 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 = '請(qǐng)選擇';
? ? } else if (!productSelect && !productAttr.length) {
? ? ? attr.productSelect = createProductSelect(3, productSelect);
? ? ? attrValue.value = '';
? ? ? attrTxt.value = '請(qǐng)選擇';
? ? }
? }
? 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 = '請(qǐng)選擇';
? ? }
? }
? /**
? *
? * @param type
? * true 加
? * false 減
? */
? function changeCartNum(type: boolean) {
? ? // 獲取當(dāng)前變動(dòng)屬性
? ? 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選擇器組件的頁面上使用:


這是一個(gè)管理sku選擇器內(nèi)商品規(guī)格選擇的Hook,在使用時(shí)只需傳入該商品的詳情數(shù)據(jù)以及一些配置項(xiàng)即可快默認(rèn)選中,節(jié)省了大量重復(fù)的控制代碼,使用該Hook后只需調(diào)用useSkuSelect即可實(shí)現(xiàn)規(guī)格的切換,加購數(shù)量的控制等等,且繼承原接口的類型.因?yàn)楸救似鋵?shí)也是hooks小白,處于學(xué)習(xí)階段,書寫的該hook和ts代碼有可能并不規(guī)范,歡迎讀者交流指正.
總結(jié)
Hooks是VUE3中利用組合式API響應(yīng)式的特性的,實(shí)現(xiàn)簡(jiǎn)單高效的邏輯復(fù)用、提高開發(fā)效率、提高VUE模塊可維護(hù)性的工具。Hooks的組合可以讓組件低代價(jià)、高效率地實(shí)現(xiàn)高復(fù)雜度業(yè)務(wù),Hooks之間通常相互獨(dú)立,沒有過度耦合,降低后期陷入維護(hù)地獄的風(fēng)險(xiǎn),而且可以使得功能模塊更加易于測(cè)試.使用開源的Hook將為開發(fā)帶來很多方便,而開發(fā)自定義Hook則需要花費(fèi)一些時(shí)間,但在實(shí)現(xiàn)后,高度的定制化將為項(xiàng)目開發(fā)帶來巨大的便利.Hooks的出現(xiàn)不意味著拋棄Class,Hooks也有自己的缺點(diǎn)比如內(nèi)存泄漏和可能的性能問題。Class更加易于上手,在經(jīng)驗(yàn)豐富、技術(shù)深厚的開發(fā)者手中也可以一定程度上避開Class的缺點(diǎn)