在 Vue 中创建可复用的分页组件

Avatar of Mateusz Rybczonek
Mateusz Rybczonek

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

大多数 Web 应用程序背后的理念是从数据库中获取数据并以最佳方式呈现给用户。当我们处理数据时,在某些情况下,最佳的呈现方式意味着创建列表。

根据数据量及其内容,我们可以决定一次显示所有内容(很少见),或者仅显示较大数据集的特定部分(更可能)。仅显示现有数据一部分的主要原因是我们希望保持应用程序尽可能高效,并避免加载或显示不必要的数据。

如果我们决定以“块”的形式显示我们的数据,那么我们需要一种方法来浏览该集合。**浏览数据集的两种最常见方法**是

首先是**分页**,这是一种将数据集拆分为_特定数量的页面_的技术,使用户免于被一页上的大量数据淹没,并允许他们一次查看一组结果。例如,您正在阅读的这个博客。首页列出了最新的 10 篇帖子。查看下一组最新的帖子需要点击一个按钮。

第二种常见技术是**无限滚动**,如果您曾经在 Facebook 或 Twitter 上滚动过时间线,那么您可能很熟悉它。

Apple News 应用程序也使用无限滚动来浏览文章列表。

我们将在本文中更深入地了解第一种类型。分页是我们几乎每天都会遇到的东西,但制作它并不完全简单。它非常适合用作组件,所以我们正是要这么做的。我们将逐步介绍创建负责显示该列表的组件以及在点击要显示的特定页面时触发获取其他文章操作的过程。换句话说,我们正在像这样在 Vue.js 中制作一个分页组件

让我们一起完成这些步骤。

步骤 1:在 Vue 中创建 ArticlesList 组件

让我们首先创建一个组件来显示文章列表(但暂时不包含分页)。我们将它命名为ArticlesList。在组件模板中,我们将遍历文章集并将单个文章项传递给每个ArticleItem组件。

// ArticlesList.vue
<template>
  <div>
    <ArticleItem
      v-for="article in articles"
      :key="article.publishedAt"
      :article="article"
    />
  </div>
</template>

在组件的脚本部分,我们设置初始数据

  • articles:这是一个空数组,在mounted钩子中填充从 API 获取的数据。
  • currentPage:用于操作分页。
  • pageCount:这是页面的总数,在mounted钩子中根据 API 响应计算得出。
  • visibleItemsPerPageCount:这是我们希望在一页上看到的文章数量。

在此阶段,我们仅获取文章列表的第一页。这将为我们提供两篇文章的列表

// ArticlesList.vue
import ArticleItem from "./ArticleItem"
import axios from "axios"
export default {
  name: "ArticlesList",
  static: {
    visibleItemsPerPageCount: 2
  },
  data() {
    return {
      articles: [],
      currentPage: 1,
      pageCount: 0
    }
  },
  components: { 
    ArticleItem, 
  },
  async mounted() {
    try {
      const { data } = await axios.get(
        `?country=us&page=1&pageSize=${
          this.$options.static.visibleItemsPerPageCount
        }&category=business&apiKey=065703927c66462286554ada16a686a1`
      )
      this.articles = data.articles
      this.pageCount = Math.ceil(
        data.totalResults / this.$options.static.visibleItemsPerPageCount
      )
    } catch (error) {
      throw error
    }
  }
}

步骤 2:创建 pageChangeHandle 方法

现在我们需要创建一个方法来加载下一页、上一页或选定的页面。

pageChangeHandle方法中,在加载新文章之前,我们会根据传递给方法的属性更改currentPage值,并从 API 中获取特定页面的数据。收到新数据后,我们将用包含新页面文章的新数据替换现有的articles数组。

// ArticlesList.vue
...
export default {
...
  methods: {
    async pageChangeHandle(value) {
      switch (value) {
        case 'next':
          this.currentPage += 1
          break
        case 'previous':
          this.currentPage -= 1
          break
        default:
          this.currentPage = value
      }
      const { data } = await axios.get(
        `?country=us&page=${this.currentPage}&pageSize=${
          this.$options.static.visibleItemsPerPageCount
        }&category=business&apiKey=065703927c66462286554ada16a686a1`
      )
      this.articles = data.articles
    }
  }
}

步骤 3:创建触发页面更改的组件

我们有pageChangeHandle方法,但我们没有在任何地方触发它。我们需要创建一个负责此操作的组件。

此组件应执行以下操作

  1. 允许用户转到下一页/上一页。
  2. 允许用户转到当前选中页面范围内的特定页面。
  3. 根据当前页面更改页面编号的范围。

如果我们要将其草绘出来,它看起来会像这样

