jqGrid,REST,AJAX和Spring MVC集成

两年多以前,我写了一篇关于两个如何在Struts2中实现优雅的CRUD的文章。 实际上,我必须就该主题写两篇文章,因为该主题如此广泛。 今天,我采用了一套更为流行的,完善的框架和库,采用了更为轻量级的现代方法。 也就是说,我们将在后端使用Spring MVC为我们的资源提供REST接口,为jQuery提供出色的jqGrid插件以呈现表格网格(还有更多!),并且我们将使用少量JavaScript和AJAX连接所有内容。

后端实际上是该展示柜中最不有趣的部分,它可以使用能够处理RESTful请求的任何服务器端技术来实现,现在也许应该将JAX-RS视为该领域的标准。 我没有任何理由就选择了Spring MVC,但这对于这个任务来说也不是一个坏选择。 我们将通过REST接口公开CRUD操作; 历史上最畅销的书籍清单将是我们的领域模型(您能猜出谁登上领奖台吗?)

@Controller
@RequestMapping(value = "/book")
public class BookController {

  private final Map<Integer, Book> books = new ConcurrentSkipListMap<Integer, Book>();

  @RequestMapping(value = "/{id}", method = GET)
  public @ResponseBody Book read(@PathVariable("id") int id) {
    return books.get(id);
  }

  @RequestMapping(method = GET)
  public @ResponseBody Page<Book> listBooks(
      @RequestParam(value = "page", required = false, defaultValue = "1") int page,
      @RequestParam(value = "max", required = false, defaultValue = "20") int max) {
    final ArrayList<Book> booksList = new ArrayList<Book>(books.values());
    final int startIdx = (page - 1) * max;
    final int endIdx = Math.min(startIdx + max, books.size());
    return new Page<Book>(booksList.subList(startIdx, endIdx), page, max, books.size());
  }

}

几乎没有什么需要解释的。 首先,出于这个简单展示的目的,我没有使用任何数据库,所有书籍都存储在控制器内部的内存映射中。 原谅我。 第二个问题更加微妙。 由于在如何使用RESTful Web服务处理分页方面似乎尚未达成共识 ,因此我使用了简单的查询参数。 您可能会发现它很丑陋,但是我发现滥用Accept-Ranges和Range标头以及206 HTTP响应代码更加丑陋。

最后一个值得注意的细节是Page wrapper类:

@XmlRootElement
public class Page<T> {

  private List<T> rows;

  private int page;
  private int max;
  private int total;

  //...

}

我可以返回原始列表(或更确切地说,返回列表的请求部分),但是我还需要一种方法来向视图层提供方便的元数据(如记录总数),更不用说在编组/解组原始列表时遇到的一些困难。

现在,我们准备启动我们的应用程序并使用curl进行一些测试:

<!-- $ curl -v "http://localhost:8080/books/rest/book?page=1&max=2" -->

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<page>
  <total>43</total>
  <page>1</page>
  <max>3</max>
  <rows xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="book">
    <author>Charles Dickens</author>
    <available>true</available>
    <cover>PAPERBACK</cover>
    <id>1</id>
    <publishedYear>1859</publishedYear>
    <title>A Tale of Two Cities</title>
  </rows>
  <rows xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="book">
    <author>J. R. R. Tolkien</author>
    <available>true</available>
    <cover>HARDCOVER</cover>
    <id>2</id>
    <publishedYear>1954</publishedYear>
    <title>The Lord of the Rings</title>
  </rows>
  <rows xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="book">
    <author>J. R. R. Tolkien</author>
    <available>true</available>
    <cover>PAPERBACK</cover>
    <id>3</id>
    <publishedYear>1937</publishedYear>
    <title>The Hobbit</title>
  </rows>
</page>

如果未指定,则响应类型默认为XML,但是如果我们将Jackson库添加到CLASSPATH,Spring会选择它并允许我们使用JSON:

// $ curl -v -H "Accept: application/json" "http://localhost:8080/books/rest/book?page=1&max=3"

{
    "total":43,
    "max":3,
    "page":1,
    "rows":[
        {
            "id":1,
            "available":true,
            "author":"Charles Dickens",
            "title":"A Tale of Two Cities",
            "publishedYear":1859,
            "cover":"PAPERBACK",
            "comments":null
        },
        {
            "id":2,
            "available":true,
            "author":"J. R. R. Tolkien",
            "title":"The Lord of the Rings",
            "publishedYear":1954,
            "cover":"HARDCOVER",
            "comments":null
        },
        {
            "id":3,
            "available":true,
            "author":"J. R. R. Tolkien",
            "title":"The Hobbit",
            "publishedYear":1937,
            "cover":"PAPERBACK",
            "comments":null
        }
    ]
}

