在做移动端的需求时,我们经常会开发一些列表页,这些列表页大多数有着相似的功能:分页获取列表、上拉加载、下拉刷新···
在 Vue 出来 compositionAPI 之前,我们想要复用这样的逻辑还是比较麻烦的,好在现在 Vue2.7+都支持 compositionAPI语法了,这篇文章我将 手把手带你用 compositionAPI 封装一个名为 useList 的 hooks来实现列表页的逻辑复用。
一个列表,最基本的需求应该包括: 发起请求,获取到列表的数组,然后将该数组渲染成相应的 DOM 节点。要实现这个功能,我们需要以下变量:
template模板中使用 v-for来遍历渲染成我们想要的样子。axios的实例有了上面的分析,我们可以很轻松地在 setup中写出如下代码:
import { ref } from 'vue'
import axios from 'axios' // 简单示例,就不给出封装axios的代码了
const list = ref([])
const listReq = () => {
axios.get('/url/to/getList').then((res) => {
list.value = res.list
})
}
listReq()
这样,我们就完成了一个基本的列表需求的逻辑部分。大部分的列表需求都是类似的逻辑,既然如此,Don't Repeat Yourself!(不要重复写你的代码!),我们来把它封装成通用的方法:
useList的时候都拿到独属于自己的那一份数据。是不是感觉很熟悉?对的,就是以前的 data为什么是一个函数那个问题!所以我们的 useList是需要导出一个函数,我们从这个函数中获取数据与方法。让这个函数导出一个对象/数组,这样调用的时候 解构就可以拿到我们需要的变量和方法了// useList.js 中
const useList = () => {
// 待补充的函数体
return {}
}
export default useList
// useList.js 中
import { ref } from 'vue'
const useList = (listReq) => {
if (!listReq) {
return new Error('请传入接口调用方法!')
}
const list = ref([])
const getList = () => {
listReq().then((res) => (list.value = res.list))
}
return {
list,
getList,
}
}
export default useList
这样,我们就完成了一个简单的列表 hooks,使用的时候直接:
// setup中
import useList from '@/utils'
const { list, getList } = useList(axios.get('url/to/get/list'))
getList()
等等!列表好像不涉及到 DOM操作,那咱们再偷点懒,直接在 useList内部就调用了吧!
// useList.js中
import { ref } from 'vue'
const useList = (listReq) => {
if (!listReq) {
return new Error('请传入接口调用方法!')
}
const list = ref([])
const getList = () => {
listReq().then((res) => (list.value = res.list))
}
getList() // 直接初始化,省去在外面初始化的步骤
return {
list,
getList,
}
}
export default useList
这时有老哥要说了,那我要是一个页面有多个列表怎么办?嘿嘿,别忘了,解构的时候是可以重命名的
// setup中
const { list: goodsList, getList: getGoodsList } = useList(
axios.get('/url/get/goods')
)
const { list: recommendList, getList: getRecommendList } = useList(
axios.get('/url/get/goods')
)
这样,我们就同时在一个页面里面,获取到了商品列表以及推荐列表所需要的变量与方法啦
如果数据量比较大的话,所有的数据全部拿出来渲染显然不合理,所以我们一般要进行分页处理,我们来分析一下这个需求:
pageInfo对象中loadmore函数initList函数需求分析好了,代码实现起来就简单了,废话少说,上代码!
// useList.js中
import { ref } from 'vue'
const useList = (listReq) => {
if (!listReq) {
return new Error('请传入接口调用方法!')
}
const list = ref([])
// 新增pageInfo对象保存分页数据
const pageInfo = ref({
pageNum: 1,
pageSize: 10,
total: 0,
})
const getList = () => {
// 分页数据作为参数传递给接口调用函数即可
// 将请求这个Promise返回出去,以便链式then
return listReq(pageInfo.value).then((res) => {
list.value = res.list
// 更新总数量
pageInfo.value.total = res.total
// 返回出去,交给then默认的Promise,以便后续使用
return res
})
}
// 新增加载下一页的函数
const loadmore = () => {
// 下一页,那咱们把当前页自增一下就行了
pageInfo.value.pageNum += 1
// 如果已经是最后一页了(本次获取到空数组)
getList().then((res) => {
if (!res.list.length) {
uni.showToast({
title: '没有更多了',
icon: 'none',
})
}
})
}
// 新增初始化
const initList = () => {
// 初始化一般是要把所有的查询条件都初始化,这里只有分页,咱就回到第一页就行
pageInfo.value.pageNum = 1
getList()
}
getList()
return {
list,
getList,
loadmore,
initList,
}
}
export default useList
完工!跑起来试试,Perfec......等等,好像不太对...
加载更多,应该是把两次请求的数据合并到一起渲染出来才对,这怎么直接替换掉了?
回头看看代码,原来是咱们漏了拼接的逻辑,补上,补上
// useList.js中
// ...省略其余代码
const getList = () => {
// 分页数据作为参数传递给接口调用函数即可
return listReq(pageInfo.value).then((res) => {
// 当前页不为1则是加载更多,需要拼接数据
if (pageInfo.value.pageNum === 1) {
list.value = res.list
} else {
list.value = [...list.value, ...res.list]
}
pageInfo.value.total = res.total
return res
})
}
// ...省略其余代码
上面的分页版,我们给出了 加载更多和 初始化列表功能,但是还是要手动调用。仔细想想,咱们刷新列表,一般都是在页面顶部下拉的时候刷新的;而加载更多,一般都是在滚动到底部的时候加载的。既然都是一样的触发时机,那咱们继续封装吧!
onPullDownRefresh和 onReachBottom钩子,在其中处理相关逻辑即可scroll-view中,还是需要手动处理,因此上面的函数咱们依然需要导出钩子函数(hooks)接受一个回调函数作为参数,咱们直接把上面的函数传入即可
需要注意的是,uni-app 中,下拉刷新的动画需要手动关闭,咱们还需要改造一下 listReq函数
// useList中
import { onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
// ...省略其余代码
onPullDownRefresh(initList)
onReachBottom(loadmore)
const getList = () => {
// 分页数据作为参数传递给接口调用函数即可
return listReq(pageInfo.value)
.then((res) => {
// ...省略其余代码
})
.finally((info) => {
// 不管成功还是失败,关闭下拉刷新的动画
uni.stopPullDownRefresh()
// 在最后再把前面返回的消息return出去,以便后续处理
return info
})
}
// ...省略其余代码
其实在实际开发中,我们在发起请求时可能还需要其他的参数,上面我们都是固定的只有分页的参数,可以稍加改造
可能大家第一反应是多一个参数,或者用 展开运算符 (...)再定义一个形参就行了。这么做肯定是没问题的,不过在这里的话不够优雅~
我们这里是要增加一个传给后端的参数,一般都是一起以 JSON 对象的形式传过去,既然如此,那咱们把所有的参数都用一个对象接受,发起请求的时候和分页参数对象合并为一个对象,代码的可读性会更高,使用者在使用时也可以自由地定义 key-value 键值对
// useList中
const useList = (listReq, data) => {
// ...省略其余代码
// 判断第二个参数是否是对象,以免后面使用展开运算符时报错
if (data && Object.prototype.toString.call(data) !== '[object Object]') {
return new Error('额外参数请使用对象传入')
}
const getList = () => {
const params = {
...pageInfo.value,
...data,
}
return listReq(params).then((res) => {
// ...省略其余代码
})
}
// ...省略其余代码
}
// ...省略其余代码
有些时候我们的列表是在页面中间,不需要触底加载更多;有时候我们可能需要在不同的地方调用相同的接口,但是需要获取的数据量不一样....
为了适应各种各样的需求,我们可以稍加改造,添加一个带有默认值的配置对象,
// useList.js中
const defaultConfig = {
pageSize: 10, // 每页数量,其实也可以在data里面覆盖
needLoadMore: true, // 是否需要下拉加载
data: {}, // 这个就是给接口用的额外参数了
// 还可以根据自己项目需求添加其他配置
}
// 添加一个有默认值的参数,依然满足大部分列表页传入接口即可使用的需求
const useList = (listReq, config = defaultConfig) => {
// 解构的时候赋上初始值,这样即使配置参数只传了一个参数,也不影响其他的配置
const {
pageSize = defaultConfig.pageSize,
needLoadMore = defaultConfig.needLoadMore,
data = defaultConfig.data,
} = config
// 应用相应的配置
if (needLoadMore) {
onReachBottom(loadmore)
}
const pageInfo = ref({
pageNum: 1,
pageSize,
total: 0,
})
// ...省略其余代码
}
// ...省略其余代码
这样一来,咱们就实现了一个满足大部分移动端列表页的逻辑复用 hooks
web 端的几乎只有加载更多(翻页)的时候逻辑不太一样,不需要拼接数据,在封装的时候可以把分页器的处理逻辑一起封装进来
类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc
使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta
查看Ruby的CSV库的文档,我非常确定这是可能且简单的。我只需要使用Ruby删除CSV文件的前三列,但我没有成功运行它。 最佳答案 csv_table=CSV.read(file_path_in,:headers=>true)csv_table.delete("header_name")csv_table.to_csv#=>ThenewCSVinstringformat检查CSV::Table文档:http://ruby-doc.org/stdlib-1.9.2/libdoc/csv/rdoc/CSV/Table.html
我正在编写一个包含C扩展的gem。通常当我写一个gem时,我会遵循TDD的过程,我会写一个失败的规范,然后处理代码直到它通过,等等......在“ext/mygem/mygem.c”中我的C扩展和在gemspec的“扩展”中配置的有效extconf.rb,如何运行我的规范并仍然加载我的C扩展?当我更改C代码时,我需要采取哪些步骤来重新编译代码?这可能是个愚蠢的问题,但是从我的gem的开发源代码树中输入“bundleinstall”不会构建任何native扩展。当我手动运行rubyext/mygem/extconf.rb时,我确实得到了一个Makefile(在整个项目的根目录中),然后当
是否有类似“RVMuse1”或“RVMuselist[0]”之类的内容而不是键入整个版本号。在任何时候,我们都会看到一个可能包含5个或更多ruby的列表,我们可以轻松地键入一个数字而不是X.X.X。这也有助于rvmgemset。 最佳答案 这在RVM2.0中是可能的=>https://docs.google.com/document/d/1xW9GeEpLOWPcddDg_hOPvK4oeLxJmU3Q5FiCNT7nTAc/edit?usp=sharing-知道链接的任何人都可以发表评论
我发现ActiveRecord::Base.transaction在复杂方法中非常有效。我想知道是否可以在如下事务中从AWSS3上传/删除文件:S3Object.transactiondo#writeintofiles#raiseanexceptionend引发异常后,每个操作都应在S3上回滚。S3Object这可能吗?? 最佳答案 虽然S3API具有批量删除功能,但它不支持事务,因为每个删除操作都可以独立于其他操作成功/失败。该API不提供任何批量上传功能(通过PUT或POST),因此每个上传操作都是通过一个独立的API调用完成的
Sinatra新手;我正在运行一些rspec测试,但在日志中收到了一堆不需要的噪音。如何消除日志中过多的噪音?我仔细检查了环境是否设置为:test,这意味着记录器级别应设置为WARN而不是DEBUG。spec_helper:require"./app"require"sinatra"require"rspec"require"rack/test"require"database_cleaner"require"factory_girl"set:environment,:testFactoryGirl.definition_file_paths=%w{./factories./test/
我已经在Sinatra上创建了应用程序,它代表了一个简单的API。我想在生产和开发上进行部署。我想在部署时选择,是开发还是生产,一些方法的逻辑应该改变,这取决于部署类型。是否有任何想法,如何完成以及解决此问题的一些示例。例子:我有代码get'/api/test'doreturn"Itisdev"end但是在部署到生产环境之后我想在运行/api/test之后看到ItisPROD如何实现? 最佳答案 根据SinatraDocumentation:EnvironmentscanbesetthroughtheRACK_ENVenvironm
我正在阅读SandiMetz的POODR,并且遇到了一个我不太了解的编码原则。这是代码:classBicycleattr_reader:size,:chain,:tire_sizedefinitialize(args={})@size=args[:size]||1@chain=args[:chain]||2@tire_size=args[:tire_size]||3post_initialize(args)endendclassMountainBike此代码将为其各自的属性输出1,2,3,4,5。我不明白的是查找方法。当一辆山地自行车被实例化时,因为它没有自己的initialize方法
我有两个Rails模型,即Invoice和Invoice_details。一个Invoice_details属于Invoice,一个Invoice有多个Invoice_details。我无法使用accepts_nested_attributes_forinInvoice通过Invoice模型保存Invoice_details。我收到以下错误:(0.2ms)BEGIN(0.2ms)ROLLBACKCompleted422UnprocessableEntityin25ms(ActiveRecord:4.0ms)ActiveRecord::RecordInvalid(Validationfa