サイトに大量のコンテンツが含まれている場合、それを表示するには、ユーザーは何らかの方法でコンテンツを共有する必要があります。
私が知っているすべての方法には欠点があり、実装するのが難しくなくても、それらのいくつかを解決できるシステムを作成しようとしました。
既存の方法
1.ページネーション(別々のページへの分割)
ページ付けまたは個別のページへの分割は、コンテンツを分割するかなり古い方法であり、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>
これで、必要に応じて、目的のページに移動できます。