分页算法(两个部分之一)
#javascript #网络开发人员 #编程 #pagination

如今,大多数应用程序接缝以采用Web技术,而不是排除业务系统。与“内容为king”的网站不同,Web应用程序具有不同的优先级(或优先级。),其业务系统帮助最终用户以尽可能高效,无错误和安全的方式完成其任务是必不可少的。

,业务应用程序不用为查看者提供“内容”,而是必须为用户提供功能和数据,以便提取信息,通常以支持决策。当数据以表格形式显示时,最终将在问题出现时,如何执行分页。这些选项是一个在Web浏览器中的客户端,或者仅发送少量记录(数据页)发送给客户端的服务器端。

决策中的一些关键因素包括:

  • 记录结构的复杂性(及其大小)
  • 在客户端和服务器之间运输数据时,网络流量的配置文件和争议。
  • 数据集的波动性以及演示文稿最新的重要性。

其他考虑因素

分页很少是列表数据的唯一功能最终用户所需的功能。定期要求的其他设施包括:

  • 搜索:在选定的一组列中的任何一个中找到包含搜索词的记录。
  • 过滤:通过排除具有特定值的行与规定的列/属性匹配的行来减少数据集,或多个。
  • 排序:根据一个或多个列/属性的顺序排列数据行。

这些设施需要按特定顺序应用,以实现所需的数据表示。如果我们在排序之前提取数据页面,则不太可能像预期的那样过滤或搜索结果。我们可能还必须考虑在特殊情况下支持“预先庆祝”的过滤标准。

在这两个帖子中的第一篇文章中,我们将考虑如何在客户端和服务器端分页过程中应用这些功能。我们将通过考虑一般过程并准备一个有效的示例来获得简单的解决方案。在第二篇文章中,我们将扩展工作示例,以包括一些常见的其他功能,例如搜索,过滤和排序。可以使用功能性编码样式的技术和使用JavaScript的一些新(ISH)功能的技术来开发用于执行此三重奏的算法。

案例分析

本文基于处理我们遇到的绩效问题的个人经验。最初,我们有屏幕下载了整个数据集并一次提供了一个简单的列表。没有排序或过滤,只有简单的搜索,但都在浏览器中令人满意地执行。

当下载了一个更复杂的数据集时,

性能开始降低。浏览器开始变得无响应,使客户端功能无法使用。同时,我们注意到搜索,分类和过滤的组合偶尔会产生意外的结果。他们我们不参加音乐会。

为了解决性能的降级,我们首先通过通信协议提供了服务器端的分页。初始请求(查询)以及初始合同(页码,页码,搜索列,过滤列值和排序标准)的详细信息发送到服务器。每当用户更改任何合同值时,将更新的合同发送到服务器。作为响应,客户端(Web浏览器)在流程结果集中接收了许多行以及页数。

对于奇偶校验,我们实施了使用相同的合同/协议执行客户端分页所需的低级功能。通过符合双方协议,如果服务器是node.js(或其他基于JS的服务器),则可以应用相同的低级功能。

定义功能

分页
输入:页码和页码
输出:页面内容和可用页面数量。

排序
输入:具有数据类型和排序方向的零或许多列(按优先顺序)。
输出:根据组合顺序排列的行。

过滤
输入:零或许多列以及过滤器值列表。
输出:只有符合过滤器条件的行。过滤(在此示例中)将or条件应用于同一列中的值和列之间的and条件。

搜索
输入:应应用搜索词的列列表,搜索词本身。
输出:只有那些在规定的搜索列之一中包含搜索词的行。


工作的例子

出于学习目的,我们将通过whiskyhunter的公共API获取一些数据,该数据为我们提供了270多个威士忌酒厂的列表。

如果您想跟随,这是我们可以使用的开发环境。

有关使用node.js的一些知识:

  1. (从第18版开始)节点尚未完全实现Fetch API,但可用。它是在节点版本17.6中引入的,作为实验功能。要使用我们需要使用以下命令行开关--experimental-fetch调用节点的功能。使用节点版本18+,我们不再需要开关,但是警告可能仍会出现。
  2. 提取API是异步的,但节点尚未支持ECMA脚本模块之外的top-level koude3。启用ES模块将需要另一个命令行开关和更多代码。但是,正如我们将看到的那样,有一项相对简单的工作。
  3. 我们将使用JavaScript Module系统在随后的示例中使用数据检索机制。要启用此功能,我们需要配置一个简单的package.json文件。
    • 在同一文件夹中您将创建示例JS文件,打开命令提示符,然后输入以下命令npm init -y。这将使用默认值创建初始的package.json文件。
    • 编辑package.json文件并添加以下属性"type": "module",记住相应地更新JSON。例如:
{
  "name": "pagination",
  "version": "1.0.0",
  "author": "Tracy Gilmore",
  "license": "ISC",
  "type": "module"
}

检索和处理源数据

在查看代码以获取我们将使用的数据之前,还有另一点要注意。源数据比我们需要的要复杂一些,因此我们创建的功能“获取”数据也将预处理。

