Vue3 這樣子寫頁面更快更高效

前言

在開發管理后臺過程中,一定會遇到不少了增刪改查頁面,而這些頁面的邏輯大多都是相同的,如獲取列表數據,分頁,篩選功能這些基本功能。而不同的是呈現出來的數據項。還有一些操作按鈕。
對于剛開始只有 1,2 個頁面的時候大多數開發者可能會直接將之前的頁面代碼再拷貝多一份出來,而隨著項目的推進類似頁面數量可能會越來越多,這直接導致項目代碼耦合度越來越高。
這也是為什么在項目中一些可復用的函數或組件要抽離出來的主要原因之一。
下面,我們封裝一個通用的useList,適配大多數增刪改查的列表頁面,讓你更快更高效的完成任務,準點下班 ~

封裝

我們需要將一些通用的參數和函數抽離出來,封裝成一個通用hook,后續在其他頁面復用相同功能更加簡單方便。

定義列表頁面必不可少的分頁數據

export default function useList() {
  // 加載態
  const loading = ref(false);
  // 當前頁
  const curPage = ref(1);
  // 總數量
  const total = ref(0);
  // 分頁大小
  const pageSize = ref(10);
}

如何獲取列表數據

思考一番,讓useList函數接收一個listRequestFn參數,用于請求列表中的數據。
定義一個list變量,用于存放網絡請求回來的數據內容,由于在內部無法直接確定列表數據類型,通過泛型的方式讓外部提供列表數據類型。

export default function useList<ItemType extends Object>(
  listRequestFn: Function
) {
  // 忽略其他代碼
  const list = ref<ItemType[]>([]);
}

在useList中創建一個loadData函數,用于調用獲取數據函數,該函數接收一個參數用于獲取指定頁數的數據(可選,默認為curPage的值)。
執行流程

  1. 設置加載狀態
  2. 調用外部傳入的函數,將獲取到的數據賦值到list和total中
  3. 關閉加載態

這里使用了 async/await 語法,假設請求出錯、解構出錯情況會走 catch 代碼塊,再關閉加載態
這里需要注意,傳入的 listRequestFn 函數接收的參數數量和類型是否正常對應上 請根據實際情況進行調整

export default function useList<ItemType extends Object>(
  listRequestFn: Function
) {
  // 忽略其他代碼
  // 數據
  const list = ref<ItemType[]>([]);
  // 過濾數據
  // 獲取列表數據
  const loadData = async (page = curPage.value) => {
    // 設置加載中
    loading.value = true;
    try {
      const {
        data,
        meta: { total: count },
      } = await listRequestFn(pageSize.value, page);
      list.value = data;
      total.value = count;
    } catch (error) {
      console.log("請求出錯了", "error");
    } finally {
      // 關閉加載中
      loading.value = false;
    }
  };
}

別忘了,還有切換分頁要處理
使用 watch 函數監聽數據,當curPage,pageSize的值發生改變時調用loadData函數獲取新的數據。

export default function useList<ItemType extends Object>(
  listRequestFn: Function
) {
  // 忽略其他代碼
  // 監聽分頁數據改變
  watch([curPage, pageSize], () => {
    loadData(curPage.value);
  });
}

現在實現了基本的列表數據獲取

實現數據篩選器

在龐大的數據列表中,數據篩選是必不可少的功能
通常,我會將篩選條件字段定義在一個ref中,在請求時將ref丟到請求函數即可。
在 useList 函數中,第二個參數接收一個filterOption對象,對應列表中的篩選條件字段。
調整一下loadData函數,在請求函數中傳入filterOption對象即可

注意,傳入的 listRequestFn 函數接收的參數數量和類型是否正常對應上 請根據實際情況進行調整

export default function useList<
  ItemType extends Object,
  FilterOption extends Object
>(listRequestFn: Function, filterOption: Ref<Object>) {
  const loadData = async (page = curPage.value) => {
    // 設置加載中
    loading.value = true;
    try {
      const {
        data,
        meta: { total: count },
      } = await listRequestFn(pageSize.value, page, filterOption.value);
      list.value = data;
      total.value = count;
    } catch (error) {
      console.log("請求出錯了", "error");
    } finally {
      // 關閉加載中
      loading.value = false;
    }
  };
}

注意,這里 filterOption 參數類型需要的是 ref 類型,否則會丟失響應式 無法正常工作

清空篩選器字段

在頁面中,有一個重置的按鈕,用于清空篩選條件。這個重復的動作可以交給 reset 函數處理。
通過使用 Reflect 將所有值設定為undefined,再重新請求一次數據。

export default function useList<
  ItemType extends Object,
  FilterOption extends Object
>(listRequestFn: Function, filterOption: Ref<Object>) {
  const reset = () => {
    if (!filterOption.value) return;
    const keys = Reflect.ownKeys(filterOption.value);
    filterOption.value = {} as FilterOption;
    keys.forEach((key) => {
      Reflect.set(filterOption.value!, key, undefined);
    });
    loadData();
  };
}

導出功能

除了對數據的查看,有些界面還需要有導出數據功能(例如導出 csv,excel 文件),我們也把導出功能寫到useList里
通常,導出功能是調用后端提供的導出Api獲取一個文件下載地址,和loadData函數類似,從外部獲取exportRequestFn函數來調用Api
在函數中,新增一個exportFile函數調用它。

export default function useList<
  ItemType extends Object,
  FilterOption extends Object
>(
  listRequestFn: Function,
  filterOption: Ref<Object>,
  exportRequestFn?: Function
) {
  // 忽略其他代碼
  const exportFile = async () => {
    if (!exportRequestFn) {
      throw new Error("當前沒有提供exportRequestFn函數");
    }
    if (typeof exportRequestFn !== "function") {
      throw new Error("exportRequestFn必須是一個函數");
    }
    try {
      const {
        data: { link },
      } = await exportRequestFn(filterOption.value);
      window.open(link);
    } catch (error) {
      console.log("導出失敗", "error");
    }
  };
}

注意,傳入的 exportRequestFn 函數接收的參數數量和類型是否正常對應上 請根據實際情況進行調整

優化

現在,整個useList已經滿足了頁面上的需求了,擁有了獲取數據,篩選數據,導出數據,分頁功能
還有一些細節方面,在上面所有代碼中的try..catch中的catch代碼片段并沒有做任何的處理,只是簡單的console.log一下

提供鉤子

在useList新增一個 Options 對象參數,用于函數成功、失敗時執行指定鉤子函數與輸出消息內容。
定義 Options 類型

export interface MessageType {
  GET_DATA_IF_FAILED?: string;
  GET_DATA_IF_SUCCEED?: string;
  EXPORT_DATA_IF_FAILED?: string;
  EXPORT_DATA_IF_SUCCEED?: string;
}
export interface OptionsType {
  requestError?: () => void;
  requestSuccess?: () => void;
  message: MessageType;
}

export default function useList<
  ItemType extends Object,
  FilterOption extends Object
>(
  listRequestFn: Function,
  filterOption: Ref<Object>,
  exportRequestFn?: Function,
  options? :OptionsType
) {
  // ...
}

設置Options默認值

const DEFAULT_MESSAGE = {
  GET_DATA_IF_FAILED: "獲取列表數據失敗",
  EXPORT_DATA_IF_FAILED: "導出數據失敗",
};

const DEFAULT_OPTIONS: OptionsType = {
  message: DEFAULT_MESSAGE,
};

export default function useList<
  ItemType extends Object,
  FilterOption extends Object
>(
  listRequestFn: Function,
  filterOption: Ref<Object>,
  exportRequestFn?: Function,
  options = DEFAULT_OPTIONS
) {
  // ...
}

在沒有傳遞鉤子的情況霞,推薦設置默認的失敗時信息顯示

優化loadData,exportFile函數
基于 elementui 封裝 message 方法

import { ElMessage, MessageOptions } from "element-plus";

export function message(message: string, option?: MessageOptions) {
  ElMessage({ message, ...option });
}
export function warningMessage(message: string, option?: MessageOptions) {
  ElMessage({ message, ...option, type: "warning" });
}
export function errorMessage(message: string, option?: MessageOptions) {
  ElMessage({ message, ...option, type: "error" });
}
export function infoMessage(message: string, option?: MessageOptions) {
  ElMessage({ message, ...option, type: "info" });
}

loadData 函數

