65.9K
CodeProject 正在变化。 阅读更多。
Home

为 AngularJS 和 UI-Grid 显示加载数据

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2023年2月14日

MIT

18分钟阅读

viewsIcon

8876

downloadIcon

61

在基于 AngularJS 的应用程序中分批加载数据并使用 UI-Grid 显示

引言

新年伊始,我就一直在思考一个技术问题及其解决方案。想象一下,我有一个数据列表。我需要加载这些数据并将其显示在页面上。当数据项数量较少时,比如100、1,000或10,000条,一次性加载它们不会花费太多时间。但当数据项数量庞大时,比如800,000条左右,或者每个数据项包含大量内容时,一次性加载所有数据将是一个非常耗时的过程。而用户最讨厌的就是坐在舒适的椅子上等待数据加载完成。

这个问题困扰了我一段时间。我喜欢一次性加载所有数据,然后在前端将其分解成较小的数据集作为页面的想法。这种方法可以让分页过程非常流畅。但让用户长时间等待的问题让我非常烦恼。随着数据量的增长,这个等待时间可能会变得更长。一种解决方案是,我可以按固定大小的批次读取所有数据。一旦一批数据准备就绪,就将它们添加到现有数据列表的末尾。然后让列表组件将长列表分解成数据页面。这是一个想法。为了实现这个想法,我需要一种机制来执行批量读取,以及某种类型的前端组件,它能够自动将列表分解成数据页面并正确显示数据。

事实证明,批量读取数据可以毫无困难地实现。还有一个名为 ui-grid 的 AngularJS 组件,可以轻松地将一个大数据列表分解并正确地显示在页面上。我决定使用 ui-grid 并创建一个通用的批量读取循环机制来尝试我的实现。本教程将讨论这是如何完成的。

整体架构

我的示例应用程序是一个基于 Spring Boot 的 Web 应用程序。没有花哨的身份验证机制。只有一个 RESTFul API 控制器负责批量数据读取。前端应用程序是一个基于 AngularJS 的单页应用程序。它所做的就是通过调用 RESTFul API 控制器来加载数据,并持续这样做直到所有数据都加载完毕。该页面将托管一个 ui-grid 组件,一旦数据可用,它就会显示所有数据。

我实现的机制是通用的。也就是说,我可以将接口和类复制到不同的项目中,添加一些项目特定的实现代码,这个机制就能如预期般工作。为了利用这个机制,我不得不在前端实现一个递归,以便它可以连续加载数据,直到没有更多数据可加载为止。该机制本身是使用策略模式完成的。这是确保我可以将此机制移至其他项目并重新利用的唯一方法。

 

让我们从请求和响应对象的设计开始本教程。然后,我将详细讨论批量加载机制。之后,我将向您展示利用批量读取机制的 AngularJS 代码,以及如何运行示例应用程序。

请求和响应对象的设计

让我从后端 API 代码的请求和响应对象类型开始。对于一次性加载所有内容的旧方法,不需要特定的请求对象,响应就是数据项的列表。既然我们知道这种方法不是最优的,我们必须将长列表分解成更小的部分来逐个获取,那么我们就需要一个请求来指定要获取长列表的哪一部分,并且我们还需要一个响应来代表我们请求的那部分长列表。是的,我描述的就是分页数据获取。请求包含我想要的数据项页面,响应将包含我请求的数据项页面。这是请求对象类型:

package org.hanbo.boot.rest.models;

public class BatchReadRequest
{
   private int startIdx;
   
   private int batchItemsCount;

   public int getStartIdx()
   {
      return startIdx;
   }

   public void setStartIdx(int startIdx)
   {
      this.startIdx = startIdx;
   }

   public int getBatchItemsCount()
   {
      return batchItemsCount;
   }

   public void setBatchItemsCount(int batchItemsCount)
   {
      this.batchItemsCount = batchItemsCount;
   }
}

这个类非常简单。该对象将有两个属性。第一个指定了数据项在长列表中的起始索引。第二个指定了本次请求中要获取的数据项数量。是的,正如我所说,这就是分页数据获取,没什么秘密可言。

接下来,我想向您展示响应对象类型:

package org.hanbo.boot.rest.models;

import java.util.List;
import java.util.ArrayList;

public class BatchRetrievalResponse<T extends Object>
{
   private List<T> listOfItems;
   
   private int totalCount;
   
   private int pageItemsCount;
   
   private int recordStartIdx;
   
