前端提升生產力系列三(vant3 vue3 移動端H5下拉刷新,上拉加載組件的封裝)

| 在日常的移動端開發中,經常會遇到列表的展示,以及數據量變多的情況下還會有上拉和下拉的操作。進入新公司後發現移動端好多列表,但是在看代碼的時候發現,每個列表都是單獨的代碼,沒有任何的封裝,都是通過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
最後自己每天工作中的筆記記錄倉庫,主要以文章鏈接和問題處理方案爲主。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章