前端提升生产力系列三(vant3 vue3 移动端H5下拉刷新,上拉加载组件的封装)

  • A+
所属分类:Web前端
摘要

| 在日常的移动端开发中,经常会遇到列表的展示,以及数据量变多的情况下还会有上拉和下拉的操作。进入新公司后发现移动端好多列表,但是在看代码的时候发现,每个列表都是单独的代码,没有任何的封装,都是通过vant组件,里面充满了过多的重复代码,在有bug或者有需求变更的时候,每次的改动都要对很多个相同逻辑的页面组件进行修改,于是花了一点时间,将其进行封装,发现还是节省了很多的时间。自己做一个记录。

| 在日常的移动端开发中,经常会遇到列表的展示,以及数据量变多的情况下还会有上拉和下拉的操作。进入新公司后发现移动端好多列表,但是在看代码的时候发现,每个列表都是单独的代码,没有任何的封装,都是通过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、实现效果的演示

前端提升生产力系列三(vant3 vue3 移动端H5下拉刷新,上拉加载组件的封装)

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
最后自己每天工作中的笔记记录仓库,主要以文章链接和问题处理方案为主。