   private int recordEndIdx;
   
   private boolean moreRecordsAvailable;
   
   public BatchRetrievalResponse()
   {
      setListOfItems(new ArrayList<T>());
   }

   public List<T> getListOfItems()
   {
      return listOfItems;
   }

   public void setListOfItems(List<T> listOfItems)
   {
      this.listOfItems = listOfItems;
   }

   public long getTotalCount()
   {
      return totalCount;
   }

   public void setTotalCount(int totalCount)
   {
      this.totalCount = totalCount;
   }

   public long getRecordStartIdx()
   {
      return recordStartIdx;
   }

   public void setRecordStartIdx(int recordStartIdx)
   {
      this.recordStartIdx = recordStartIdx;
   }

   public long getRecordEndIdx()
   {
      return recordEndIdx;
   }

   public void setRecordEndIdx(int recordEndIdx)
   {
      this.recordEndIdx = recordEndIdx;
   }

   public int getPageItemsCount()
   {
      return pageItemsCount;
   }

   public void setPageItemsCount(int pageItemsCount)
   {
      this.pageItemsCount = pageItemsCount;
   }

   public boolean isMoreRecordsAvailable()
   {
      return moreRecordsAvailable;
   }

   public void setMoreRecordsAvailable(boolean moreRecordsAvailable)
   {
      this.moreRecordsAvailable = moreRecordsAvailable;
   }
}

这个类比请求对象类型稍微复杂一些。它包含以下属性:

  • listOfItems:返回的数据项列表
  • totalCount:整个列表中的数据项总数
  • pageItemsCount:返回列表的数据项计数。通常,它与请求中的项目计数相同。如果响应包含整个列表的末尾部分,则此属性包含列表的实际项目计数。
  • recordStartIdx:数据项在整个列表中的起始索引
  • recordEndIdx:数据项在整个列表中的结束索引
  • moreRecordsAvailable:一个标志变量,可以指示整个列表中是否还有更多项目

我将此响应类型创建为模板数据类型。模板类型扩展自 Object 类,因此在此类中使用的实际数据类型必须是对象,而不是原始类型。通过将此类定义为模板类,我可以在任何适合的项目中重用此对象类型。只要有机会,我就会利用这一点。

在下一节中,我将讨论能够使用这些请求和响应对象类型来处理分页数据获取的 RESTFul API 的设计。

一次获取一页数据

为了分批获取数据并使机制尽可能通用,以便我可以在其他项目中重用它,我必须使用接口和实现,然后使它们尽可能松散耦合。我需要三个不同的部分:

  • 第一部分是转换器。当我从后端获取数据列表时,数据类型可能与前端使用的数据类型不同。因此,需要一个通用的数据转换器。
  • 第二部分是数据加载器。这个数据加载器必须有项目特定的实现,以便它可以根据页面计算(即起始索引和要加载的项目数)来加载数据。
  • 第三部分是一个通用的协调器,它可以协调前两部分来加载数据、转换数据,然后将数据列表打包到响应对象中。

如果您通读了描述,您就知道您正在看的是策略模式。现在,让我们来看看这些“部分”的接口和实现。我将从三者中最简单的数据转换器开始。

这是数据转换器类的接口:

package org.hanbo.boot.rest.services;

import java.util.List;

public interface DataObjectConverter<T extends Object, K extends Object>
{
   T convertValue(K origVal);
   
   List<T> convertValues(List<K> origVals);
}

这个接口非常简单。它使用了两种模板数据类型,TK。两者都继承自 Object 类。该接口定义了将一种类型的对象转换为另一种类型对象的契约。有两个方法。第一个将一个对象从一种类型转换为另一种。第二个将一个类型的对象列表转换为另一种类型。现在让我们看一下实现。实现是项目特定的。在这个示例应用程序中,使用的数据类型就是 String 类型。实现类看起来像这样:

package org.hanbo.boot.rest.services;

import java.util.ArrayList;
import java.util.List;

import org.springframework.stereotype.Service;

@Service
public class SampleObjectsConverter
   implements DataObjectConverter<String, String>
{
   @Override
   public String convertValue(String origVal) {
      return origVal;
   }

   @Override
   public List<String> convertValues(List<String> origVals) {
      List<String> retVal = new ArrayList<String>();
      if (origVals != null)
      {
         retVal.addAll(origVals);
      }
      
      return retVal;
   }
}

