進入Hooks時代寫出高質量react及vue組件詳解

概述

vue和react都已經全面進入瞭hooks時代(在vue中也稱為組合式api,為瞭方便後面統一稱為hooks),然而受到以前react中類組件和vue2寫法的影響,很多開發者都不能及時轉換過來,以致於開發出一堆面條式代碼,整體的代碼質量反而不如改版以前瞭。

hooks組件到底應該如何寫,我也曾為此迷惘過一段時間。特別我以前以react開發居多,但在轉到新崗位後又變成瞭使用vue3開發,對於兩個框架在思維方式和寫法的不同上,很是花瞭一段時間適應。好在幾個月下來,我發現二者雖然在寫法上有區別之處,但思想上卻大同小異。

所以在比較瞭兩個框架的異同後,我總結出瞭一套通用的hooks api的抽象方式,在這裡分享給大傢。如果您有不同意見歡迎在評論區指正。

一個組件內部的所有代碼——無論vue還是react——都可以抽象成以下幾個部分:

  • 組件視圖,組件中用來描述視覺效果的部分,如css和html、react的jsx或者vue的template代碼
  • 組件相關邏輯,如組件生命周期,按鈕交互,事件等
  • 業務相關邏輯,如登錄註冊,獲取用戶信息,獲取商品列表等與組件無關的業務抽象

單獨拆分這三塊並不難,難的是一個組件可能寫得特別復雜,裡面可能包含瞭多個視圖,每個視圖相互之間又有交互;同時又可能包含多個業務邏輯,多個業務的函數和變量雜亂無章地隨意放置,導致後續維護的時候要在代碼之間反復橫跳。

要寫出高質量的組件,可以思考以下幾個問題:

1.組件什麼時候拆?怎麼拆?

一個常見的誤區是,隻有需要復用的時候才去拆分組件,這種看法顯然過於片面瞭。你可以思考一下,自己是如何抽象一個函數的,你隻會在代碼需要復用的時候才抽出一個函數嗎?顯然不是。因為函數不僅有代碼復用的功能,還具有一定的描述性質以及代碼封閉性。這種特性使得我們看到一個函數的時候,不必關註代碼細節,就能大概知道這部分代碼是幹啥的。

我們還可以再用函數將一部分函數組合起來,形成更高層級的抽象。按國內流行的說法,高層級的抽象被稱為粗粒度,低層級的抽象被稱為細粒度,不同粗細粒度的抽象可以稱它們為不同的抽象層級。並且一個理想的函數內部,一般隻會包含同一抽象層級的代碼。

組件的拆分也可以遵循同樣的道理。我們可以按照當前的結構或者功能、業務,將組件拆分為功能清晰且單一、與外部耦合程度低的組件(即所謂高內聚,低耦合)。如果一個組件裡面幹瞭太多事,或者依賴的外部狀態太多,那麼就不是一個容易維護的組件瞭。

然而,為瞭保持組件功能單一,我們是不是要將組件拆分得特別細才可以呢?事實並非如此。因為上面說過,抽象是有粗細粒度之分的,也許一個組件從較細的粒度來講功能並不單一,但是從較粗的粒度來說,可能他們的功能就是單一的瞭。例如登錄和註冊是兩個不同的功能,但是你從更高層級的抽象來看,它們都屬於用戶模塊的一部分。

所以是否要拆分組件,最關鍵還是得看復雜度。如果一個頁面特別簡單,那麼不進行拆分也是可以,有時候拆分得過於細可能反而不利於維護。

如何判斷一個組件是否復雜?恐怕這裡不能給出一個準確的答案,畢竟代碼的實現方式千奇百怪,很難有一個機械的標準評判。但是我們不妨站在第三方角度看看自己的代碼,如果你是一個工作一年的程序員,是否能比較容易地看懂這裡的代碼?如果不能就要考慮進行拆分瞭。如果你非要一個機械的判斷標準,我建議是代碼控制在200行內。

總結一下,拆分組件的時候可以參考下面幾個原則:

  • 拆分的組件要保持功能單一。即組件內部代碼的代碼都隻跟這個功能相關;
  • 組件要保持較低的耦合度,不要與組件外部產生過多的交互。如組件內部不要依賴過多的外部變量,父子組件的交互不要搞得太復雜等等。
  • 用組件名準確描述這個組件的功能。就像函數那樣,可以讓人不用關心組件細節,就大概知道這個組件是幹嘛的。如果起名比較困難,考慮下是不是這個組件的功能並不單一。

2.如何組織拆分出的組件文件?

拆分出來的組件應該放在哪裡呢?一個常見的錯誤做法是一股腦放在一個名為components文件夾裡,最後搞得這個文件夾特別臃腫。我的建議是相關聯的代碼最好盡量聚合在一起。

