Creamos una paginación en caché que no teme la adición inesperada de datos a la base de datos

Si su sitio contiene una gran cantidad de contenido, entonces, para mostrarlo, el usuario debe compartirlo de una forma u otra.





Todos los métodos que conozco tienen inconvenientes y traté de crear un sistema que pueda resolver algunos de ellos sin ser demasiado difícil de implementar.





Métodos existentes

1. Paginación (división en páginas separadas)

Un ejemplo del sitio habr.com
Un ejemplo del sitio habr.com

La paginación o división en páginas separadas es una forma bastante antigua de dividir contenido, que también se usa en Habré. La principal ventaja es su versatilidad y facilidad de implementación tanto desde el lado del servidor como desde el lado del cliente.





El código para solicitar datos de la base de datos suele limitarse a un par de líneas.





Aquí y más ejemplos en el lenguaje arangodb aql, oculté el código del servidor porque todavía no hay nada interesante.





//   20    . 

LET count = 20
LET offset = count * ${page}

FOR post IN posts
	SORT post.date DESC //     
	LIMIT offset, count
	RETURN post
      
      



En el lado del cliente, solicitamos y mostramos el resultado resultante, utilizo vuejs con nuxtjs como ejemplo, pero se puede hacer lo mismo en cualquier otra pila, firmaré todos los puntos específicos de vue.





# https://example.com/posts?page=3
main.vue

<template> <!--  template   body	-->
	<div>
    <template v-for="post in posts"> <!--   	-->
			<div :key="post.id">
				{{ item.title }}
			</div>
    </template>
	</div>
</template>

<script>
export default {
	data() {
		return {
			posts: [], //    
		}
	},
  computed: { //   this,    
  	currentPage(){
      //            +
      return +this.$route.query.page || 0
    },
  },
	async fetch() { //    
    
		const page = this.currentPage
    
    //   ,      
  	this.posts = await this.$axios.$get('posts', {params: {page}})
  }
}
</script>
      
      



Ahora tenemos todas las publicaciones en la página mostradas, pero espera, ¿cómo cambiarán los usuarios entre las páginas? Agreguemos un par de botones para pasar las páginas.





<template> <!--  template   body	-->
  <div>
    <div>
      <template v-for="post in posts"> <!--   	-->
        <div :key="post.id">
          {{ item.title }}
        </div>
      </template>
    </div>
    <div>  <!--  	-->
      <button @click="prev">
      	 
      </button>
      <button @click="next">
      	 
      </button>
    </div>
  </div>
</template>

<script>
export default {
  //...     
  
  methods: {//    
    prev(){
			const page = this.currentPage()
      if(page > 0)
        //   https://example.com/posts?page={page - 1}
				this.$router.push({query: {page: page - 1}})
    },
    next(){
			const page = this.currentPage()
      if(page < 100) //          100 
        //   https://example.com/posts?page={page + 1}
				this.$router.push({query: {page: page + 1}})
    },
  },
}
</script>
      
      



Contras de este método





  • .





  • , . 2, , 3, 4 , . GET .





  • , , .





2.

, .





, .





№3 , 2 , , id , 40 ? 3  ,   , . 2 ( 20 ). !





:





  • , , , . , mvp.





  • , , . 2 . -,   . -,   , , .  ,   , , , .





  • , . , . !





, , .





, .





0, 1, (page) , . , offset ().





LET count = 20
LET offset = ${offset}

FOR post IN posts
	SORT post.date ASC //       
	LIMIT offset, count
	RETURN post
      
      



, GET "/?offset=0" .





, , ( nodejs):