让我们继续!

需求 1:允许用户转到下一页或上一页

我们的BasePagination将包含两个按钮,分别用于转到下一页和上一页。

// BasePagination.vue
<template>
  <div class="base-pagination">
    <BaseButton
      :disabled="isPreviousButtonDisabled"
      @click.native="previousPage"
    >
      ←
    </BaseButton>
    <BaseButton
      :disabled="isNextButtonDisabled"
      @click.native="nextPage"
    >
      →
    </BaseButton>
  </div>
</template>

该组件将从父组件接收currentPagepageCount属性,并在点击下一页或上一页按钮时向父组件发出相应的操作。它还将负责在我们在第一页或最后一页时禁用按钮,以防止超出现有集合。

// BasePagination.vue
import BaseButton from "./BaseButton.vue";
export default {
  components: {
    BaseButton
  },
  props: {
    currentPage: {
      type: Number,
      required: true
    },
    pageCount: {
      type: Number,
      required: true
    }
  },
  computed: {
    isPreviousButtonDisabled() {
      return this.currentPage === 1
    },
    isNextButtonDisabled() {
      return this.currentPage === this.pageCount
    }
  },
  methods: {
    nextPage() {
      this.$emit('nextPage')
    },
    previousPage() {
      this.$emit('previousPage')
    }
  }

我们将在ArticlesList组件中的ArticleItems下方渲染该组件。

// ArticlesList.vue
<template>
  <div>
    <ArticleItem
      v-for="article in articles"
      :key="article.publishedAt"
      :article="article"
    />
    <BasePagination
      :current-page="currentPage"
      :page-count="pageCount"
      class="articles-list__pagination"
      @nextPage="pageChangeHandle('next')"
      @previousPage="pageChangeHandle('previous')"
    />
  </div>
</template>

这是简单的部分。现在我们需要创建一个页面编号列表,每个编号都允许我们选择一个特定页面。页面数应该是可自定义的,我们还需要确保不显示可能导致我们超出集合范围的任何页面。

需求 2:允许用户转到范围内的特定页面

让我们首先创建一个组件,该组件将用作单个页面编号。我将其命名为BasePaginationTrigger。它将执行两件事:显示从BasePagination组件传递的页面编号,并在用户点击特定编号时发出事件。

// BasePaginationTrigger.vue
<template>
  <span class="base-pagination-trigger" @click="onClick">
    {{ pageNumber }}
  </span>
</template>
<script>
export default {
  props: {
    pageNumber: {
      type: Number,
      required: true
    }
  },
  methods: {
    onClick() {
      this.$emit("loadPage", this.pageNumber)
    }
  }
}
</script>

然后,此组件将在BasePagination组件中的下一页和上一页按钮之间呈现。

// BasePagination.vue
<template>
  <div class="base-pagination">
    <BaseButton />
    ...
    <BasePaginationTrigger
      class="base-pagination__description"
      :pageNumber="currentPage"
      @loadPage="onLoadPage"
    />
    ...
    <BaseButton />
  </div>
</template>

在脚本部分,我们需要添加另一个方法(onLoadPage),当从触发器组件发出loadPage事件时,该方法将被触发。此方法将接收被点击的页面编号,并将事件向上级联到ArticlesList组件。

// BasePagination.vue
export default {
  ...
    methods: {
    ...
    onLoadPage(value) {
      this.$emit("loadPage", value)
    }
  }
}

然后,在ArticlesList中,我们将监听该事件并触发pageChangeHandle方法,该方法将为我们的新页面获取数据。

// ArticlesList
<template>
  ...
    <BasePagination
      :current-page="currentPage"
      :page-count="pageCount"
      class="articles-list__pagination"
      @nextPage="pageChangeHandle('next')"
      @previousPage="pageChangeHandle('previous')"
      @loadPage="pageChangeHandle"
    />
  ...
</template>

需求 3:根据当前页面更改页面编号的范围

好的,现在我们有一个单一的触发器,它向我们显示当前页面并允许我们再次获取同一页面。是不是有点没用?让我们利用新创建的触发器组件。我们需要一个页面列表,它允许我们在不需要遍历中间页面即可从一个页面跳转到另一个页面。

我们还需要确保以一种良好的方式显示页面。我们始终希望在分页列表中显示第一页(最左侧)和最后一页(最右侧),然后显示它们之间的其余页面。

我们有三种可能的情况

  1. 选定的页面编号小于列表宽度的一半(例如 1 – 2 – 3 – 4 – 18)
  2. 选定的页面编号大于从列表末尾算起的列表宽度的一半(例如 1 – 15 – 16 – 17 – 18)
  3. 所有其他情况(例如 1 – 4 – 5 – 6 – 18)

为了处理这些情况,我们将创建一个计算属性,该属性将返回应在下一页和上一页按钮之间显示的一组数字。为了使组件更具可复用性,我们将接受一个属性visiblePagesCount,该属性将指定分页组件中应显示多少个页面。

在逐一查看这些情况之前,我们创建几个变量

  • visiblePagesThreshold:- 告诉我们从中心显示多少个页面(应显示选定的页面)
  • paginationTriggersArray:将填充页面编号的数组

  • visiblePagesCount: 创建一个所需长度的数组
// BasePagination.vue
export default {
  props: {
    visiblePagesCount: {
      type: Number,
      default: 5
    }
  }
  ...
  computed: {
    ...
      paginationTriggers() {
        const currentPage = this.currentPage
        const pageCount = this.pageCount
        const visiblePagesCount = this.visiblePagesCount
        const visiblePagesThreshold = (visiblePagesCount - 1) / 2
        const pagintationTriggersArray = Array(this.visiblePagesCount - 1).fill(0)
      }
    ...
    }
  ...
}

现在让我们来逐一分析每种情况。

场景 1:选中的页码小于列表宽度的一半

我们将第一个元素始终设置为等于 1。然后我们遍历列表,为每个元素添加索引。最后,我们添加最后一个值并将其设置为等于最后一页的页码——如果需要,我们希望能够直接跳转到最后一页。

if (currentPage <= visiblePagesThreshold + 1) {
  pagintationTriggersArray[0] = 1
  const pagintationTriggers = pagintationTriggersArray.map(
    (paginationTrigger, index) => {
      return pagintationTriggersArray[0] + index
    }
  )
  pagintationTriggers.push(pageCount)
  return pagintationTriggers
}
场景 2:选中的页码从列表末尾算起大于列表宽度的一半

与上一个场景类似,我们从最后一页开始,遍历列表,这次从每个元素中减去索引。然后我们反转数组以获得正确的顺序,并将 1 推送到数组中的第一个位置。

if (currentPage >= pageCount - visiblePagesThreshold + 1) {
  const pagintationTriggers = pagintationTriggersArray.map(
    (paginationTrigger, index) => {
      return pageCount - index
    }
  )
  pagintationTriggers.reverse().unshift(1)
  return pagintationTriggers
}
场景 3:所有其他情况

我们知道列表中心应该显示哪个数字:当前页。我们也知道列表的长度。这使我们能够获得数组中的第一个数字。然后我们通过为每个元素添加索引来填充列表。最后,我们将 1 推送到数组中的第一个位置,并将最后一个数字替换为最后一页的页码。

pagintationTriggersArray[0] = currentPage - visiblePagesThreshold + 1
const pagintationTriggers = pagintationTriggersArray.map(
  (paginationTrigger, index) => {
    return pagintationTriggersArray[0] + index
  }
)
pagintationTriggers.unshift(1);
pagintationTriggers[pagintationTriggers.length - 1] = pageCount
return pagintationTriggers

这涵盖了我们所有的场景!我们只剩下一件事要做。

步骤 5:在 BasePagination 组件中渲染数字列表

现在我们确切地知道要在分页中显示什么数字,我们需要为每个数字渲染一个触发器组件。

我们使用 v-for 指令来实现。我们还可以添加一个条件类来处理选择当前页。

// BasePagination.vue
<template>
  ...
  <BasePaginationTrigger
    v-for="paginationTrigger in paginationTriggers"
    :class="{
      'base-pagination__description--current':
        paginationTrigger === currentPage
    }"
    :key="paginationTrigger"
    :pageNumber="paginationTrigger"
    class="base-pagination__description"
    @loadPage="onLoadPage"
  />
  ...
</template>

大功告成!我们刚刚在 Vue 中构建了一个漂亮且可重用的分页组件。

何时避免使用此模式

尽管这个组件非常棒,但它并不是所有涉及分页的用例的灵丹妙药。

例如,对于持续流式传输且结构相对扁平的内容,最好避免使用此模式,例如每个项目都处于相同的层次结构级别,并且对用户来说具有相似的趣味性。换句话说,类似于包含多个页面的文章,而不是类似于主导航的内容。

另一个例子是浏览新闻而不是查找特定的新闻文章。我们不需要知道新闻的确切位置以及滚动到特定文章需要滚动多少。

总结!

希望您能在项目中发现此模式有用,无论是简单的博客、复杂的电子商务网站,还是介于两者之间的内容。分页可能很麻烦,但拥有一个不仅可以重复使用,而且考虑了大量场景的模块化模式,可以使处理分页变得更加容易。