现在,我们转向下一个接口。这个也非常简单——数据加载对象类型。这个接口看起来像这样:

package org.hanbo.boot.rest.services;

import java.util.List;

public interface BatchObjectsFetcher<K extends Object>
{
   int getTotalItemsCount();
   
   boolean batchLoadItems(int startIdx, int itemsCount, List<K> retList);
}

这是此数据加载对象类型的项目特定实现:

package org.hanbo.boot.rest.services;

import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Repository;

@Repository
public class SampleBatchObjectsFetcher
   implements BatchObjectsFetcher<String>
{
   private static final int TOTAL_ITEMS = 800399;
   private static List<String> allDataItems;
   
   static
   {
      allDataItems = new ArrayList<String>();
      
      for (int i = 0; i < TOTAL_ITEMS; i++)
      {
         String itemToAdd = String.format("This is text line #%d.", (i+1));
         allDataItems.add(itemToAdd);
      }
   }
   
   @Override
   public int getTotalItemsCount()
   {
      return allDataItems.size();
   }
   
   @Override
   public boolean batchLoadItems(int startIdx, int itemsCount, List<String> retList)
   {
      if (retList == null)
      {
         return false;
      }
      
      retList.clear();
      int tempStartIdx = startIdx;
      if (startIdx < 0)
      {
         tempStartIdx = 0;
      }
      
      if (startIdx >= allDataItems.size())
      {
         startIdx = allDataItems.size() - 1;
      }
      
      int tempItemsCount = itemsCount;
      if (tempItemsCount < 0)
      {
         tempItemsCount = 1;
      }
      
      int tempEndIdx = tempStartIdx + tempItemsCount - 1;
      if (tempEndIdx >= allDataItems.size())
      {
         tempEndIdx = allDataItems.size() - 1;
      }
      
      for (int i = tempStartIdx; i <= tempEndIdx; i++)
      {
         String objToAdd = allDataItems.get(i);
         if (objToAdd != null)
         {
            retList.add(objToAdd);
         }
      }
      
      return true;
   }
}

该实现类被标记为一个仓库(repository)。我用这个类的目的是从后端数据存储中加载数据。为了演示,我创建了一个包含 800,399 个句子的长列表。这个实现的唯一方法是通过查找起始索引和要加载的项目数来加载数据,这些是该方法的参数。

我想指出这个方法的一些特性。它会检查输入参数,确保起始索引有效,并进行计算以确保结束索引也是正确的。之后,我使用一个 for 循环将句子从原始列表复制到返回列表中。由于这是一个简单的演示,计算和数据复制很容易做。对于真实项目,后端数据存储可能简单也可能复杂,通过起始索引和项目计数加载数据可能会更复杂。我将在总结部分讨论这一点。最后,我将向您展示数据加载协调接口及其实现。

这就是策略模式发挥作用的地方。我上面讨论的两个接口是策略,而协调接口将利用这些策略来处理加载分页数据的请求。这个协调对象的接口看起来像这样:

package org.hanbo.boot.rest.services;

import org.hanbo.boot.rest.models.BatchRetrievalResponse;

public interface BatchLoadObjectsService<T extends Object, K extends Object>
{
   BatchRetrievalResponse<T> batchLoadObjects(
      BatchObjectsFetcher<K> objsFetcher,
      DataObjectConverter<T, K> converter,
      int startIdx, int itemsCount);
}

如图所示,该接口只有一个方法。它接受获取器(fetcher)对象和数据对象转换器的参数,以及原始列表中项目的起始索引和要获取的项目计数。我选择使用参数而不是将对象获取器和数据转换器自动装配(auto-wiring)为实现的属性,原因是我可以创建此接口的一个通用实现,该实现可以在其他项目中重用。如果我强制将这两个对象作为实现类的自动装配属性,我不仅需要为每个新项目创建一个新的实现,而且可能还需要在同一个项目中创建此接口的多个实现。通过将这两个对象移入方法作为参数,我可以创建此接口的一个实现,该实现可用于几乎所有相关的设计需求。这是我的实现:

package org.hanbo.boot.rest.services;

import java.util.ArrayList;
import java.util.List;

import org.hanbo.boot.rest.models.BatchRetrievalResponse;
import org.springframework.stereotype.Service;

