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