Wir erstellen eine zwischengespeicherte Paginierung, die keine Angst vor unerwarteten Daten zur Datenbank hat

Wenn Ihre Website eine große Menge an Inhalten enthält, muss der Benutzer diese auf die eine oder andere Weise freigeben, um sie anzuzeigen.





Alle mir bekannten Methoden haben Nachteile, und ich habe versucht, ein System zu erstellen, das einige davon lösen kann, ohne zu schwierig zu implementieren zu sein.





Bestehende Methoden

1. Paginierung (Aufteilung in separate Seiten)

Ein Beispiel von der Website habr.com
Ein Beispiel von der Website habr.com

Das Paginieren oder Aufteilen in separate Seiten ist eine ziemlich alte Methode zum Teilen von Inhalten, die auch bei Habré verwendet wird. Der Hauptvorteil ist die Vielseitigkeit und einfache Implementierung sowohl auf der Serverseite als auch auf der Clientseite.





Der Code zum Anfordern von Daten aus der Datenbank ist meist auf einige Zeilen beschränkt.





Hier und in weiteren Beispielen in der Sprache arangodb aql habe ich den Servercode versteckt, da dort noch nichts Interessantes ist.





//   20    . 

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

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



Auf der Clientseite fordern wir das resultierende Ergebnis an und zeigen es an. Ich verwende vuejs mit nuxtjs als Beispiel, aber das gleiche kann auf jedem anderen Stapel gemacht werden. Ich werde alle vue-spezifischen Punkte signieren.





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



Jetzt werden alle Beiträge auf der Seite angezeigt, aber warten Sie, wie werden Benutzer zwischen den Seiten wechseln? Fügen wir ein paar Schaltflächen hinzu, um Seiten umzublättern.





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



Nachteile dieser Methode





  • .





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







Die Grundlage der Methode zur Erzeugung der Paginierung wird aus dieser Diskussion entnommen: https://gist.github.com/kottenator/9d936eb3e4e3c3e02598#gistcomment-3238804 und mit meiner Lösung gekreuzt.





Bonusfortsetzung anzeigen

Zunächst müssen Sie diese Hilfsmethode in das <script> -Tag einfügen





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
}
      
      



Fehlende Methoden hinzufügen





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

      
      



Jetzt können Sie bei Bedarf zur gewünschten Seite wechseln.








All Articles