async getPosts({offset}) {
  const isOffset = offset !== undefined
  if (isOffset && isNaN(+offset)) throw new BadRequestException()

	const count = 20
	//      ,    
	if (offset % count !== 0) throw new BadRequestException()

	const sort = isOffset ? `
		SORT post.date DESC
		LIMIT ${+offset}, ${count}
	` : `
		SORT post.date ASC
		LIMIT 0, ${count * 2} //        *
	`

	const q = {
		query: `
			FOR post IN posts
			${sort}
			RETURN post
		`,
		bindVars: {}
	}

	//         
	const cursor = await this.db.query(q, {fullCount: true, count: isOffset})
	const fullCount = cursor.extra.stats.fullCount

	/* 
  	*        count{20}      2  [21-39] 
		       .     
		     20      1-  c count{20} 
	*/

  let data;
	if (isOffset) {
    //          
		const allow = offset <= fullCount - cursor.count - count
		if (!allow) throw new NotFoundException()
		//    , .        
		data = (await cursor.all()).reverse()
	} else {
		const all = await cursor.all()
		if (fullCount % count === 0) {
      //   20 ,     ,      ,    
			data = all.slice(0, count) 
		} else {
      /*  ,         0-20 ,
            20     ,
             0-20   ,
                 40 
          
      */
			const pagesCountUp = Math.ceil(fullCount / count)
			const resultCount = fullCount - pagesCountUp * count + count * 2
			data = all.slice(0, resultCount)
		}
	}

	if (!data.length) throw new NotFoundException()

	return { fullCount, count: data.length, data }
}
      
      



