データベースへの予期しないデータの追加を恐れないキャッシュされたページ付けを作成します

サイトに大量のコンテンツが含まれている場合、それを表示するには、ユーザーは何らかの方法でコンテンツを共有する必要があります。





私が知っているすべての方法には欠点があり、実装するのが難しくなくても、それらのいくつかを解決できるシステムを作成しようとしました。





既存の方法

1.ページネーション(別々のページへの分割)

サイトhabr.comからの例
サイトhabr.comからの例

ページ付けまたは個別のページへの分割は、コンテンツを分割するかなり古い方法であり、Habréでも使用されています。主な利点は、サーバー側とクライアント側の両方からの汎用性と実装の容易さです。





データベースからデータを要求するためのコードは、ほとんどの場合、数行に制限されています。





こことarangodbaql言語のその他の例では、まだ興味深いものがないため、サーバーコードを非表示にしました。





//   20    . 

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

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



クライアント側では、結果を要求して表示します。例としてvuejsとnuxtjsを使用しますが、他のスタックでも同じことができます。すべての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>
      
      



これで、ページ上のすべての投稿が表示されましたが、待ってください。ユーザーはどのようにページを切り替えますか?ページをめくるボタンをいくつか追加してみましょう。





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



この方法の短所





  • .





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







ページ付けを生成する方法の基礎は、このディスカッションから取得されますhttps//gist.github.com/kottenator/9d936eb3e4e3c3e02598#gistcomment-3238804そして私のソリューションと交差します。





ボーナス継続を表示

まず、このヘルパーメソッドを<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
}
      
      



不足しているメソッドの追加





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

      
      



これで、必要に応じて、目的のページに移動できます。








All Articles