使用Spring REST和AngularJS表进行分页

2023/05/19

1. 概述

在本文中,我们将主要关注在Spring REST API和简单的 AngularJS 前端中实现服务器端分页。

我们还将探索 Angular 中一个常用的表格网格,名为UI Grid

2.依赖关系

这里我们详细介绍本文需要的各种依赖。

2.1. JavaScript

为了让 Angular UI Grid 正常工作,我们需要在 HTML 中导入以下脚本。

2.2. 行家

对于我们的后端,我们将使用Spring Boot,因此我们需要以下依赖项:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-tomcat</artifactId>
    <scope>provided</scope>
</dependency>

注意:此处未指定其他依赖项,有关完整列表,请查看GitHub项目中的完整pom.xml 。

三、关于申请

该应用程序是一个简单的学生目录应用程序,允许用户在分页表格网格中查看学生详细信息。

该应用程序使用Spring Boot并在具有嵌入式数据库的嵌入式 Tomcat 服务器中运行。

最后,在 API 方面,有几种方法可以进行分页,在此处的 Spring 中的 REST 分页一文中有所描述——强烈建议与本文一起阅读。

我们这里的解决方案很简单——在 URI 查询中包含分页信息,如下所示:/student/get?page=1&size=2。

4.客户端

首先,我们需要创建客户端逻辑。

4.1. 用户界面网格

我们的index.html将包含我们需要的导入和表格网格的简单实现:

<!DOCTYPE html>
<html lang="en" ng-app="app">
    <head>
        <link rel="stylesheet" href="https://cdn.rawgit.com/angular-ui/
          bower-ui-grid/master/ui-grid.min.css">
        <script src="https://ajax.googleapis.com/ajax/libs/angularjs/
          1.5.6/angular.min.js"></script>
        <script src="https://cdn.rawgit.com/angular-ui/bower-ui-grid/
          master/ui-grid.min.js"></script>
        <script src="view/app.js"></script>
    </head>
    <body>
        <div ng-controller="StudentCtrl as vm">
            <div ui-grid="gridOptions" class="grid" ui-grid-pagination>
            </div>
        </div>
    </body>
</html>

让我们仔细看看代码:

  • ng-app – 是加载模块app的 Angular 指令。这些下的所有元素都将成为应用程序模块的一部分
  • ng-controller – 是 Angular 指令,它使用别名vm加载控制器StudentCtrl 。这些下的所有元素都将成为StudentCtrl控制器的一部分
  • ui-grid – 是属于 Angular ui-grid的 Angular 指令,使用gridOptions作为其默认设置,gridOptions在app.js中的$scope下声明

4.2. AngularJS 模块

让我们首先在app.js中定义模块:

var app = angular.module('app', ['ui.grid','ui.grid.pagination']);

我们声明了应用程序模块并注入了 ui.grid以启用 UI-Grid 功能;我们还注入了 ui.grid.pagination以启用分页支持。

接下来,我们将定义控制器:

app.controller('StudentCtrl', ['$scope','StudentService', 
    function ($scope, StudentService) {
        var paginationOptions = {
            pageNumber: 1,
            pageSize: 5,
        sort: null
        };

    StudentService.getStudents(
      paginationOptions.pageNumber,
      paginationOptions.pageSize).success(function(data){
        $scope.gridOptions.data = data.content;
        $scope.gridOptions.totalItems = data.totalElements;
      });

    $scope.gridOptions = {
        paginationPageSizes: [5, 10, 20],
        paginationPageSize: paginationOptions.pageSize,
        enableColumnMenus:false,
    useExternalPagination: true,
        columnDefs: [
           { name: 'id' },
           { name: 'name' },
           { name: 'gender' },
           { name: 'age' }
        ],
        onRegisterApi: function(gridApi) {
           $scope.gridApi = gridApi;
           gridApi.pagination.on.paginationChanged(
             $scope, 
             function (newPage, pageSize) {
               paginationOptions.pageNumber = newPage;
               paginationOptions.pageSize = pageSize;
               StudentService.getStudents(newPage,pageSize)
                 .success(function(data){
                   $scope.gridOptions.data = data.content;
                   $scope.gridOptions.totalItems = data.totalElements;
                 });
            });
        }
    };
}]);