@Service
public class BatchLoadObjectsServiceImpl<T extends Object, K extends Object>
   implements BatchLoadObjectsService<T, K>
{
   public BatchLoadObjectsServiceImpl()
   {
   }
   
   @Override
   public BatchRetrievalResponse<T> batchLoadObjects(
      BatchObjectsFetcher<K> objsFetcher,
      DataObjectConverter<T, K> converter,
      int startIdx, int itemsCount)
   {
      BatchRetrievalResponse<T> resp = new BatchRetrievalResponse<T>();

      if (objsFetcher == null)
      {
         throw new IllegalArgumentException("Objects fetcher cannot be null.");
      }
      
      if (converter == null)
      {
         throw new IllegalArgumentException("Objects converter cannot be null.");
      }
      
      if (startIdx < 0)
      {
         startIdx = 0;
      }
      
      if (itemsCount <= 0)
      {
         itemsCount = 1;
      }
      
      int totalItems = objsFetcher.getTotalItemsCount();
      if (totalItems <= 0)
      {
         resp.setTotalCount(0);
         resp.setPageItemsCount(0);
         resp.setRecordStartIdx(0);
         resp.setMoreRecordsAvailable(false);
         resp.setRecordEndIdx(0);
         resp.setListOfItems(new ArrayList<T>());
         
         return resp;
      }
         
      resp.setTotalCount(totalItems);
      resp.setPageItemsCount(itemsCount);
      resp.setRecordStartIdx(startIdx);
      int endIdx = startIdx + itemsCount - 1;
      if (endIdx >= totalItems)
      {
         endIdx = totalItems - 1;
         resp.setPageItemsCount((int)(endIdx - startIdx + 1));
      }
      resp.setRecordEndIdx(endIdx);
      
      List<K> respList = new ArrayList<K>();
      objsFetcher.batchLoadItems(startIdx, itemsCount, respList);
      if (respList.size() == 0)
      {
         resp.setTotalCount(0);
         resp.setPageItemsCount(0);
         resp.setRecordStartIdx(startIdx);
         resp.setMoreRecordsAvailable(false);
         resp.setRecordEndIdx(0);
         resp.setListOfItems(new ArrayList<T>());
      }
      else if (respList.size() > 0 && respList.size() < itemsCount)
      {
         resp.setMoreRecordsAvailable(false);
         resp.setPageItemsCount(respList.size());
         List<T> respList2 = converter.convertValues(respList);
         resp.setListOfItems(respList2);
      }
      else
      {
         resp.setMoreRecordsAvailable(true);
         resp.setPageItemsCount(itemsCount);
         List<T> respList2 = converter.convertValues(respList);
         resp.setListOfItems(respList2);
      }
      
      return resp;
   }
}

为什么我要创建这样一个实现来满足几乎所有相关的设计要求?原因是一旦我创建了一种设计要求,对实现稍作调整就可以满足不同的设计要求。我相信我们所有人都做过这样的事情:复制一个接口的实现,进行调整,然后应用到同一个项目的不同部分,或者将调整后的实现复制到不同的项目中。这可能导致大量重复代码,并给维护带来噩梦。

该方法首先检查输入参数,以确保项目的起始索引和项目计数是有效的。起始索引必须大于或等于 0。项目计数必须大于 0。两个策略对象都不能为 null。检查之后,该方法将获取原始列表中的项目总数。获取总项目数不是必须的,但这个数字可以在几个方面帮助我们,例如双重检查起始索引或让客户端能够估计获取所有项目所需的次数等。

如果原始列表中的项目总数为 0,那么该方法将不执行任何获取操作,而是创建一个包装了空列表的响应对象。如果总数大于 0,那么该方法将使用对象获取器来获取由起始索引和项目计数指定的项目页面。获取操作将有三种不同的结果。第一种可能是结果包含一个代表数据项页面的项目列表,并且还有更多项目可用。第二种结果可能是它已经到达列表的末尾,返回的列表将是剩余的项目,并且将不再有更多项目可用。第三种结果可能是列表为空,这表示原始列表中没有更多项目了。这三个 if-else 块处理了这些可能的结果。并根据每种结果,创建并返回响应对象给调用者。一旦返回的列表可用,该方法会使用数据转换器对象来为响应对象转换列表。只要后端数据能够以列表的形式表示,该方法就应该能够以分批页面的形式从中获取项目。

接下来,我们将在 RESTFul API 控制器中使用这个策略模式。这是我的控制器:

package org.hanbo.boot.rest.controllers;

