Si votre site contient une grande quantité de contenu, alors pour l'afficher, l'utilisateur doit le partager d'une manière ou d'une autre.
Toutes les méthodes que je connais ont des inconvénients et j'ai essayé de créer un système capable de résoudre certaines d'entre elles sans être trop difficile à mettre en œuvre.
Méthodes existantes
1. Pagination (division en pages séparées)
La pagination ou la division en pages séparées est une façon assez ancienne de diviser le contenu, qui est également utilisée sur Habré. Le principal avantage est sa polyvalence et sa facilité de mise en œuvre tant du côté serveur que du côté client.
Le code pour demander des données à la base de données est le plus souvent limité à quelques lignes.
Ici et d'autres exemples dans le langage arangodb aql, j'ai caché le code du serveur car il n'y a encore rien d'intéressant.
// 20 .
LET count = 20
LET offset = count * ${page}
FOR post IN posts
SORT post.date DESC //
LIMIT offset, count
RETURN post
Côté client, nous demandons et affichons le résultat obtenu, j'utilise vuejs avec nuxtjs pour un exemple, mais la même chose peut être faite sur n'importe quelle autre pile, je signerai tous les points spécifiques à la 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>
Maintenant, nous avons tous les articles sur la page affichés, mais attendez, comment les utilisateurs vont-ils basculer entre les pages? Ajoutons quelques boutons pour tourner les pages.
<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>
Inconvénients de cette méthode
.
, . 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 de la méthode de génération de pagination est tirée de cette discussion: https://gist.github.com/kottenator/9d936eb3e4e3c3e02598#gistcomment-3238804 et croisée avec ma solution.
Afficher la suite du bonus
Tout d'abord, vous devez ajouter cette méthode d'assistance dans la balise <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
}
Ajout de méthodes manquantes
<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>
Maintenant, si nécessaire, vous pouvez accéder à la page souhaitée.