為瞭讓相關聯的代碼聚合到一起,我們可以把頁面搞成文件夾的形式,在文件夾內部存放與當前文件相關的組成部分,並將表示頁面的組件命名為index放在文件夾下。再在該文件夾下創建components目錄,將組成頁面的其他組件放在裡面。

如果一個頁面的某個組成部分很復雜,內部還需要拆分成更細的多個組件,那麼就把這個組成部分也做成文件夾,將拆分出的組件放在這個文件夾下。

最後就是組件復用的問題。如果一個組件被多個地方復用,就把它單獨提取出來,放到需要復用它的組件們共同的抽象層級上。 如下:

  • 如果隻是被頁面內的組件復用,就放到頁面文件夾下。
  • 如果隻是在當前業務場景下的不同頁面復用,就放到當前業務模塊的文件夾下。
  • 如果可以在不同業務場景間通用,就放到最頂層的公共文件夾,或者考慮做成組件庫。

關於項目文件的組織方式已經超過本文討論的范疇,我打算放到以後專門出一篇文章說下如何組織項目文件。這裡隻說下頁面級別的文件如何進行組織。下面是我常用的一種頁面級別的文件的組織方式:

homePage // 存放當前頁面的文件夾
    |-- components // 存放當前頁面組件的文件夾
        |-- componentA // 存放當前頁面的組成部分A的文件夾
            |-- index.(vue|tsx) // 組件A
            |-- AChild1.(vue|tsx) // 組件a的組成部分1
            |-- AChild2.(vue|tsx) // 組件a的組成部分2
            |-- ACommon.(vue|tsx) // 隻在componentA內部復用的組件
        |-- ComponentB.(vue|tsx) // 當前頁面的組成部分B
        |-- Common.(vue|tsx) // 組件A和組件B裡復用的組件
    |-- index.(vue|tsx) // 當前頁面

實際上這種組織方式,在抽象意義上並不完美,因為通用組件和頁面組成部分的組件並沒有區分開來。但是一般來說,一個頁面也不會抽出太多組件,為瞭方便放到一起也不會有太大問題。但是如果你的頁面實在復雜,那麼再創建一個名為common的文件夾也未嘗不可。

3.如何用hooks抽離組件邏輯?

在hooks出現之前,曾流行過一個設計模式,這個模式將組件分為無狀態組件和有狀態組件(也稱為展示組件和容器組件),前者負責控制視覺,後者負責傳遞數據和處理邏輯。但有瞭hooks之後,我們完全可以將容器組件中的代碼放進hooks裡面。後者不僅更容易維護,而且也更方便把業務邏輯與一般組件區分開來。

在抽離hooks的時候,我們不僅應該沿用一般函數的抽象思維,如功能單一,耦合度低等等,還應該註意組件中的邏輯可分為兩種:組件交互邏輯與業務邏輯。如何把文章開頭說的視圖、交互邏輯和業務邏輯區分開來,是衡量一個組件質量的重要標準。

以一個用戶模塊為例。一個包含查詢用戶信息,修改用戶信息,修改密碼等功能的hooks可以這樣寫:

// 用戶模塊hook
const useUser = () => {
    // react版本的用戶狀態
    const user = useState({});
    // vue版本的用戶狀態
    const userInfo = ref({});
    // 獲取用戶狀態
    const getUserInfo = () => {}
    // 修改用戶狀態
    const changeUserInfo = () => {};
    // 檢查兩次輸入的密碼是否相同
    const checkRepeatPass = (oldPass,newPass) => {}
    // 修改密碼
    const changePassword = () => {};
    return {
        userInfo,
        getUserInfo,
        changeUserInfo,
        checkRepeatPass,
        changePassword,
    }
}

交互邏輯的hook可以這麼寫(為瞭方便隻寫vue版本的,大傢應該也都看得懂):