import org.hanbo.boot.rest.models.BatchReadRequest;
import org.hanbo.boot.rest.models.BatchRetrievalResponse;
import org.hanbo.boot.rest.services.BatchLoadObjectsService;
import org.hanbo.boot.rest.services.BatchObjectsFetcher;
import org.hanbo.boot.rest.services.DataObjectConverter;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class BatchLoadItemsController
{
   private BatchObjectsFetcher<String> _batchObjsFetcher;
   private BatchLoadObjectsService<String, String> _batchLoadSvc;
   private DataObjectConverter<String, String> _dataObjConverter;
   
   public BatchLoadItemsController(
         BatchObjectsFetcher<String> batchObjsFetcher,
         BatchLoadObjectsService<String, String> batchLoadSvc,
         DataObjectConverter<String, String> dataObjConverter)
   {
      _batchObjsFetcher = batchObjsFetcher;
      _batchLoadSvc = batchLoadSvc;
      _dataObjConverter = dataObjConverter;
   }
   
   @RequestMapping(value = "/batchread", method = RequestMethod.POST,
                   consumes = MediaType.APPLICATION_JSON_VALUE,
                   produces = MediaType.APPLICATION_JSON_VALUE)
   public ResponseEntity<BatchRetrievalResponse<String>> 
          batchReadRecords(@RequestBody BatchReadRequest req)
   {
      BatchRetrievalResponse<String> resp = new BatchRetrievalResponse<String>();
      if (req != null)
      {
         
         resp = _batchLoadSvc.batchLoadObjects(_batchObjsFetcher, 
                _dataObjConverter, req.getStartIdx(), req.getBatchItemsCount());
         
         if (resp != null)
         {
            return ResponseEntity.ok(resp);
         }
         else
         {
            return ResponseEntity.notFound().build();
         }
      }
      else
      {
         ResponseEntity<BatchRetrievalResponse<String>> retVal = 
                                      ResponseEntity.badRequest().build();
         return retVal;
      }
   }
}

这个类并不复杂。我为数据获取操作声明了三个属性。它们就是本节中我讨论过的接口。控制器的构造函数将为这些属性自动装配实例。该类中唯一的方法处理传入的 API 请求。这个方法就是可以按页获取数据项的方法。因为请求是一个 JSON 对象,我必须让这个方法处理 HTTP POST 请求。这个方法的内容只是调用获取协调器,并使用数据获取器和转换器来完成繁重的工作。

以上都是后端的设计。为了演示后端的工作,我有一个 Angular 应用程序可以做到这一点。它如何分批或分页获取所有项目将在下一节中讨论。

通过 AngularJS 和 UI-Grid 批量读取所有数据项

为了演示数据的批量加载,我需要 UI-GridUI-Grid 是可以轻松与 AngularJS 集成的最佳开源组件。它提供了许多开箱即用的功能,可以轻松配置使用。对于这个示例应用程序,我为 UI-Grid 添加了最基本的文件集。而且它能正常工作。您可以查看 assets 文件夹中的 ui-grid 文件夹,看看文件是如何添加到这个项目中的。

正如我之前提到的,示例应用程序只有一个页面。在其中,有一个基于 UI-Grid 的表格。页面一渲染,它就会开始加载数据,只要每批加载完成,数据项就会不断累积。这是该页面的 HTML 标记页面:

<div class="row">
   <div class="col-xs-12 text-center">
      <h3>Bulk Load Lots of Items</h3>
   </div>
</div>

<div class="row">
   <div class="col-xs-12 col-sm-offset-1 col-sm-10 col-md-offset-2 
               col-md-8 col-lg-offset-3 col-lg-6">
      <div class="panel panel-default">
         <div class="panel-heading">All the Items</div>
         <div ui-grid="vm.gridData" ui-grid-pagination></div>
      </div>
   </div>
</div>

这个页面非常简单,它只有一个 Bootstrap 面板和一个基于 UI-Grid 的表格。UI-Grid 接收一个名为 vm.gridDatascope 变量。这个 scope 变量在 AngularJS 控制器中初始化。这个控制器就是完成重复批量数据加载的地方。

这是 AngularJS 控制器类:

export class AppController { 
   constructor($rootScope, $scope, $timeout, batchLoadService) {
      this._rootScope = $rootScope;
      this._scope = $scope;
      this._batchLoadService = batchLoadService;
      this._allItems = [];
      this._timeout = $timeout;
      this._gridData = { 
         paginationPageSizes: [25, 50, 75],
         paginationPageSize: 25,
         data: null,
         columnDefs: [
            {
               field: "value",
               displayName: "Sentence",
               width: 550
            }
         ]
      };
      
      this.loadData();
   }
   
