vue实现瀑布流组件滑动加载更多
建议先看vue瀑布流组件上拉加载更多再来食用本文,如果直接想看源码文末就是~
文末新增组件优化,之所以没有删优化前的代码是想让以后自己还能看到走过的路。
上一篇讲到在项目中使用上拉加载更多组件,但是由于实际项目开发中由于需求变更或者说在webview中上拉加载有些机型在上拉时候会把webview也一起上拉导致上拉加载不灵敏等问题,我们有时候也会换成滑动到底部自动加载的功能。
既然都是加载更多,很多代码思想势必相似,主要区别在于上拉和滑动到底部这个操作上,所以,我们需要注意:
1、上拉加载是point指针touch触摸事件,现在因为是滑动加载,需要添加scroll事件去监听然后执行相应回调
2、上拉加载主要计算触摸滚动距离,滑动加载主要计算container底部和视窗上边缘的距离
事件绑定改成:
mounted() { ??? this.dom.addEventListener('scroll', this.scroll, false) ??? }, beforeDestroy() { ??? this.dom.removeEventListener('scroll', this.scroll, false) ??? },
事件回调改为:
/** * 滚动钩子 */ scroll() { const viewHeight = global.innerHeight let parentNode if (this.container !== global) { parentNode = this.$el } else { parentNode = this.$el.parentNode } if (parentNode) { // 获取Vue实例使用的根 DOM 元素相对于视口的位置 const rect = parentNode.getBoundingClientRect() // this.distance 离底部多少距离开始加载 // 如果此元素底边距离视口顶部的距离小于视口高度加上distance之和,就加载下一页 if ((rect.bottom <= viewHeight + this.distance) && this.loadable && !this.loading) { this.load() } } },
源码如下:
<template> <div class="loadmore" ref="loadmore"> <div class="loadmore__body"> <slot></slot> </div> <div class="loadmore__footer"> <span v-if="loading"> <i class="tc-loading"></i> <span>正在加载</span> </span> <span v-else-if="loadable">加载更多</span> <span v-else>没有更多了</span> </div> </div> </template> <script type="text/babel"> import axios from 'axios' const CancelToken = axios.CancelToken export default { data() { return { /** * 总页数(由服务端返回) * @type {number} */ count: 0, /** * 是否正在拖拽中 * @type {boolean} */ dragging: false, /** * 已加载次数 * @type {number} */ times: 0, /** * 已开始记载 * @type {boolean} */ started: false, /** * 正在加载中 * @type {boolean} */ loading: false, dom: null, } }, props: { /** * 初始化后自动开始加载数据 */ autoload: { type: Boolean, default: true, }, /** * 离组件最近的可滚动父级元素(用于监听事件及获取滚动条位置) */ container: { // Selector or Element default: () => (global), }, /** * Axios请求参数配置对象 * {@link https://github.com/mzabriskie/axios#request-config} */ options: { type: Object, default: null, }, /** * 起始页码 */ page: { type: Number, default: 1, }, /** * 每页加载数据条数 */ rows: { type: Number, default: 10, }, /** * 数据加载请求地址 */ url: { type: String, default: '', }, /** * 距离底部多远加载 */ distance: { type: Number, default: 200, }, }, computed: { /** * 是否可以加载 * @returns {boolean} 是与否 */ loadable() { return !this.started || (this.page + this.times) <= this.count }, }, mounted() { if (this.container !== global) { this.dom = document.querySelector(this.container) } else { this.dom = this.container } if (!this.dom) { return } this.dom.addEventListener('scroll', this.scroll, false) if (this.autoload && !this.loading) { this.load() } }, // eslint-disable-next-line beforeDestroy() { if (this.dom) { this.dom.removeEventListener('scroll', this.scroll, false) } }, methods: { /** * 滚动钩子 */ scroll() { const viewHeight = global.innerHeight let parentNode if (this.container !== global) { parentNode = this.$el } else { parentNode = this.$el.parentNode } if (parentNode) { const rect = parentNode.getBoundingClientRect() if ((rect.bottom <= viewHeight + this.distance) && this.loadable && !this.loading) { this.load() } } }, /** * 加载一组数据的方法 */ load() { if (this.loading) { return } this.started = true this.loading = true const params = { currentPage: this.page + this.times, pageSize: this.rows, } const options = Object.assign({}, this.options, { url: this.url, cancelToken: new CancelToken((cancel) => { this.cancel = cancel }), }) if (String(options.method).toUpperCase() === 'POST') { options.data = Object.assign({}, options.data, params) } else { options.params = Object.assign({}, options.params, params) } this.$axios.request(options).then((res) => { const data = res.result this.times += 1 this.loading = false this.count = data.pageCount this.$emit('success', data.list) this.$emit('complete') }).catch((e) => { this.loading = false this.$emit('error', e) this.$emit('complete') }) }, /** * 重置加载相关变量 */ reset() { this.count = 0 this.times = 0 this.started = false this.loading = false }, /** *重新开始加载 */ restart() { this.reset() this.load() }, }, } </script>
―――――――-我是分割线――――――?C
2017-09-18 组件优化
我们在写组件时候,通常会大致先分为两种,业务组件和通用组件,业务组件通和业务逻辑相关,一般作为一个业务模块的局部组件, 比如列表中的列表项组件;通用组件适用面广,不会和业务有牵扯,比如弹出框组件。
所以我们开始封装一个组件的时候,就要划分业务逻辑,做什么,不做什么,从外部接收什么,向外部提供什么,这个边界应该非常清楚
但是之前的封装的loadmore组件不太符合这一点,可能是项目一开始比较关注功能的实现,将其当成的一个业务组件撰写,现在有一点需要优化:
之前我们传入了各种请求相关的参数,包括url在组件内部完成加载和页码控制等一系列操作,显然这不太符合组件功能职责单一化的原则, 其实组件内部并不关心加载到第几页或者是需要请求什么后端接口,而只要父组件告诉自己是否还可以加载就可以了, 至于加载请求列表,子组件通知父组件去加载就OK。
最终我们得到一个和业务完全分离的通用组件,代码如下:
<template> <div class="loadmore" ref="loadmore"> <div class="loadmore__body"> <slot></slot> </div> <div class="loadmore__footer"> <span v-if="loading"> <i class="tc-loading"></i> <span>正在加载</span> </span> <span v-else-if="loading">正在加载...</span> <span v-else>没有更多了</span> </div> </div> </template> <script> export default { props: { /** * 是否加载 */ loading: { type: Boolean, default: false, }, /** * 滚动外部容器, 选择器字符串 */ container: { default: () => (global), }, /** * 距离底部多远加载 */ distance: { type: Number, default: 200, }, }, data() { return { dom: null, // 外部容器dom } }, mounted() { if (this.container !== global) { this.dom = document.querySelector(this.container) } else { this.dom = this.container } if (!this.dom) { return } this.dom.addEventListener('scroll', this.scroll, false) if (this.autoload && !this.loading) { this.load() } }, methods: { /** * 滚动钩子 */ scroll() { if (!this.loading) { return } const viewHeight = global.innerHeight let parentNode if (this.container !== global) { parentNode = this.$el } else { parentNode = this.$el.parentNode } if (parentNode) { const rect = parentNode.getBoundingClientRect() if ((rect.bottom <= viewHeight + this.distance)) { this.load() } } }, /** * 加载一组数据的方法 */ load() { this.$emit('load') }, }, beforeDestroy() { if (this.dom) { this.dom.removeEventListener('scroll', this.scroll, false) } }, } </script> <style lang="stylus" rel="stylesheet/stylus" scoped> .loadmore { user-select: none; &__footer { color: #e4e4e4; padding: 20px; text-align: center; } .tc-loading { ~ span { vertical-align: middle; } } } </style>