// 用戶模塊交互邏輯hooks
const useUserControl = () => {
    // 組合用戶hook
    const { userInfo, getUserInfo, changeUserInfo, checkRepeatPass, changePassword } = useUser();
    // 數據查詢loading狀態
    const loading = ref(false);
    // 錯誤提示彈窗的狀態
    const errorModalState = reactive({
        visible: false, // 彈窗顯示/隱藏
        errorText: '',  // 彈窗文案
    });
    // 初始化數據
    const initData = () => {
        getUserInfo();
    }
    // 修改密碼表單提交
    const onChangePassword = ({ oldPass, newPass ) => {
        // 判斷兩次密碼是否一致
        if (checkRepeatPass(oldPass, newPass)) {
            changePassword();
        } else {
            errorModalState.visible = true;
            errorModalState.text = '兩次輸入的密碼不一致,請修改'
        }
    };
    return {
        // 用戶數據
        userInfo,
        // 初始化數據
        initData: getUserInfo,
        // 修改密碼
        onChangePassword,
        // 修改用戶信息
        onChangeUserInfo: changeUserInfo,
    }
}

然後隻要在組件裡面引入交互邏輯的hook即可:

vue版本:

<template>
    <!-- 視圖部分省略,在對應btn處引用onChangePassword和onChangeUserInfo即可 -->
</template>
<script setup>
import useUserControl from './useUserControl';
import { onMounted } from 'vue';
const { userInfo, initData, onChangePassword, onChangeUserInfo } = useUserControl();
onMounted(initData);
<script>

react版本:

import useUserControl from './useUserControl';
import { useEffect } from 'react';
const UserModule = () => {
    const { userInfo, initData, onChangePassword, onChangeUserInfo } = useUserControl();
    useEffect(initData, []);
    return (
        // 視圖部分省略,在對應btn處引用onChangePassword和onChangeUserInfo即可
    )
}

而拆分出的三個文件放在組件同級目錄下即可;如果拆出的hooks較多,可以單獨開辟一個hooks文件夾。如果有可以復用的hooks,參考組件拆分裡面分享的方法,放到需要復用它的組件們共同的抽象層級上即可

可以看到抽離出hooks邏輯後,組件變得十分簡單、容易理解,我們也實現瞭各個部分的分離。不過這裡還有一個問題,那就是上面的業務場景實在太過簡單,有必要拆分得這麼細,搞出三個文件這麼復雜嗎?

針對邏輯並不復雜的組件,我個人覺得和組件放到一起也未嘗不可。為瞭簡便,我們可以隻把業務邏輯封裝成hooks,而組件的交互邏輯就直接放在組件裡面。如下:

<template>
    <!-- 視圖部分省略,在對應btn處引用changePassword和changeUserInfo即可 -->
</template>
<script setup>
import { onMounted } from 'vue';
// 用戶模塊hook
const useUser = () => { 
    // 代碼省略
}
const { userInfo, getUserInfo, changeUserInfo, checkRepeatPass, changePassword } = useUser();
// 數據查詢loading狀態
const loading = ref(false);
// 錯誤提示彈窗的狀態
const errorModalState = reactive({
    visible: false, // 彈窗顯示/隱藏
    errorText: '', // 彈窗文案
});
// 初始化數據
const initData = () => { getUserInfo(); }
// 修改密碼表單提交
const onChangePassword = ({ oldPass, newPass ) => {};
onMounted(initData);
<script>

但是如果邏輯比較復雜,或者一個組件裡面包含多個復雜業務或者復雜交互,需要抽離出多個hooks的情況,還是單獨抽出一個個文件比較好。總而言之,依據代碼復雜度,選擇相對更容易理解的寫法。

也許單獨一個組件,你並不能體會出hooks寫法的優越性。但當你封裝出更多的hooks之後,你會逐漸發現這樣寫的好處。正因為不同的業務和功能被封裝在一個個hooks裡面,彼此互不幹擾,業務才能更容易區分和理解。對於項目的可維護性和可讀性提升是非常之大的。

下圖展示瞭vue2寫法和vue3 hooks寫法的區別。圖中相同顏色的代碼塊代表這些代碼是屬於同一個功能的,但vue2的寫法導致本來是相同功能的代碼,卻被拆散到瞭不同地方(react其實也容易有相同的問題,例如當一個組件有多個功能時,不同功能的代碼也很容易混雜到一起)。而通過封裝成一個個hooks,相關聯的代碼就很容易被聚合到瞭一起,且和其他功能區分開瞭。

題外話:全局狀態的管理

現在的前端項目還有一個較為常見的誤區,那就是全局狀態管理庫(即redux、vuex等)的濫用。依據抽象層級的思維,實際上很多項目並不需要放較多的狀態到全局,這種情況利用react和vue自身的狀態管理就足夠瞭。

如果非要用狀態管理庫,也要警惕放較多狀態和函數到全局。一個狀態是否要放到全局,我一般有兩個判斷標準:

  • 狀態是否在多個頁面間共享;
  • 跳轉頁面後又返回該頁面,是否需要還原跳轉之前的狀態(僅對react而言,vue有keep-alive)

而全局狀態管理庫中的函數,則隻放置與全局狀態有關的邏輯。除此之外的狀態,一律交由react和vue組件本身進行管理。

以上就是Hooks中寫出高質量的react和vue組件的詳細內容,更多關於Hooks react和vue組件的資料請關註WalkonNet其它相關文章!

推薦閱讀: