前端提升生產力系列三(vant3 vue3 移動端H5下拉重新整理,上拉載入元件的封裝)

aehyok發表於2022-03-02

| 在日常的移動端開發中,經常會遇到列表的展示,以及資料量變多的情況下還會有上拉和下拉的操作。進入新公司後發現移動端好多列表,但是在看程式碼的時候發現,每個列表都是單獨的程式碼,沒有任何的封裝,都是通過vant元件,裡面充滿了過多的重複程式碼,在有bug或者有需求變更的時候,每次的改動都要對很多個相同邏輯的頁面元件進行修改,於是花了一點時間,將其進行封裝,發現還是節省了很多的時間。自己做一個記錄。

前端提升生產力系列文章

1.前端提升生產力系列一(vue3 element-plus 配置json快速生成form表單元件)

2.前端提升生產力系列二(vue3 element-plus 配置json快速生成table列表元件)

3.前端提升生產力系列三(vant3 vue3 移動端H5下拉重新整理,上拉載入元件的封裝)

本文涉及所有原始碼已上傳 https://github.com/aehyok/vue-qiankun

1、實現功能的講解

先說一下實現的功能

  • 1、模擬了一個api請求,用於請求介面資料的,並將請求設定為5秒後資料請求成功(效果明顯一點)
  • 2、定義請求介面的頁碼相關引數,以及控制邏輯
  • 3、下拉重新整理第一頁資料,並且在重新整理過程中,不能再進行下拉重新整理
  • 4、上拉載入下一頁資料,並且在載入過程中,不能再進行上拉載入
  • 5、載入到最後一頁,則最末端會顯示【資料已載入完畢】
  • 6、如果請求api一開始就沒有資料,則顯示成一個預設圖片(代表沒有載入到資料)

2、實現效果的演示

動畫.gif

3、沒有封裝前的程式碼邏輯(內附註釋)

  <template>
    <van-pull-refresh
      v-model="isRefresh"
      @refresh="refreshClick"
      loading-text="正在請求資料"
      success-text="資料重新整理成功"
    >
      <van-list
        v-model:loading="isListLoading"
        :finished="isFinished"
        :offset="state.offset"
        finished-text="資料已載入完畢"
        :immediate-check="false"
        @load="onLoad"
      >
      <div class="main">
        <div class="flex" v-for="item in dataList" :key="item.id">
          <div :class="!item.url ? 'itemCollagen' : 'itemCollagenSeventy'">
              <p>{{ item.messageName }}</p>
              <span
              ><span :class="item.createdByDeptName ? 'createdByDeptName' : ''">{{
                  item.createdByDeptName ? item.createdByDeptName : ''
              }}</span
              >{{ item.createdAt }}</span
              >
          </div>
          <div v-if="item.url">
              <img :src="item.url" alt="" />
          </div>
          </div>
      </div>
      </van-list>
    </van-pull-refresh>
    <div v-if="state.nodata===true"><van-empty description="沒有資料"  /></div>
    
  </template>
  <script setup>
    import { PullRefresh as VanPullRefresh, List as VanList, Empty as VanEmpty } from 'vant';
    import { onBeforeMount, ref, reactive, watch } from 'vue';
    
    const setTotal = 51  // 設定列表總記錄數
    let dbList = []  // 通過迴圈向陣列插入測試資料
    for(let i= 0; i< setTotal; i++) {
        dbList.push({
          id: i + 1,
          messageName: '長圖片'+(i+1),
          createdAt: '2021-07-27 17:06:19',
          createdByDeptName: '百色',
          url: 'http://vue.tuokecat.com/cdn/h5/newslist.jpg',
        })
    } 
    const successText = ref('正在請求資料')
    const dataList = ref([]);
    const pageModel = reactive({
      page: 1,
      limit: 15,
      total: 0,
      pages: 0,
    });

    const sleep = (time) => {
      return new Promise(resolve => setTimeout(resolve, time))
    }
      /**
      * 模擬通過api獲取第幾頁的資料
      * @param {每頁多少條記錄} limit 
      * @param {第幾頁} page 
      */
    const getListApi = async(limit, page) => {
      let start = limit * (page - 1);
      let end = limit * page;
      let tempList = dbList.slice(start, end);
      console.log(pageModel,tempList, `獲取第${page}頁資料列表`);
      const result = {
          code: 200,
          message: 'success',
          data: {
              docs: tempList,
              page: page,
              limit: limit,
              total: setTotal,
              pages: Math.ceil(setTotal / 15)
          }
      }
      await sleep(5000)
      return new Promise(resolve => resolve(result))
    };

    const state = {
      offset: 6, // 滾動條與底部距離小於 offset 時觸發load事件
      nodata: false,
    };

    // 控制下拉重新整理的狀態,如果為true則會顯示,則為一直處於載入中,到請求介面成功手動設定false,則代表重新整理成功
    const isRefresh = ref(false);

    // 可以判斷如果是上拉載入的最後一頁的時候,載入成功設定為true,再上拉則不會進行載入了
    const isFinished = ref(false);

  // 是否在載入過程中,如果是true則不會繼續出發onload事件
    const isListLoading = ref(false);  

    onBeforeMount(() => {
      getList()
    });

    // 下拉重新整理列表
    const refreshClick = () => {
      isRefresh.value = true;
      isFinished.value = false;
      isListLoading.value = true;
      // 通過介面呼叫資料
      console.log('呼叫介面成功,並重置頁碼為1');
      successText.value="正在載入資料"
      pageModel.page = 1;
      getList()
    };

      //上拉載入下一頁
    const onLoad = () => {
      // 判斷當前頁碼+1 是否大於總頁數
      // 大於總頁數,結束載入,反之繼續請求
      isListLoading.value = true
      if (pageModel.page + 1 > pageModel.pages) {
        isFinished.value = true
        isListLoading.value = false
        console.warn('資料頁面已超出最大頁,不能再進行請求了')
        return;
      } else {
        pageModel.page = pageModel.page + 1;
        getList()
      }
    };


    const getList = () => {
      getListApi(pageModel.limit,pageModel.page).then(result => {
          console.log(result, 'ssssssssssssss')
          successText.value="1111111111"
          let tempList = result.data.docs
          pageModel.pages = result.data.pages
          pageModel.total = result.data.total
          isListLoading.value = false
          isRefresh.value = false
          if (pageModel.page === 1) {
              dataList.value = tempList
          } else {
              dataList.value=[...dataList.value, ...tempList]
          }
      })
    };

    watch(()=> pageModel.total, (newValue, oldValue) => {
          console.log('watch', newValue> 0, oldValue)
          state.nodata = !(newValue > 0)
    })
  </script>

