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