const X_CSRF_TOKEN = // Get from https://whiskyhunter.net/api/

// 0-retrieve-data.js

async function list() {
  const listJson = await fetch(
    'https://whiskyhunter.net/api/distilleries_info/',
    {
      method: 'GET',
      'Content-Type': 'application/json',
      'X-CSRFToken': X_CSRF_TOKEN,
    }
  );

  const listData = await listJson.json();

  return listData.map(
    ({ 
      name, 
      country, 
      whiskybase_whiskies, 
      whiskybase_votes, 
      whiskybase_rating
    }) => ({
      name,
      country,
      products: +whiskybase_whiskies,
      votes: +whiskybase_votes,
      rating: +whiskybase_rating,
    })
  );
}

上面的代码定义了一个称为listasync函数,该函数使用fetch API请求源数据,将其从JSON字符串转换为对象数组(行/记录)。在转换JSON的同时,我们从每个对象中提取感兴趣的属性,将数字值从字符串转换为数字,然后返回一个新的对象(行)。

我们可以使用以下函数调用list函数,并显示该数组返回作为控制台中的表。请注意,我们必须在异步IIFE(立即调用函数表达式)中包裹对console.table的呼叫,以解决缺乏顶级await支持。

// 0-retrieve-data.js continued

(async function demonstration() {
  console.table(await list());
})();

在“ 0- retrieve-data.js”中运行代码会产生以下输出。

Output of 0-retrieve-data.js

现在,我们已经演示了如何检索数据,我们将对“ 0- retrieve-data.js”文件进行小更改,以使其成为我们可以使用的模块。

  1. 评论(禁用)包含演示功能的IIFE。
  2. 将以下指令添加到文件底部以公开列表功能。
// 0-retrieve-data.js revised

export default list;

它将如何工作

分页过程的中心是由GeneratePaginator函数创建的paginator函数。这两个功能都用“分页合同”调用(稍后再详细介绍),但是(在我们的情况下)也调用了生成函数,或者是一个完整的数据集或查询检索数据集的参数。

对于客户端分页,生成paginator函数的预期仅被调用一次。它返回的功能(实际的Paginator)预计每次更新分页合同时都会反复调用。 Paginator返回两件事:

  1. 预处理数据集中的页面总数。 “预处理”数据集是对源数据进行过滤和搜索源数据的结果,但是在应用分类和分页之前。
  2. 处理页面中的一组记录。

Paginator

对于服务器端分页,合同可以从客户端(页面)发送到服务器,但生成器不会创建Paginator函数。相反,服务器端生成的播放器函数管理分页流程,并期望对分页合同进行更新。

分页合同

分页合同是用于通过服务器端分页的网络请求直接将参数传递给Paginators功能的数据对象。有两个版本,“初始”和“更新”:

  • “初始”'用于通过提供预计不会更改的参数来制备生成paginator。合同可以是空的,但也可以包含:
    • 可搜索的列/属性列表。
    • 初始页面大小和数字(默认为1)。

Pagination Contracts

  • '更新'用于告知用户在如何更改演示文稿方面所做的更改的Paginator功能。合同可以包含:
    • 搜索词,如果定义(不是空)。
    • 列表要应用过滤的列列表:
      • 预先盛行的过滤器的名称(该帖子范围之外),
      • 滤波器值列表
      • 滤波器操作(例如“大于大于”)以及参数。
    • 可排序列的列表及其数据类型和排序顺序(方向)。
    • 更新的页面大小和号码。

分页过程

如本文前面所示,分页的过程以及执行其他功能必须以特定的顺序进行,以产生所需的可预测结果。

Pagination process

一旦检索了原始源数据(或可能刷新),请执行三个阶段以生成准备好的数据的“页面”。

搜索和过滤

技术搜索和过滤是类似的操作。对于每个候选行,我们在搜索词或过滤条件的上下文中评估数据,以评估该行是否应包含在(二进制选择)中。

排序

有很多分类算法,但是,与经常使用的那些机制(内置在语言中)相比,比在顶部实施某些东西更好。

JavaScript具有内置在数组对象中的非常简单有效的排序方法,但仅限于单个属性。我们将扩展功能以支持多层分类。

我们将在下一篇文章中深入研究前两个阶段,但现在我们将解决“小贩”的更简单阶段,尽管它并非没有复杂性。

以其最简单的形式,此阶段将行隔离到所选页面的范围内。输出包括页面的行和可用的页数。

更复杂的形式(这超出了本文的范围),例如:

  • 数据的波动性如何(它的变化速度变化)以及用户立即接收更新以确保数据准确性的重要性。
  • 网络容量的可用性以及支持数据缓存以获得改进的用户体验的需求。

实施分页

这三个阶段分页的

实际上是实现的最简单(基本形式)。在示例代码的顶部,我们将从“ 0- retrieve-data.js”模块中导入数据检索函数list()

// 1-initial-pagination.js

import list from './0-retrieve-data.js';

如上所述,我们接下来定义了访问源数据和初始合同的GeneratePaginator函数,此时仅包含页面参数(数字和大小),并返回Paginator函数。