   get allItems() {
      return this._allItems;
   }
   
   set allItems(val) {
      this._allItems = val;
   }
   
   get gridData() {
      return this._gridData;
   }
   
   set gridData(val) {
      this._gridData = val;
   }
    
   loadData() {
      let startIdx = 0,
          batchItemsCount = 80;
    
      this.batchLoadData(startIdx, batchItemsCount);
   }
   
   batchLoadData(startIdx, itemsCount) {
      let req = {
         startIdx: startIdx,
         batchItemsCount: itemsCount
      }, self = this;
      self._batchLoadService.batchLoad(req).then(function(result) {
         if (result) {
            if (result.listOfItems) {
               let i = 0; 
               for (; i < result.listOfItems.length; i++) {
                  self._allItems.push({ "value": result.listOfItems[i] });
               }
            }
            self._gridData.data = self._allItems;
            
            self._timeout(function() {
               if (result.moreRecordsAvailable) {
                  self.batchLoadData(startIdx + itemsCount, itemsCount);
               }
            }, 3000);
         } else {
            console.log("No result available");
         }
      }, function(error) {
         if (error) {
            console.log(error);
         } else {
            console.log("Unknown error occurred while fetch batch load.");
         }
      });
   }
}

这是初始化 UI-Grid 组件的代码:

      this._gridData = { 
         paginationPageSizes: [25, 50, 75],
         paginationPageSize: 25,
         data: null,
         columnDefs: [
            {
               field: "value",
               displayName: "Sentence",
               width: 550
            }
         ]
      };

控制器的 _gridData 属性是一个 JavaScript 对象,用作 UI-Grid 组件的配置数据。我使用了最少的配置集。第一个,paginationPageSizes,是一个选项列表,允许用户设置每页(显示时)要显示的项目数。然而,这与从后端获取的项目数无关。第二个,paginationPageSize,设置页面上默认显示的项目数。同样,它与从后端获取的项目数无关。第三个,data,是要在页面上显示的项目列表。它暂时设置为 null。每当批量数据加载完成时,此属性都会更新。最后,是 columnDefs 属性。此属性用于设置列的显示标题和要显示的值。

批量读取数据的代码是这个:

...
   batchLoadData(startIdx, itemsCount) {
      let req = {
         startIdx: startIdx,
         batchItemsCount: itemsCount
      }, self = this;
      self._batchLoadService.batchLoad(req).then(function(result) {
         if (result) {
            if (result.listOfItems) {
               let i = 0; 
               for (; i < result.listOfItems.length; i++) {
                  self._allItems.push({ "value": result.listOfItems[i] });
               }
            }
            self._gridData.data = self._allItems;
            
            self._timeout(function() {
               if (result.moreRecordsAvailable) {
                  self.batchLoadData(startIdx + itemsCount, itemsCount);
               }
            }, 3000);
         } else {
            console.log("No result available");
         }
      }, function(error) {
         if (error) {
            console.log(error);
         } else {
            console.log("Unknown error occurred while fetch batch load.");
         }
      });
   }
...

这个方法不难理解,参数是长列表中项目的起始索引和要获取的项目数。进入方法,您首先会看到 request 对象的准备工作。然后,该方法调用服务对象 _batchLoadService 并调用其 batchLoad() 方法。一旦加载成功,$promisethen() 方法将处理数据,以便可以在 UI-Grid 中显示。在 then() 方法内部,数据添加到 UI-Grid 后,service 对象将通过更改起始索引为项目计数(转到下一页数据)来再次调用其 batchLoad()。在两次连续的数据读取之间,有 3 秒的等待。我使用 $timeout 来创建这个 `等待` 操作。它将使数据读取操作更容易跟踪。如果你还没有注意到,这是一个递归。它将重复进行,直到指示符 result.moreRecordsAvailable 的值被设置为 false

您可能想知道服务类 batchLoadService 是什么样的。它在这里:

export class BatchLoadService {
   constructor ($resource) {
      this._loadSvc = $resource(null, null, {
         batchLoad: {
            url: "/batchread",
            method: "post",
            isArray: false
         }
      });
   }
   
   batchLoad(req) {
      return this._loadSvc.batchLoad(req).$promise;
   }
}