很好,现在我们可以在前端工作了,希望不会使我们的手太脏。 关于HTML标记,这就是我们所需要的,认真的是:

<table id="grid"></table>
<div id="pager"></div>

请记住,我们将实现所有CRUD操作,但这仍然是我们所需要的。 没有更多HTML。 多亏了出色的jqGrid库,其余的魔术才得以实现。 这是一个基本设置:

$("#grid")
    .jqGrid({
      url:'rest/book',
      colModel:[
        {name:'id', label: 'ID', formatter:'integer', width: 40},
        {name:'title', label: 'Title', width: 300},
        {name:'author', label: 'Author', width: 200},
        {name:'publishedYear', label: 'Published year', width: 80, align: 'center'},
        {name:'available', label: 'Available', formatter: 'checkbox', width: 46, align: 'center'}
      ],
      caption: "Books",
      pager : '#pager',
      height: 'auto'
    })
    .navGrid('#pager', {edit:false,add:false,del:false, search: false});

从技术上讲,这就是我们所需要的。 用来获取数据的URL,指向我们的控制器(jqGrid将为我们执行所有AJAX魔术)和数据模型(您可能会识别书本字段及其描述)。 但是,由于jqGrid是高度可定制的,因此我进行了一些调整以使网格看起来更好。 另外,我也不喜欢建议使用的元数据名称,例如从服务器返回的字段总数应该是页面总数,而不是记录总数,这很违反直觉。 这是我调整的选项:

$.extend($.jgrid.defaults, {
  datatype: 'json',
  jsonReader : {
    repeatitems:false,
    total: function(result) {
      //Total number of pages
      return Math.ceil(result.total / result.max);
    },
    records: function(result) {
      //Total number of records
      return result.total;
    }
  },
  prmNames: {rows: 'max', search: null},
  height: 'auto',
  viewrecords: true,
  rowList: [10, 20, 50, 100],
  altRows: true,
  loadError: function(xhr, status, error) {
    alert(error);
  }
  });

渴望看到结果吗? 这是浏览器的屏幕截图:

jqGrid,REST,AJAX和Spring MVC集成

漂亮的外观,可自定义的分页,轻巧的刷新……而且我们的手还是比较干净的! 但是我答应了CRUD ...如果您小心的话,您可能已经注意到了一些navGrid属性,并且很想打开它:

var URL = 'rest/book';
var options = {
  url: URL,
  editurl: URL,
  colModel:[
    {
      name:'id', label: 'ID',
      formatter:'integer',
      width: 40,
      editable: true,
      editoptions: {disabled: true, size:5}
    },
    {
      name:'title',
      label: 'Title',
      width: 300,
      editable: true,
      editrules: {required: true}
    },
    {
      name:'author',
      label: 'Author',
      width: 200,
      editable: true,
      editrules: {required: true}
    },
    {
      name:'cover',
      label: 'Cover',
      hidden: true,
      editable: true,
      edittype: 'select',
      editrules: {edithidden:true},
      editoptions: {
        value: {'PAPERBACK': 'paperback', 'HARDCOVER': 'hardcover', 'DUST_JACKET': 'dust jacket'}
      }
    },
    {
      name:'publishedYear',
      label: 'Published year',
      width: 80,
      align: 'center',
      editable: true,
      editrules: {required: true, integer: true},
      editoptions: {size:5, maxlength: 4}
    },
    {
      name:'available',
      label: 'Available',
      formatter: 'checkbox',
      width: 46,
      align: 'center',
      editable: true,
      edittype: 'checkbox',
      editoptions: {value:"true:false"}
    },
    {
      name:'comments',
      label: 'Comments',
      hidden: true,
      editable: true,
      edittype: 'textarea',
      editrules: {edithidden:true}
    }
  ],
  caption: "Books",
  pager : '#pager',
  height: 'auto'
};
$("#grid")
    .jqGrid(options)
    .navGrid('#pager', {edit:true,add:true,del:true, search: false});

配置变得非常冗长,但并没有什么复杂的事情–对于每个字段,我们都添加了一些其他属性来控制在编辑模式下应如何处理该字段。 这包括应该代表哪种类型HTML输入,验证规则,可见性等。但是说实话,我认为这是值得的:

jqGrid,REST,AJAX和Spring MVC集成

jqGrid根据上面提到的我们的编辑选项(包括验证逻辑)完全生成了一个外观漂亮的编辑窗口。 我们可以在编辑对话框中使某些字段在网格中隐藏/不活动(如id)可见,反之亦然(封面和注释不存在于网格中,但是您可以对其进行修改)。 另外请注意,在网格的左下角几乎看不到新图标。 添加和删​​除也是可能的-我们还没有编写一行HTML / JSP / JavaScript(jqGrid配置对象除外)。

