Vue3 組合式 API 的實戰技巧 —— 組合式 API 幫我解決了不少 Options API 難以應對的問題,尤其是在代碼復用和復雜組件維護上。
一、為什么放棄 Options API?聊聊三年項目里的真實痛點?
剛接觸 Vue3 時,我曾因 “慣性” 繼續用 Options API 寫業務,但隨著項目復雜度提升,兩個痛點越來越明顯:?
- 代碼碎片化:一個數據請求相關的邏輯(加載態、數據處理、錯誤提示),要分散在data、methods、mounted里,后期維護時需在多個選項間跳轉,尤其復雜表單組件,邏輯溯源成本極高;?
- 復用性差:比如多個組件都需要 “分頁加載列表” 功能,Options API 只能通過mixins實現,但mixins存在命名沖突、邏輯來源不清晰的問題(比如同事接手項目時,不知道某個變量是來自組件本身還是mixins)。?
直到用組合式 API 重構了第一個列表頁,才真正體會到 “邏輯聚合” 的爽 —— 所有和 “列表請求” 相關的代碼都放在一起,復用只需復制一個函數,這也是我今天想重點分享的核心:用組合式 API 封裝通用 Hook,解決重復造輪子問題。?
二、實戰:封裝 2 個 Vue3+JS 通用 Hook(附完整代碼)?
下面結合我項目中高頻使用的場景,分享兩個通用 Hook 的封裝思路,所有代碼均基于 Vue3+JS 編寫,可直接復制到項目中使用。?
1. useRequest:統一處理請求狀態(加載 / 成功 / 失敗)?
幾乎所有業務組件都需要請求接口,而 “加載態顯示”“錯誤提示”“請求取消” 是通用需求。之前每個組件都要寫loading: false、error: '',重復代碼占比 30% 以上,用useRequest可完全統一。?
代碼實現:
import { ref, onUnmounted } from 'vue';
import { ElMessage } from 'element-plus'; // 假設項目用Element Plus,可替換為其他UI庫/*** 通用請求Hook* @param {Function} requestFn - 接口請求函數(需返回Promise)* @param {Object} options - 配置項(可選)* @param {boolean} options.autoRun - 是否默認執行請求(默認true)* @param {Function} options.onSuccess - 請求成功回調* @param {Function} options.onError - 請求失敗回調*/
export function useRequest(requestFn, options = {}) {const { autoRun = true, onSuccess, onError } = options;const loading = ref(false); // 加載態const data = ref(null); // 請求結果const error = ref(''); // 錯誤信息const controller = new AbortController(); // 用于取消請求// 核心請求函數const fetchData = async (...args) => {loading.value = true;error.value = '';try {// 傳遞signal,支持取消請求const result = await requestFn(...args, { signal: controller.signal });data.value = result;onSuccess && onSuccess(result); // 自定義成功回調} catch (err) {// 排除手動取消請求的錯誤if (err.name !== 'AbortError') {error.value = err.message || '請求失敗,請重試';ElMessage.error(error.value); // 全局錯誤提示onError && onError(err); // 自定義失敗回調}} finally {loading.value = false;}};// 自動執行請求(默認開啟)if (autoRun) {fetchData();}// 組件卸載時取消請求,避免內存泄漏onUnmounted(() => {controller.abort();});return {loading,data,error,fetchData, // 手動觸發請求(比如刷新按鈕)cancelRequest: () => controller.abort() // 手動取消請求};
}
使用示例(用戶列表組件):
import { useRequest } from '@/hooks/useRequest';
import { getUserList } from '@/api/user'; // 接口函數export default {setup() {// 1. 初始化請求Hook,傳入接口函數和配置const { loading, data: userList, fetchData } = useRequest(getUserList, {onSuccess: (res) => {console.log('用戶列表請求成功', res);}});// 2. 手動刷新(比如搜索按鈕點擊)const handleRefresh = (searchParams) => {fetchData(searchParams); // 傳遞參數給接口函數};return {loading,userList,handleRefresh};}
};
2. usePagination:快速實現分頁功能?
管理后臺中 “分頁列表” 是高頻場景,頁碼切換、每頁條數變更、總數計算這些邏輯完全可以復用。usePagination可結合上面的useRequest,快速搭建分頁功能。?
代碼實現:
import { ref, computed } from 'vue';/*** 通用分頁Hook* @param {Function} fetchFn - 列表請求函數(需接收page、pageSize參數)* @param {Object} defaultParams - 默認分頁參數(可選)*/
export function usePagination(fetchFn, defaultParams = {}) {// 分頁基礎參數const page = ref(defaultParams.page || 1); // 當前頁碼const pageSize = ref(defaultParams.pageSize || 10); // 每頁條數const total = ref(0); // 總條數// 計算總頁數const totalPage = computed(() => Math.ceil(total.value / pageSize.value) || 1);// 頁碼切換事件const handlePageChange = (newPage) => {page.value = newPage;fetchFn({ page: newPage, pageSize: pageSize.value }); // 觸發請求};// 每頁條數變更事件const handlePageSizeChange = (newSize) => {pageSize.value = newSize;page.value = 1; // 重置為第一頁fetchFn({ page: 1, pageSize: newSize }); // 觸發請求};// 重置分頁參數(比如搜索時)const resetPagination = () => {page.value = 1;pageSize.value = defaultParams.pageSize || 10;};return {page,pageSize,total,totalPage,handlePageChange,handlePageSizeChange,resetPagination,// 分頁參數對象(方便傳遞給請求函數)paginationParams: computed(() => ({page: page.value,pageSize: pageSize.value}))};
}
結合 useRequest 使用示例:
import { useRequest } from '@/hooks/useRequest';
import { usePagination } from '@/hooks/usePagination';
import { getUserList } from '@/api/user';export default {setup() {// 1. 初始化分頁Hook,定義請求函數(接收分頁參數)const { paginationParams, total, handlePageChange, handlePageSizeChange } = usePagination(async (params) => {// 調用useRequest的fetchData,傳遞分頁參數await fetchData({ ...params, ...searchParams.value });});// 2. 初始化請求Hook,請求時帶上分頁參數const searchParams = ref({}); // 搜索參數(可選)const { loading, data: userList, fetchData } = useRequest(async (params = {}) => {const res = await getUserList({ ...paginationParams.value, ...params });total.value = res.total; // 更新總條數return res.list;});// 3. 搜索功能(重置分頁)const handleSearch = (params) => {searchParams.value = params;handlePageChange(1); // 搜索時跳轉到第一頁};return {loading,userList,page: paginationParams.page,pageSize: paginationParams.pageSize,total,handlePageChange,handlePageSizeChange,handleSearch};}
};
三、Vue3+JS 開發的 3 個避坑技巧(三年經驗總結)?
- 避免在 setup 中直接修改 props:Vue3 中 props 仍是單向數據流,若需修改 props,可通過emit通知父組件,或用toRef創建 props 的響應式引用(如const name = toRef(props, 'name'));?
- watch 監聽對象時要加 deep:若監聽的是對象 / 數組,需開啟deep: true才能監聽到內部屬性變化(如watch(user, () => {}, { deep: true })),但盡量避免監聽整個對象,可直接監聽具體屬性(如watch(() => user.name, () => {})),性能更好;?
- Hook 命名規范統一:自定義 Hook 建議以use開頭(如useRequest、usePagination),方便團隊識別和維護,避免出現getPagination、paginationUtil這類不統一的命名。?
四、總結與交流?
以上就是我基于 Vue3+JS 棧的實戰分享 —— 從項目痛點出發,用組合式 API 封裝通用 Hook,既能減少重復代碼,又能提升項目可維護性。這也是我三年前端開發中,從 “寫功能” 到 “寫優雅的功能” 的一個重要轉變。?