在这个服务类 BatchLoadService 中,我注入了 $resource,并用它来调用后端批量读取数据的 API。在我过去的教程中,我已经多次解释过这是如何做的,所以我就不再赘述代码是如何工作的了。

这是设置页面导航路由的代码:

export function appRouting($routeProvider) {
  $routeProvider.when("/", {
    templateUrl : "/assets/app/pages/index.html",
    controller: "AppController",
    controllerAs: "vm"
  });
}

这是一个可以添加到 AngularJS 应用程序中的方法,用于帮助配置浏览器 URL 和要显示的页面。我就是在这里将页面和页面控制器关联起来的。

以下代码是包含这个单页应用程序的 AngularJS 模块。它设置了应用程序各个部分所使用的依赖注入:

import { appRouting } from '/assets/app/js/app-routing.js';
import { AppController } from '/assets/app/js/AppController.js';
import { BatchLoadService } from '/assets/app/js/BatchLoadService.js';

let app = angular.module('startup', [ "ngRoute", "ngResource", 
                         'ui.grid', 'ui.grid.pagination' ]);
app.config(appRouting);
app.factory("BatchLoadService", [ "$resource", BatchLoadService])
app.controller("AppController", [ "$rootScope", "$scope", 
               "$timeout", "BatchLoadService", AppController ]);

我所列出的是前端 Web 应用程序最基本的部分。我必须说,这个前端应用程序的错误处理部分非常薄弱。但由于这只是一个无用的演示,所以我在这里就偷工减料了。既然我们已经将所有东西都集成好了,是时候来试试这个示例应用程序了。在下一节中,我将描述如何构建并运行这个示例应用程序。

如何运行示例应用程序

在您下载源代码后,请将其解压到您选择的文件夹中,然后浏览项目的子目录,将所有 *.sj 文件重命名为 *.js 文件。此外,此应用程序使用 Java 17,如果您使用的是较低版本的 Java,请修改 pom.xml 文件以降低 Java 版本。

在项目的基础目录中,您可以找到 pom.xml 文件。使用控制台应用程序并运行以下命令:

mvn clean install 

一旦构建过程成功完成,就该运行应用程序了:

java -jar target/hanbo-angular-batch-read-sample-1.0.1.jar 

应用程序启动时会在控制台窗口中输出大量信息。一旦启动成功完成,您就可以使用浏览器运行该应用程序,只需导航到以下 URL:

https://:8080/ 

页面一显示,UI-Grid 表格中就会有数据显示出来。并且每隔三秒钟,就会有更多数据被添加到 UI-Grid 表格中。由于我硬编码了 800,000 多条数据项,并且每批硬编码的项目数为 80,所以加载所有数据需要很长时间。尽管如此,您应该能够实时看到数据的加载情况。如果您打开浏览器的“开发者模式”,并对内存使用情况进行分析,您会发现持续的操作并不会消耗大量内存。此外,UI-Grid 能够根据指定的页面项目计数正确地将数据项分解成页面。

摘要

作为我 2023 年的第一篇教程,这是一次巨大的成功。在本教程中,我详细解释了如何分批加载海量数据。这种设计的目的是,我想尽快加载少量数据,以便可以在应用程序中显示。然后应用程序可以花时间加载剩余的数据并显示它们。

与那种花费很长时间一次性加载所有内容,然后在应用程序中显示的直接方法相比,这种方法要复杂得多。实现起来也更困难一些。必须在垂直技术栈的每一层都进行更改,前端应用程序必须更改为执行批量读取。在 RESTFul API 层,我们需要添加一些新的 API 来处理批量读取。这意味着服务层也必须添加新代码或修改现有代码库以适应这种变化。最后,在数据仓库层和数据库层,必须正确地完成数据的查询、排序和分页。如果您的项目代码库一团糟,我建议您不要尝试用这种新方法去改造它。而对于一个易于维护的项目,当您尝试添加这种设计时也要非常小心。

不管怎样,我很高兴这篇教程的结果。UI-Grid 对于 AngularJS 应用程序来说确实是一个了不起的 UI 组件。没有它,要试验这样的设计会困难得多。这证明了那句老话:“不要自己创造设计,而是使用一个好的替代品……” 我希望您喜欢我最新的教程。一如既往,我享受写作的过程。希望它能帮助您解决类似的问题。祝您好运!

历史

  • 2023年2月9日 - 初稿
© . All rights reserved.