jqGrid,REST,AJAX和Spring MVC集成

当然,我们都知道用户界面就是应用程序 ,我们的界面还不错,但是有时候我们真的想要一个漂亮且可以运行的应用程序。 当前,后一个要求是我们的致命弱点。 不是因为后端尚未准备好,而是相当简单的:

@Controller
@RequestMapping(value = "/book")
public class BookController {

  private final Map<Integer, Book> books = new ConcurrentSkipListMap<Integer, Book>();

  @RequestMapping(value = "/{id}", method = GET)
  public @ResponseBody Book read(@PathVariable("id") int id) {
    //...
  }

  @RequestMapping(method = GET)
  public
  @ResponseBody
  Page<Book> listBooks(
      @RequestParam(value = "page", required = false, defaultValue = "1") int page,
      @RequestParam(value = "max", required = false, defaultValue = "20") int max) {
    //...
  }

  @RequestMapping(value = "/{id}", method = PUT)
  @ResponseStatus(HttpStatus.NO_CONTENT)
  public void updateBook(@PathVariable("id") int id, @RequestBody Book book) {
    //...
  }

  @RequestMapping(method = POST)
  public ResponseEntity<String> createBook(HttpServletRequest request, @RequestBody Book book) {
    //...
  }

  @RequestMapping(value = "/{id}", method = DELETE)
  @ResponseStatus(HttpStatus.NO_CONTENT)
  public void deleteBook(@PathVariable("id") int id) {
    //...
  }

}

服务器端已经准备就绪,但是当涉及到客户端的数据操作时,jqGrid会揭示其肮脏的秘密–到服务器的所有流量都使用POST发送,如下所示:

Content-Type: application/x-www-form-urlencoded in the following format:
id=&title=And+Then+There+Were+None&author=Agatha+Christie&cover=PAPERBACK&publishedYear=1939&available=true&comments=&oper=add

最后一个属性(oper = add)是至关重要的。 不是真正的惯用REST,您不觉得吗? 如果我们只能适当地使用POST / PUT / DELETE并使用JSON或XML序列化数据……修改服务器,使其与某些JavaScript库兼容(无论它有多酷),那似乎是不得已的方法。 值得庆幸的是,只需少量的工作就可以自定义所有内容。

$.extend($.jgrid.edit, {
      ajaxEditOptions: { contentType: "application/json" },
      mtype: 'PUT',
      serializeEditData: function(data) {
        delete data.oper;
        return JSON.stringify(data);
      }
    });
$.extend($.jgrid.del, {
      mtype: 'DELETE',
      serializeDelData: function() {
        return "";
      }
    });

var URL = 'rest/book';
var options = {
  url: URL,
  //...
}

var editOptions = {
  onclickSubmit: function(params, postdata) {
    params.url = URL + '/' + postdata.id;
  }
};
var addOptions = {mtype: "POST"};
var delOptions = {
  onclickSubmit: function(params, postdata) {
    params.url = URL + '/' + postdata;
  }
};

$("#grid")
    .jqGrid(options)
    .navGrid('#pager',
    {}, //options
    editOptions,
    addOptions,
    delOptions,
    {} // search options
);

我们为每个操作定制了HTTP方法,使用JSON处理序列化,最后用于编辑和删除操作的URL现在带有/ record_id后缀。 现在它不仅看起来,而且可以工作! 查看浏览器与服务器的交互(注意不同的HTTP方法和URL):

jqGrid,REST,AJAX和Spring MVC集成

这是在浏览器端创建新资源的示例:

jqGrid,REST,AJAX和Spring MVC集成

为了尽可能严格地遵循REST原则,我返回201 Created响应代码以及Location标头,该标头指向新创建的资源。 如您所见,数据现在以JSON格式发送到服务器。

总而言之,这种方法具有很多优点:

  • GUI非常敏感,页面会立即显示(它可以是CDN提供的静态资源),而数据是通过AJAX以轻量级JSON格式异步加载的
  • 我们免费获得CRUD操作
  • 其他系统的REST接口也是免费的

将其与任何Web框架进行比较。 我是否在JavaScript结霜方面提到了这个小问题:jqGrid完全符合jQuery UI主题 ,还支持国际化。 这是具有更改的主题和语言的同一应用程序:

jqGrid,REST,AJAX和Spring MVC集成

完整的源代码可在Tomek的GitHub帐户上获得 该应用程序是独立的,只需构建它并将其部署到某个servlet容器中即可。

参考: 穷人的CRUD:jQGrid,REST,AJAX和Spring MVC在我们的JCG合作伙伴 Tomek Nurkiewicz的NoBlogDefFound博客中合而为一

相关文章 :

翻译自: https://www.javacodegeeks.com/2011/07/jqgrid-rest-ajax-spring-mvc-integration.html