现在让我们看一下$scope.gridOptions中的自定义分页设置:

  • paginationPageSizes – 定义可用的页面大小选项
  • paginationPageSize – 定义默认页面大小
  • enableColumnMenus – 用于启用/禁用列上的菜单
  • useExternalPagination – 如果你在服务器端分页则需要
  • columnDefs – 将自动映射到从服务器返回的 JSON 对象的列名称。从服务器返回的 JSON 对象中的字段名和定义的列名应该匹配。
  • onRegisterApi – 在网格内注册公共方法事件的能力。在这里,我们注册了gridApi.pagination.on.paginationChanged以告诉 UI-Grid 在页面更改时触发此函数。

并将请求发送到 API:

app.service('StudentService',['$http', function ($http) {

    function getStudents(pageNumber,size) {
        pageNumber = pageNumber > 0?pageNumber - 1:0;
        return $http({
          method: 'GET',
            url: 'student/get?page='+pageNumber+'&size='+size
        });
    }
    return {
        getStudents: getStudents
    };
}]);

5.后端和API

5.1. RESTful 服务

下面是支持分页的简单 RESTful API 实现:

@RestController
public class StudentDirectoryRestController {

    @Autowired
    private StudentService service;

    @RequestMapping(
      value = "/student/get", 
      params = { "page", "size" }, 
      method = RequestMethod.GET
    )
    public Page<Student> findPaginated(
      @RequestParam("page") int page, @RequestParam("size") int size) {

        Page<Student> resultPage = service.findPaginated(page, size);
        if (page > resultPage.getTotalPages()) {
            throw new MyResourceNotFoundException();
        }

        return resultPage;
    }
}

@RestController是在 Spring 4.0中引入的,作为一种方便的注解,它隐式声明了@Controller和@ResponseBody。

对于我们的 API,我们声明它接受两个参数,即页面和大小,这两个参数也将决定返回给客户端的记录数。

我们还添加了一个简单的验证,如果页码高于总页数,该验证将抛出MyResourceNotFoundException 。

最后,我们将返回Page作为 Response——这是 S pring Data的一个超级有用的组件,它保存了分页数据。

5.2. 服务实施

我们的服务将简单地根据控制器提供的页面和大小返回记录:

@Service
public class StudentServiceImpl implements StudentService {

    @Autowired
    private StudentRepository dao;

    @Override
    public Page<Student> findPaginated(int page, int size) {
        return dao.findAll(new PageRequest(page, size));
    }
}

5.3. 存储库实现

对于我们的持久层,我们使用嵌入式数据库和 Spring Data JPA。

首先,我们需要设置持久性配置:

@EnableJpaRepositories("com.baeldung.web.dao")
@ComponentScan(basePackages = { "com.baeldung.web" })
@EntityScan("com.baeldung.web.entity") 
@Configuration
public class PersistenceConfig {

    @Bean
    public JdbcTemplate getJdbcTemplate() {
        return new JdbcTemplate(dataSource());
    }

    @Bean
    public DataSource dataSource() {
        EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
        EmbeddedDatabase db = builder
          .setType(EmbeddedDatabaseType.HSQL)
          .addScript("db/sql/data.sql")
          .build();
        return db;
    }
}

持久性配置很简单——我们有@EnableJpaRepositories来扫描指定的包并找到我们的 Spring Data JPA 存储库接口。

我们这里有@ComponentScan来自动扫描所有 bean,我们有 @EntityScan(来自 Spring Boot)来扫描实体类。

我们还声明了我们的简单数据源——使用将运行启动时提供的 SQL 脚本的嵌入式数据库。

现在是我们创建数据存储库的时候了:

public interface StudentRepository extends JpaRepository<Student, Long> {}

这基本上就是我们在这里需要做的所有事情;如果你想更深入地了解如何设置和使用功能强大的 Spring Data JPA,请务必阅读此处的指南

6. 分页请求和响应

调用 API – http://localhost:8080/student/get?page=1&size=5时,JSON 响应将如下所示:

{
    "content":[
        {"studentId":"1","name":"Bryan","gender":"Male","age":20},
        {"studentId":"2","name":"Ben","gender":"Male","age":22},
        {"studentId":"3","name":"Lisa","gender":"Female","age":24},
        {"studentId":"4","name":"Sarah","gender":"Female","age":26},
        {"studentId":"5","name":"Jay","gender":"Male","age":20}
    ],
    "last":false,
    "totalElements":20,
    "totalPages":4,
    "size":5,
    "number":0,
    "sort":null,
    "first":true,
    "numberOfElements":5
}

这里要注意的一件事是服务器返回一个org.springframework.data.domain.Page DTO,包装我们的学生资源。

Page对象将具有以下字段:

  • last – 如果是最后一页则设置为true否则为 false
  • first – 如果是第一页则设置为true否则为 false
  • totalElements – 行/记录的总数。在我们的示例中,我们将其传递给ui-grid选项$scope.gridOptions.totalItems以确定有多少页面可用
  • totalPages – 从 ( totalElements / size )派生的页面总数
  • 大小——每页的记录数,这是通过参数大小从客户端传递的
  • number – 客户端发送的页码,在我们的响应中这个数字是 0 因为在我们的后端我们使用一个Student的数组,它是一个从零开始的索引,所以在我们的后端,我们将页码减 1
  • sort – 页面的排序参数
  • numberOfElements – 页面返回的行数/记录数

7. 测试分页

现在让我们使用RestAssured为我们的分页逻辑设置一个测试;要了解有关RestAssured的更多信息,你可以查看本教程

7.1. 准备测试

为了便于开发我们的测试类,我们将添加静态导入:

io.restassured.RestAssured.
io.restassured.matcher.RestAssuredMatchers.
org.hamcrest.Matchers.

接下来,我们将设置启用 Spring 的测试:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
@IntegrationTest("server.port:8888")

@SpringApplicationConfiguration帮助 Spring 知道如何加载ApplicationContext ,在这种情况下,我们使用Application.java来配置我们的ApplicationContext。

@WebAppConfiguration被定义为告诉 Spring 要加载的ApplicationContext应该是一个WebApplicationContext。

并且@IntegrationTest被定义为在运行测试时触发应用程序启动,这使得我们的 REST 服务可用于测试。

7.2. 测试

这是我们的第一个测试用例:

@Test
public void givenRequestForStudents_whenPageIsOne_expectContainsNames() {
    given().params("page", "0", "size", "2").get(ENDPOINT)
      .then()
      .assertThat().body("content.name", hasItems("Bryan", "Ben"));
}

上面的这个测试用例是为了测试当页面 1 和大小 2 被传递给 REST 服务时,从服务器返回的 JSON 内容应该有名字Bryan和Ben。

让我们剖析测试用例:

  • given – RestAssured的一部分,用于开始构建请求,你也可以使用with()
  • get – RestAssured的一部分,如果使用会触发 get 请求,请使用 post() 进行 post 请求
  • hasItems——检查值是否匹配的 hamcrest 部分

我们再添加几个测试用例:

@Test
public void givenRequestForStudents_whenResourcesAreRetrievedPaged_thenExpect200() {
    given().params("page", "0", "size", "2").get(ENDPOINT)
      .then()
      .statusCode(200);
}

此测试断言,当实际调用该点时,会收到 OK 响应:

@Test
public void givenRequestForStudents_whenSizeIsTwo_expectNumberOfElementsTwo() {
    given().params("page", "0", "size", "2").get(ENDPOINT)
      .then()
      .assertThat().body("numberOfElements", equalTo(2));
}

此测试断言,当请求页面大小为二时,返回的页面大小实际上为二:

@Test
public void givenResourcesExist_whenFirstPageIsRetrieved_thenPageContainsResources() {
    given().params("page", "0", "size", "2").get(ENDPOINT)
      .then()
      .assertThat().body("first", equalTo(true));
}

该测试断言,当第一次调用资源时,第一个页面名称的值为真。

存储库中还有更多测试,所以一定要看看GitHub项目。

八. 总结

本文阐述了如何在AngularJS中使用UI-Grid实现数据表格网格,以及如何实现所需的服务器端分页。

这些示例和测试的实现可以在GitHub 项目中找到。这是一个 Maven 项目,因此它应该很容易导入并按原样运行。

要运行 Spring boot 项目,你只需执行mvn spring-boot:run并在http://localhost:8080/上本地访问它。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章