Kami membuat pagination dalam cache yang tidak takut dengan penambahan data yang tidak terduga ke database

Jika situs Anda berisi konten dalam jumlah besar, maka untuk menampilkannya, pengguna harus membagikannya dengan satu atau lain cara.





Semua metode yang saya tahu memiliki kekurangan dan saya mencoba membuat sistem yang dapat menyelesaikan beberapa di antaranya tanpa terlalu sulit untuk diterapkan.





Metode yang ada

1. Penomoran halaman (pembagian menjadi halaman terpisah)

Contoh dari situs habr.com
Contoh dari situs habr.com

Penomoran halaman atau pemisahan menjadi halaman terpisah adalah cara yang cukup lama untuk membagi konten, yang juga digunakan di Habré. Keunggulan utamanya adalah keserbagunaan dan kemudahan penerapannya baik dari sisi server maupun dari sisi klien.





Kode untuk meminta data dari database paling sering dibatasi pada beberapa baris.





Di sini dan contoh lebih lanjut dalam bahasa aql arangodb, saya menyembunyikan kode server karena belum ada yang menarik di sana.





//   20    . 

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

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



Di sisi klien, kami meminta dan menampilkan hasil yang dihasilkan, saya menggunakan vuejs dengan nuxtjs sebagai contoh, tetapi hal yang sama dapat dilakukan pada tumpukan lain, saya akan menandatangani semua poin khusus 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>
      
      



Sekarang kita memiliki semua posting di halaman yang ditampilkan, tapi tunggu, bagaimana pengguna akan beralih antar halaman? Mari tambahkan beberapa tombol untuk membalik halaman.





<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>
      
      



Kontra dari metode ini





  • .





  • , . 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] >







Dasar dari metode untuk menghasilkan pagination diambil dari diskusi ini: https://gist.github.com/kottenator/9d936eb3e4e3c3e02598#gistcomment-3238804 dan disilangkan dengan solusi saya.





Tampilkan kelanjutan bonus

Pertama, Anda perlu menambahkan metode pembantu ini di dalam tag <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
}
      
      



Menambahkan metode yang hilang





<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>

      
      



Sekarang, jika perlu, Anda dapat membuka halaman yang diinginkan.








All Articles