// 1-initial-pagination.js continued

function GeneratePaginator(
  data,
  contract = {
    page: { number: 1, size: 20 },
  }
) {
  let currentContract = contract;
  return (newContract = { page: { number: 1, size: 20 } }) =>
    {
      currentContract.page = {
        ...currentContract.page,
        ...newContract.page
      };
      const {
        number: pageNumber,
        size: pageSize
      } = currentContract.page;
      const dataset = pageNumber * pageSize;
      return {
        page: data.slice(dataset - pageSize, dataset),
        pages: Math.ceil(data.length / pageSize),
      };
    };
}

(async function () {
  const paginator = GeneratePaginator(await list());

  // Test call 1: Using default parameters
  console.table(paginator().page);

  // Test call 2: Using custom parameters
  console.table(paginator({ page: { number: 3, size: 5 } }).page);

  // Test call 3: Updating custom parameters
  const pagedData = paginator({ page: { number: 4 } });
  console.log('Total pages in dataset:', pagedData.pages);
  console.table(pagedData.page);

  // Test call 4: Accessing the last (partial) page
  console.table(paginator({ page: { number: 55 } }).page);
})();

在上面示例的后半部分,我们使用IIFE首先创建Paginator实例“ Paginator”,并使用我们的测试数据填充它。然后,我们使用console.table进行一系列测试调用,并提供结果,如下所示。请注意,我们每个呼叫都会获得两个值:

  • page 包含包含要呈现的行的源数据的子集。
  • 页面包含一个代表显示整个数据集所需的几页的数字。

测试调用1:使用默认参数

默认情况下,Paginator将选择具有20行大小的数据的第一页,如初始合同所定义。
Using default parameters

测试呼叫2:使用自定义参数

我们可以更新合同以规定不同的页码和大小。
Using custom parameters

测试呼叫3:更新自定义参数

我们还可以修改合同,指定新的页面大小或如下所示的替代页码。
Updating the custom parameters

测试电话4:访问最后一个(部分)页面

当我们请求上一个可用页面时,我们可能不会获得与页面大小规定的行一样多的行,但是我们将在最后一页中获取所有行。
Accessing the last (partial) page

使用274行数据,我们将获得以下参数相同的结果:

page
数字 6 7 10 11 16 19 28 31 46 55
大小 54 45 30 27 18 15 10 9 6 5

在上面的代码中,我们假设页码将在页面大小定义的页面范围1内。但是,如果某种程度上要求的页码小于1,或者更有可能的页面比可用的页面数大的页面怎么办?

扩展分页

如果我们调整页面大小,但是页码仍在范围内?理想情况下,我们应该修改页码以与先前呈现的数据保持一致。我们需要如下增强GeneratePaginator函数。

// 2-enhanced-pagination.js

function GeneratePaginator(
  data,
  contract = {
    page: { number: 1, size: 20 },
  }
) {
  let currentContract = contract;
  return (newContract = { page: { number: 1, size: 20 } }) => {

// [1]
    const currentPageSize = (newContract.page?.number
      ? newContract : currentContract
    ).page.size;

    currentContract.page = {
      ...currentContract.page,
      ...newContract.page
    };
    let { number: pageNumber, size: pageSize } =
      currentContract.page;

// [2]
    pageNumber = Math.ceil(
      ((pageNumber - 1) * currentPageSize + 1) / pageSize);
    const pages = Math.ceil(data.length / pageSize);

// [3]
    if (pageNumber < 1) pageNumber = 1;
    if (pageNumber > pages) pageNumber = pages;

    const dataset = pageNumber * pageSize;
    return {
      page: data.slice(dataset - pageSize, dataset),
      pages,
    };
  };
}
  1. 我们需要保留以前的pageSize进行比较,但只有在呼叫中没有提供。
  2. 接下来,我们需要确保pageNumber处于范围内,因此,无论pageNumber是否对pageSize,页面的顶部都保持不变。
  3. 我们还需要检查新的pageNumber的上和下部范围,以确保其处于范围。

让我们看看点2如何与以下测试一起工作。

// 2-enhanced-pagination.js revised

(async function () {
  const paginator = GeneratePaginator(await list());

  // Test call 5a: Page resizing - initial
  let pagedData = paginator({
    page: { number: 3, size: 10 },
  });
  console.log('Total pages in dataset:', pagedData.pages);
  console.table(pagedData.page);

  // Test call 5b: Page resizing - resized
  pagedData = paginator({ page: { size: 5 } });
  console.log('Total pages in dataset:', pagedData.pages);
  console.table(pagedData.page);
})();

在5A时,我们通过将初始页面参数设置为数字3和大小10来准备测试案例,该参数将呈现21至30行,并提供28页。

Initialising page resizing

在5B中,我们将页面大小调整到5行,可用的页面数量增加到55,但我们仍然显示21至25行。

Update page resizing


part two中,我们将更深入地研究辅助功能(搜索,过滤和排序),并查看功能编程(FP)技术如何帮助创建有效的解决方案。