Se o seu site contém uma grande quantidade de conteúdo, para exibi-lo, o usuário deve compartilhá-lo de uma forma ou de outra.
Todos os métodos que conheço têm desvantagens e tentei criar um sistema que possa resolver alguns deles sem ser muito difícil de implementar.
Métodos existentes
1. Paginação (divisão em páginas separadas)
A paginação ou divisão em páginas separadas é uma maneira bastante antiga de dividir o conteúdo, que também é usada no Habré. A principal vantagem é sua versatilidade e facilidade de implementação tanto do lado do servidor quanto do lado do cliente.
O código para solicitar dados do banco de dados geralmente é limitado a algumas linhas.
Aqui e em outros exemplos na linguagem arangodb aql, escondi o código do servidor porque ainda não há nada de interessante nele.
// 20 .
LET count = 20
LET offset = count * ${page}
FOR post IN posts
SORT post.date DESC //
LIMIT offset, count
RETURN post
No lado do cliente, solicitamos e exibimos o resultado resultante, eu uso vuejs com nuxtjs por exemplo, mas o mesmo pode ser feito em qualquer outra pilha, assinarei todos os pontos específicos de 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>
Agora temos todas as postagens da página exibidas, mas espere, como os usuários alternarão entre as páginas? Vamos adicionar alguns botões para virar as páginas.
<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>
Contras deste método
.
, . 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] >
A base do método para gerar paginação é tirada desta discussão: https://gist.github.com/kottenator/9d936eb3e4e3c3e02598#gistcomment-3238804 e cruzada com minha solução.
Mostrar continuação de bônus
Primeiro, você precisa adicionar este método auxiliar dentro da tag <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
}
Adicionando métodos ausentes
<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>
Agora, se necessário, você pode ir para a página desejada.