const loadData = async (page = curPage.value) => {
  loading.value = true;
  try {
    const {
      data,
      meta: { total: count },
    } = await listRequestFn(pageSize.value, page, filterOption.value);
    list.value = data;
    total.value = count;
    // 執行成功鉤子
    options?.message?.GET_DATA_IF_SUCCEED &&
      message(options.message.GET_DATA_IF_SUCCEED);
    options?.requestSuccess?.();
  } catch (error) {
    options?.message?.GET_DATA_IF_FAILED &&
      errorMessage(options.message.GET_DATA_IF_FAILED);
    // 執行失敗鉤子
    options?.requestError?.();
  } finally {
    loading.value = false;
  }
};

exportFile 函數

const exportFile = async () => {
  if (!exportRequestFn) {
    throw new Error("當前沒有提供exportRequestFn函數");
  }
  if (typeof exportRequestFn !== "function") {
    throw new Error("exportRequestFn必須是一個函數");
  }
  try {
    const {
      data: { link },
    } = await exportRequestFn(filterOption.value);
    window.open(link);
    // 顯示信息
    options?.message?.EXPORT_DATA_IF_SUCCEED &&
      message(options.message.EXPORT_DATA_IF_SUCCEED);
    // 執行成功鉤子
    options?.exportSuccess?.();
  } catch (error) {
    // 顯示信息
    options?.message?.EXPORT_DATA_IF_FAILED &&
      errorMessage(options.message.EXPORT_DATA_IF_FAILED);
    // 執行失敗鉤子
    options?.exportError?.();
  }
};

useList 使用方法

<template>
  <el-collapse class="mb-6">
    <el-collapse-item title="篩選條件" name="1">
      <el-form label-position="left" label-width="90px" :model="filterOption">
        <el-row :gutter="20">
          <el-col :xs="24" :sm="12" :md="8" :lg="8" :xl="8">
            <el-form-item label="用戶名">
              <el-input
                v-model="filterOption.name"
                placeholder="篩選指定簽名名稱"
              />
            </el-form-item>
          </el-col>
          <el-col :xs="24" :sm="12" :md="8" :lg="8" :xl="8">
            <el-form-item label="注冊時間">
              <el-date-picker
                v-model="filterOption.timeRange"
                type="daterange"
                unlink-panels
                range-separator="到"
                start-placeholder="開始時間"
                end-placeholder="結束時間"
                format="YYYY-MM-DD HH:mm"
                value-format="YYYY-MM-DD HH:mm"
              />
            </el-form-item>
          </el-col>
          <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
            <el-row class="flex mt-4">
              <el-button type="primary" @click="filter">篩選</el-button>
              <el-button type="primary" @click="reset">重置</el-button>
            </el-row>
          </el-col>
        </el-row>
      </el-form>
    </el-collapse-item>
  </el-collapse>
  <el-table v-loading="loading" :data="list" border style="width: 100%">
    <el-table-column label="用戶名" min-width="110px">
      <template #default="scope">
        {{ scope.row.name }}
      </template>
    </el-table-column>
    <el-table-column label="手機號碼" min-width="130px">
      <template #default="scope">
        {{ scope.row.mobile || "未綁定手機號碼" }}
      </template>
    </el-table-column>
    <el-table-column label="郵箱地址" min-width="130px">
      <template #default="scope">
        {{ scope.row.email || "未綁定郵箱地址" }}
      </template>
    </el-table-column>
    <el-table-column prop="createAt" label="注冊時間" min-width="220px" />
    <el-table-column width="200px" fixed="right" label="操作">
      <template #default="scope">
        <el-button type="primary" link @click="detail(scope.row)"
          >詳情</el-button
        >
      </template>
    </el-table-column>
  </el-table>
  <div v-if="total > 0" class="flex justify-end mt-4">
    <el-pagination
      v-model:current-page="curPage"
      v-model:page-size="pageSize"
      background
      layout="sizes, prev, pager, next"
      :total="total"
      :page-sizes="[10, 30, 50]"
    />
  </div>
</template>
<script setup lang="ts">
import { UserInfoApi } from "@/network/api/User";
import useList from "@/lib/hooks/useList/index";
const filterOption = ref<UserInfoApi.FilterOptionType>({});
const {
  list,
  loading,
  reset,
  filter,
  curPage,
  pageSize,
  reload,
  total,
  loadData,
} = useList<UserInfoApi.UserInfo[], UserInfoApi.FilterOptionType>(
  UserInfoApi.list,
  filterOption
);
</script>
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,737評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,699評論 2 370

推薦閱讀更多精彩內容