4、封裝後直接呼叫的全部程式碼片段

可以發現如果每個列表都去做上述主要的五件事情,就會有很多重複的程式碼,
先來看一下直接封裝後寫一個列表有多少程式碼

    <template>
        <list-view :getListApi="getListApi" v-model:pageModel="pageModel" v-model:dataList="dataList">
          <item-view :dataList="dataList"></item-view>
        </list-view>
    </template>
    <script lang="ts" setup>
      import listView from '../../components/list/index.vue';
      import itemView from './item-view.vue';
      import {reactive, ref } from 'vue';
      import type {PageModel } from '../../types/models/index.d';

      const dataList = ref([]);

      const pageModel = reactive<PageModel>({
        page: 1,
        limit: 15,
        total: 0,
        pages: 0,
      });


    const setTotal = 51  // 設定列表總記錄數
    let dbList: any= []  // 通過迴圈向陣列插入測試資料
    for (let i = 0; i < setTotal; i++) {
        dbList.push({
            id: i + 1,
            messageName: '長圖片' + (i + 1),
            createdAt: '2021-07-27 17:06:19',
            createdByDeptName: '百色',
            url: 'http://vue.tuokecat.com/cdn/h5/newslist.jpg',
        })
    }

    /**
    * 模擬通過api獲取第幾頁的資料
    * @param {每頁多少條記錄} limit 
    * @param {第幾頁} page 
    */
    const getListApi = async (limit, page) => {
        let start = limit * (page - 1);
        let end = limit * page;
        let tempList = dbList.slice(start, end);
        console.log(pageModel, tempList, `獲取第${page}頁資料列表`);
        const result = {
            code: 200,
            message: 'success',
            data: {
                docs: tempList,
                page: page,
                limit: limit,
                total: setTotal,
                pages: Math.ceil(setTotal / 15)
            }
        }

        const sleep = (time) => {
            return new Promise(resolve => setTimeout(resolve, time))
        }

        await sleep(1000)
        return new Promise(resolve => resolve(result))
    };
    </script>
  • 解析以上程式碼:

    • 1、最重要便是list-view是我們封裝的元件,只需要引用即可

    • 2、而item-view則是我們列表中每一項的UI檢視佈局和樣式,相當於抽離出來了。跟原來一模一樣

    • 3、主要是準備模擬api請求多了不少程式碼

    • 4、宣告變數和一些定義

  • 封裝的理念

    • 1、將盡可能通用的程式碼,抽離出來,不用再進行復制貼上的操作
    • 2、修改維護邏輯時只需要修改一個地方即可,無需每個列表都要修改
    • 3、每次寫出來的列表bug少,效率高