:





  • id .





  • , id offset.





  • (





:





  • , , , null , , .. , , "null-" , null- .





  • ( ), . ( id).





№2.





<template>
	<div>
		<div ref='posts'>
			<template v-for="post in posts">
				<div :key="post.id" style="height: 200px"> <!--   ,    	-->
					{{ item.title }}
				</div>
			</template>
		</div>
		<div> <!--     .   	-->
			<button @click="prev" v-if="currentPage > 1">
				 
			</button>
		</div>
	</div>
</template>

<script>
const count = 20
export default {
	data() {
		return {
			posts: [],
			fullCount: 0,
			pagesCount: 0,
			dataLoading: true,
			offset: undefined,
		}
	},
	async fetch() {
		const offset = this.$route.query?.offset
		this.offset = offset
		this.posts = await this.loadData(offset)
		setTimeout(() => this.dataLoading = false)
	},
	computed: {
		currentPage() {
			return this.offset === undefined ? 1 : this.pageFromOffset(this.offset)
		}
	},
	methods: {
     //         
		pageFromOffset(offset) {
			return offset === undefined ? 1 : this.pagesCount - offset / count
		},
		offsetFromPage(page) {
			return page === 1 ? undefined : this.pagesCount * count - count * page
		},
		prev() {
			const offset = this.offsetFromPage(this.currentPage - 1)
			this.$router.push({query: {offset}})
		},
		async loadData(offset) {
			try {
				const data = await this.$axios.$get('posts', {params: {offset}})
				this.fullCount = data.fullCount
				this.pagesCount = Math.ceil(data.fullCount / count)
				//         
				if (this.fullCount % count !== 0)
					this.pagesCount -= 1
				return data.data
			} catch (e) {
				//...  404    
				return []
			}
		},
		onScroll() {
			//  1000      
			const load = this.$refs.posts.getBoundingClientRect().bottom - window.innerHeight < 1000
			const nextPage = this.pageFromOffset(this.offset) + 1
			const nextOffset = this.offsetFromPage(nextPage)
			if (!this.dataLoading && load && nextPage <= this.pagesCount) {
				this.dataLoading = true
				this.offset = nextOffset
				this.loadData(nextOffset).then(async (data) => {
					const top = window.scrollY
					//       
					this.posts.push(...data)
					await this.$router.replace({query: {offset: nextOffset}})
					this.$nextTick(() => {
						//    viewport      
						window.scrollTo({top});
						this.dataLoading = false
					})
				})
			}
		}
	},
	mounted() {
		window.addEventListener('scroll', this.onScroll)
	},
	beforeDestroy() {
		window.removeEventListener('scroll', this.onScroll)
	},
}
</script>
      
      



. , , .





:

1 , , ( ):





< 1 ... 26 [27] 28 ... 255 >







< [1] 2 3 4 5 ... 255 >







< 1 ... 251 252 253 254 [255] >







La base del método para generar paginación se toma de esta discusión: https://gist.github.com/kottenator/9d936eb3e4e3c3e02598#gistcomment-3238804 y se cruza con mi solución.





Mostrar continuación de bonificación

Primero, debe agregar este método auxiliar dentro de la etiqueta <script>





const getRange = (start, end) => Array(end - start + 1).fill().map((v, i) => i + start)
const pagination = (currentPage, pagesCount, count = 4) => {
	const isFirst = currentPage === 1
	const isLast = currentPage === pagesCount

	let delta
	if (pagesCount <= 7 + count) {
		// delta === 7: [1 2 3 4 5 6 7]
		delta = 7 + count
	} else {
		// delta === 2: [1 ... 4 5 6 ... 10]
		// delta === 4: [1 2 3 4 5 ... 10]
		delta = currentPage > count + 1 && currentPage < pagesCount - (count - 1) ? 2 : 4
		delta += count
		delta -= (!isFirst + !isLast)
	}

	const range = {
		start: Math.round(currentPage - delta / 2),
		end: Math.round(currentPage + delta / 2)
	}

	if (range.start - 1 === 1 || range.end + 1 === pagesCount) {
		range.start += 1
		range.end += 1
	}

	let pages = currentPage > delta
		 ? getRange(Math.min(range.start, pagesCount - delta), Math.min(range.end, pagesCount))
		 : getRange(1, Math.min(pagesCount, delta + 1))

	const withDots = (value, pair) => (pages.length + 1 !== pagesCount ? pair : [value])

	if (pages[0] !== 1) {
		pages = withDots(1, [1, '...']).concat(pages)
	}

	if (pages[pages.length - 1] < pagesCount) {
		pages = pages.concat(withDots(pagesCount, ['...', pagesCount]))
	}
	if (!isFirst) pages.unshift('<')
	if (!isLast) pages.push('>')

	return pages
}
      
      



Agregar métodos que faltan





<template>
	<div ref='posts'>
		<div>
			<div v-for="post in posts" :key="item.id">{{ post.title }}</div>
		</div>
		<div style="position: fixed; bottom: 0;"> <!--      -->
			<template v-for="(i, key) in pagination">
				<button v-if="i === '...'" :key="key + i" @click="selectPage()">{{ i }}</button>
				<button :key="i" v-else :disabled="currentPage === i" @click="loadPage(pagePaginationOffset(i))">{{ i }}</button>
			</template>
		</div>
	</div>
</template>

<script>
export default {
	data() {
		return {
			posts: [],
			fullCount: 0,
			pagesCount: 0,
			interval: null,
			dataLoading: true,
			offset: undefined,
		}
	},
	async fetch() {/*   */},
	computed: {
		currentPage()  {/*   */},
		
		//          
		pagination() {
			return this.pagesCount ? pagination(this.currentPage, this.pagesCount) : []
		},
	},
	methods: {
		pageFromOffset(offset) {/*   */},
		offsetFromPage(page) {/*   */},
		async loadData(offset) {/*   */},
		onScroll() {/*   */},

		//       
		loadPage(offset) {
			window.scrollTo({top: 0})
			this.dataLoading = true

			this.loadData(offset).then((data) => {
				this.offset = offset
				this.posts = data
				this.$nextTick(() => {
					this.dataLoading = false
				})
			})
		},
		//     
		pagePaginationOffset(item) {
			if (item === '...') return undefined
			let page = isNaN(item) ? this.currentPage + (item === '>') - (item === '<') : item
			return page <= 1 ? undefined : this.offsetFromPage(page)
		},
		//       
		selectPage() {
			const page = +prompt("   ");
			this.loadPage(this.offsetFromPage(page))
		},
	},
	mounted() {
		window.addEventListener('scroll', this.onScroll)
	},
	beforeDestroy() {
		window.removeEventListener('scroll', this.onScroll)
	},
}
</script>

      
      



Ahora, si es necesario, puede ir a la página deseada.








All Articles