5、元件封裝的程式碼

  <template>
    <van-pull-refresh
      v-model="isRefresh"
      @refresh="refreshClick"
      :loading-text="'正在請求資料'"
      success-text="資料重新整理成功"
    >
      <van-list
        v-model:loading="isListLoading"
        :finished="isFinished"
        :offset="state.offset"
        finished-text="資料已載入完畢"
        :immediate-check="false"
        @load="onLoad"
      >
        <slot></slot>
      </van-list>
    </van-pull-refresh>
    <div v-if="state.nodata === true">
      <van-empty description="沒有資料" />
    </div>
  </template>
  <script lang="ts" setup>
  import { PullRefresh as VanPullRefresh, List as VanList, Empty as VanEmpty } from 'vant';
  import { onBeforeMount, ref, PropType, watch } from 'vue';
  import { PageModel } from '/@/types/models';
  const emits = defineEmits(['getList', 'update:pageModel', 'update:dataList']);
  const props = defineProps({
    pageModel: {
      type: Object as PropType<PageModel>,
      default: () => { },
    },
    dataList: {
      type: [Array],
      default: () => []
    },
    getListApi: {
      type: Function,
      default: () => { }
    }
  });

  const state = {
    offset: 6, // 滾動條與底部距離小於 offset 時觸發load事件
    nodata: false,
  };

  // 控制下拉重新整理的狀態,如果為true則會顯示,則為一直處於載入中,到請求介面成功手動設定false,則代表重新整理成功
  const isRefresh = ref(false);

  // 可以判斷如果是上拉載入的最後一頁的時候,載入成功設定為true,再上拉則不會進行載入了
  const isFinished = ref(false);

  // 是否在載入過程中,如果是true則不會繼續出發onload事件
  const isListLoading = ref(false);

  onBeforeMount(() => {
    // emits('getList');
    console.log('getList')
    getList()
  });

  const refreshClick = () => {
    isRefresh.value = false;
    isFinished.value = false;
    isListLoading.value = true;
    // 通過介面呼叫資料
    console.log('呼叫介面成功');
    props.pageModel.page = 1;
    emits('update:pageModel', props.pageModel)
    // emits('getList');
    getList()
  };

  const onLoad = () => {
    isListLoading.value = true
    if (props.pageModel.page + 1 > props.pageModel.pages) {
      isFinished.value = true
      isListLoading.value = false
      console.warn('資料頁面已超出最大頁,不能再進行請求了')
      return;
    } else {
      props.pageModel.page = props.pageModel.page + 1;
    }
    emits('update:pageModel', props.pageModel)
    // emits('getList');
    getList()
  };
  const dataList: any = ref([]);
  const getList = () => {
    props.getListApi(props.pageModel.limit, props.pageModel.page).then((result: any) => {
      console.log(result, 'ssssssssssssss')
      let tempList: [] = result.data.docs
      props.pageModel.limit = result.data.limit
      props.pageModel.page = result.data.page
      props.pageModel.pages = result.data.pages
      props.pageModel.total = result.data.total
      isListLoading.value = false
      isRefresh.value = false
      if (props.pageModel.page === 1) {
        dataList.value = tempList
      } else {
        dataList.value = [...props.dataList, ...tempList]
      }
      emits('update:dataList', dataList.value)
      emits('update:pageModel', props.pageModel)
    })
  };

  // 判斷是否有資料
  watch(() => props.pageModel.total, (newValue, oldValue) => {
    console.log('watch', newValue > 0, oldValue)
    state.nodata = !(newValue > 0)
  })
  </script>
  • 解析封裝的程式碼
    • 1、通過watch 監測tatal,判斷是否有資料,來確定是否要顯示沒有資料時的預設圖片
    • 2、將請求通過props進行傳遞,在封裝的元件中進行統一處理,當然這裡就要要求使用元件的介面要統一引數
    • 3、請求資料後要將資料列表和分頁資料通過emits進行回傳父元件,用於顯示列表資料
    • 4、下拉重新整理判斷仍然存在統一封裝
    • 5、上拉載入列表資料判斷仍熱存在統一封裝
    • 6、最後一次載入資料進行判斷處理
    • 7、TypeScript用的還不夠熟練,資料列表這一塊的封裝還不到位,爭取有時間再進行深入一下。

總結

  • 實際使用過程中還可以繼續優化很多的細節工作,比如有些列表一次性載入即可,不需要進行下拉重新整理或者上拉載入的功能,都可以通過傳遞引數進行控制等等。
  • 封裝的過程就是對那些重複性的工作進行提煉,提高程式碼的複用性,減少程式碼的拷貝貼上,這樣呼叫元件後的程式碼也方便維護和測試工作,相對來說穩定性也更加強勁。

https://github.com/aehyok/vue-qiankun/vite-vue+react+demo/vite-h5/src/views/news-list/
本文中涉及到的程式碼連結,其中的news-before是沒有封裝的程式碼,news-after則是封裝後的程式碼。

https://github.com/aehyok/2022
最後自己每天工作中的筆記記錄倉庫,主要以文章連結和問題處理方案為主。

相關文章