显示图书类别

说明:

(1) 本篇博客的任务:在首页显示图书类别;

(2) 本篇博客说明:

● 本篇博客是:基于【SSM+Mybatis-Plus】开发的第一个案例,所以在阐述的时候略显啰嗦;后面我们在开发其他功能时,会反复使用到本篇博客的SOP

● 本篇博客有任何迷糊或者忘记的地方,都可以快速参考以前的博客,都有很详细的阐述;


为了防止干扰,把前面几篇博客创建的测试与演示用的内容删除了;

一:编码前,先了解一下情况;

1.明确任务:在首页显示图书类别;

2.看一下前端的初始情况;

3.看一下底层的数据表;


二:正式编码开发;

1.开发【实体类】,与底层数据表对应;

在和数据表交互的时候,我们一般需要先创建与之对应的实体类;

Category实体类:

     package com.imooc.reader.entity;
 
     import com.baomidou.mybatisplus.annotation.IdType;
     import com.baomidou.mybatisplus.annotation.TableField;
     import com.baomidou.mybatisplus.annotation.TableId;
     import com.baomidou.mybatisplus.annotation.TableName;
 
     /**
      * 图书分类实体类,对应category表;
      */
     @TableName("category")
     public class Category {
         @TableId(type = IdType.AUTO)
     //    @TableField("category_id")
         private Long categoryId;
     //    @TableField("category_name")
         private String categoryName;
 
         public Long getCategoryId() {
             return categoryId;
         }
 
         public void setCategoryId(Long categoryId) {
             this.categoryId = categoryId;
         }
 
         public String getCategoryName() {
             return categoryName;
         }
 
         public void setCategoryName(String categoryName) {
             this.categoryName = categoryName;
         }
 
         @Override
         public String toString() {
             return "Category{" +
                     "categoryId=" + categoryId +
                     ", categoryName='" + categoryName + '\'' +
                     '}';
         }
     }
 

说明:

(1) 实体类属性最好按照驼峰命名规则比照着数据表的字段名来写;然后,因为我们要通过Mybatis-Plus来查,所以在类上使用Mybatis-Plus的注解;

(2) 为了打印Category类对象时,更好的看到对象的内容,所以我们重写了toString()方法;

2.开发【Mapper接口】;

创建操作category图书分类表的Mapper接口;

Mapper接口:

     package com.imooc.reader.mapper;
 
     import com.baomidou.mybatisplus.core.mapper.BaseMapper;
     import com.imooc.reader.entity.Category;
 
     /**
      * 图书分类Mapper接口;
      */
     public interface CategoryMapper extends BaseMapper<Category> {
     }
 

说明:

(1) 我们要使用Mybatis- Plus,所以其继承了BaseMapper;同时,这个Mapper接口,使用来服务于category表的,即其增删改查的对象是Category对象,所以设置BaseMapper接口的泛型为Category;

3.开发mapper接口对应的【xml】;

定义了接口,就要有与之对应的xml;

category.xml:

 
     <?xml version="1.0" encoding="UTF-8"?>
     <!DOCTYPE mapper
             PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
     <mapper namespace="com.imooc.reader.mapper.CategoryMapper">
     </mapper>

说明:

(1) mapper.xml的xml声明内容,没必要记忆,需要的时候过来复制就行了;

(2) category.xml内容说明;

4.开发【Service】,调用Dao层功能,并根据业务需要编写逻辑代码;

自从接触Spring后,一直在强调接口的重要性,也在一致强调面向接口编程;通过接口,可以隐藏底层类的创建细节,提高程序的可扩展和易维护性;即,在实际开发时,service部分,一般会先定义接口,然后再创建具体的实现类;

CategoryService接口:

     package com.imooc.reader.service;
 
     import com.imooc.reader.entity.Category;
 
     import java.util.List;
 
     public interface CategoryService {
         public List<Category> selectAll();
     }
 

说明:

(1) 在CategoryService接口中,定义selectAll()方法;这个方法的目的就是查询category表的所有内容;


有了CategoryService接口后,就要编写该接口的实现;

CategoryServiceImpl实现类:

     package com.imooc.reader.service.impl;
 
     import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
     import com.imooc.reader.entity.Category;
     import com.imooc.reader.mapper.CategoryMapper;
     import com.imooc.reader.service.CategoryService;
     import org.springframework.stereotype.Service;
     import org.springframework.transaction.annotation.Propagation;
     import org.springframework.transaction.annotation.Transactional;
 
     import javax.annotation.Resource;
     import java.util.List;
 
     @Service("categoryService")
     @Transactional(propagation = Propagation.NOT_SUPPORTED,readOnly = true)
     public class CategoryServiceImpl implements CategoryService {
         @Resource
         private CategoryMapper categoryMapper;
 
         /**
          * 查询所有图书分类
          * @return
          */
         public List<Category> selectAll() {
             List<Category> list = categoryMapper.selectList(new
 QueryWrapper<Category>());
             return list;
         }
     }

说明:

(1) 类内容说明;(如果有不明白,或者模糊的地方,去前面相关的博客去查就是了)

(2) 关于事务的内容可以参考【Spring】专栏中的内容;然后目前有两点感觉:

● 在使用声明式事务时,【注解方式】似乎比【配置方式】更好点;因为【注解方式】更加灵活,更加能基于当下情况灵活设置;

● 具体一个类的事务传播方式设置成什么,需要看这个类的内容;如果一个类大部分是查询方法,那么这个类的事务传播方式完全可以设为“NOT_SUPPORTED”,然后如果类中有增删改的方法需要开启事务,那么在该类上再额外设置就好了;

(插).及时的单元测试;

以前就说过,及时的单元测试很有必要。我们写好了Dao层和Service层,可以测试一下;

CategoryServiceImplTest:

     package com.imooc.reader.service.impl;
 
     import com.imooc.reader.entity.Category;
     import com.imooc.reader.service.CategoryService;
     import org.junit.Test;
     import org.junit.runner.RunWith;
     import org.springframework.test.context.ContextConfiguration;
     import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
     import javax.annotation.Resource;
 
     import java.util.List;
 
     import static org.junit.Assert.*;
 
     @RunWith(SpringJUnit4ClassRunner.class)
     @ContextConfiguration(locations = {"classpath:applicationContext.xml"})
     public class CategoryServiceImplTest {
 
         @Resource
         private CategoryService categoryService;
         @Test
         public void selectAll() {
             List<Category> list = categoryService.selectAll();
             System.out.println(list);
             int i = 0;
         }
     }

说明:

(1) 测试类说明;

(2) 运行结果;;

5.开发【Controller】,调用service,然后将数据放入到当前请求中与模板引擎组合;

Controller控制器是承上启下的组件;

BookController类:

     package com.imooc.reader.controller;
 
     import com.imooc.reader.entity.Category;
     import com.imooc.reader.service.CategoryService;
     import org.springframework.stereotype.Controller;
     import org.springframework.web.bind.annotation.GetMapping;
     import org.springframework.web.servlet.ModelAndView;
 
     import javax.annotation.Resource;
     import java.util.List;
 
     @Controller
     public class BookController {
         @Resource
         private CategoryService categoryService;
 
         /**
          * 显示首页
          * @return
          */
         @GetMapping("/")
         public ModelAndView showIndex() {
             ModelAndView modelAndView = new ModelAndView("/index");
             List<Category> list = categoryService.selectAll();
             modelAndView.addObject("categoryList", list);
             return modelAndView;
         }
 
     }
 

说明:

(1) 这个Controller类的主要作用就是调用Service获得数据,并渲染到index.ftl这个前端文件上,并将页面推到响应中,以便浏览器显示;

(2) 类内容说明;

(3) 如果还有不明白的地方,可以快速参考【SpringMVC】中的相关内容;

6.前端文件:模板引擎读取数据,完成界面渲染,产生HTML;

index.ftl:

<!DOCTYPE html>
<html lang="en"><head>
    <meta charset="UTF-8">
    <title>MK书评网</title>
    <meta name="viewport" content="width=device-width,initial-scale=1.0, maximum-scale=1.0,user-scalable=no">
    <link rel="stylesheet" href="./resources/bootstrap/bootstrap.css">
    <link rel="stylesheet" href="./resources/raty/lib/jquery.raty.css">
    <script src="./resources/jquery.3.3.1.min.js"></script>
    <script src="./resources/bootstrap/bootstrap.min.js"></script>
    <script src="./resources/art-template.js"></script>
    <script src="./resources/raty/lib/jquery.raty.js"></script>
 
    <style>
        .highlight {
            color: red !important;
        }
        a:active{
            text-decoration: none!important;
        }
    </style>
 
 
    <style>
        .container {
            padding: 0px;
            margin: 0px;
        }
 
        .row {
            padding: 0px;
            margin: 0px;
        }
 
        .col- * {
            padding: 0px;
        }
    </style>
    <!--定义模板-->
    <script type="text/html" id="tpl">
        <a href="/book/{{bookId}}" style="color: inherit">
            <div class="row mt-2 book">
                <div class="col-4 mb-2 pr-2">
                    <img class="img-fluid" src="{{cover}}">
                </div>
                <div class="col-8  mb-2 pl-0">
                    <h5 class="text-truncate">{{bookName}}</h5>
                    <div class="mb-2 bg-light small  p-2 w-100 text-truncate">{{author}}</div>
                    <div class="mb-2 w-100">{{subTitle}}</div>
                    <p>
                        <span class="stars" data-score="{{evaluationScore}}" title="gorgeous"></span>
                        <span class="mt-2 ml-2">{{evaluationScore}}</span>
                        <span class="mt-2 ml-2">{{evaluationQuantity}}</span>
                    </p>
                </div>
            </div>
        </a>
        <hr>
    </script>
    <script>
        $.fn.raty.defaults.path = "./resources/raty/lib/images"
 
        //loadMore():加载更多数据;
        //isReset参数设置为true时:代表查询第一个数据;否则按nextPage查询后续页;
        function loadMore(isReset) {
            if (isReset == true) {
                $("#nextPage").val(1);
            }
            var nextPage = $("#nextPage").val();//首先获取下一页页号
            $.ajax({
                "url" : "/books",
                "data" : {p:nextPage},
                "type" : "get",
                "dataType" :"json",
                success : function (json) {
                    console.log(json);
                    var list = json.records;
                    for (var i = 0; i < list.length; i++) {
                        var book = json.records[i];
                        // var html = "<li>" + book.bookName+ "</li>";
                        var html = template("tpl" , book);
                        console.log(html);
                        $("#bookList").append(html);
                    }
                    $(".stars").raty({readOnly:true});//显示星型评价组件
 
                    if (json.current < json.pages) { //如果当前页小于总页数,说明还有数据:
                        $("#nextPage").val(parseInt(json.current) + 1);//把下一页的隐藏域的值,增加1
                        $("#btnMore").show();//【点击显示更多按钮】按钮应该继续显示;
                        $("#divNoMore").hide();//【没有其他数据了】不应该显示;
                    } else {   //否则,如果当前页不小于总页数,说明已经是最后一页了:
                        $("#btnMore").hide();//【点击显示更多按钮】按钮不应该继续显示了;
                        $("#divNoMore").show();//【没有其他数据了】应该显示;
                    }
                }
            })
        }
        //这个页面就绪函数,用于首页加载时,默认显示第一页数据;
        $(function () {
            loadMore(true);
            // $.ajax({
            //     "url" : "/books",
            //     "data" : {p:1},
            //     "type" : "get",
            //     "dataType" :"json",
            //     success : function (json) {
            //         console.log(json);
            //         var list = json.records;
            //         for (var i = 0; i < list.length; i++) {
            //             var book = json.records[i];
            //             // var html = "<li>" + book.bookName+ "</li>";
            //             var html = template("tpl" , book);
            //             console.log(html);
            //             $("#bookList").append(html);
            //         }
            //         $(".stars").raty({readOnly:true});//显示星型评价组件
            //     }
            // })
        })
 
        //这个页面就绪函数,用于绑定【点击加载更多】按钮的点击事件
        $(function () {
            $("#btnMore").click(function () {  //click()中增加一个匿名函数,用于事件处理
                loadMore();
            })
        })
    </script>
 
</head>
<body>
<div class="container">
    <nav class="navbar navbar-light bg-white shadow mr-auto">
        <ul class="nav">
            <li class="nav-item">
                <a href="/">
                    <img src="https://m.imooc.com/static/wap/static/common/img/logo2.png" class="mt-1" style="width: 100px">
                </a>
            </li>
 
        </ul>
        <a href="/login.html" class="btn btn-light btn-sm">
            <img style="width: 2rem;margin-top: -5px" class="mr-1" src="./images/user_icon.png">登录
        </a>
    </nav>
    <div class="row mt-2">
 
 
        <div class="col-8 mt-2">
            <h4>热评好书推荐</h4>
        </div>
 
        <div class="col-8 mt-2">
            <span data-category="-1" style="cursor: pointer" class="highlight  font-weight-bold category">全部</span>
            |
            <#list categoryList as category>
                <a style="cursor: pointer" data-category="${category.categoryId}" class="text-black-50 font-weight-bold category">${category.categoryName}</a>
                <#if category_has_next>
                    |
                </#if>
            </#list>
        </div>
 
        <div class="col-8 mt-2">
            <span data-order="quantity" style="cursor: pointer" class="order highlight  font-weight-bold mr-3">按热度</span>
 
            <span data-order="score" style="cursor: pointer" class="order text-black-50 mr-3 font-weight-bold">按评分</span>
        </div>
    </div>
    <div class="d-none">
        <input type="hidden" id="nextPage" value="2">
        <input type="hidden" id="categoryId" value="-1">
        <input type="hidden" id="order" value="quantity">
    </div>
 
    <div id="bookList">
        <!--这个div,是显示图书的容器-->
    </div>
    <button type="button" id="btnMore" data-next-page="1" class="mb-5 btn btn-outline-primary btn-lg btn-block">
        点击加载更多...
    </button>
    <div id="divNoMore" class="text-center text-black-50 mb-5" style="display: none;">没有其他数据了</div>
</div>
 
</body></html>

说明:

(1) index.ftl我们修改的内容;

(2) index.ftl获取数据;

一个老生常谈的地方:新引入依赖后,如有需要,记得添加到发布中去;

7.启动Tomcat,测试运行,观察效果;

然后访问【localhost/】;

效果:是OK的;

图书分页查询

说明:

(1) 本篇博客任务:图书分页查询:首页加载时,先默认显示第一页数据:Dao和Service部分;

(2) 本篇博客内容:图书分页查询的Dao和Service部分;其主要知识点就是Mybatis-Plus的分页查询;包括:

● Mybatis-Plus的分页查询方法:BaseMapper接口中的selectPage()方法的简介;

● IPage分页对象简述;

● IPage的三个常用方法的引入:【List<Book records = pageObject.getRecords();//获取当前页的数据】,【pageObject.getPages();//获取总页数】,【pageObject.getTotal();//获取总记录数】

● 然后,还提到了创建【接口实现类】的快捷方式:【Alt+Enter】;


我们在【2 【Mybatis-Plus】和【SSM】整合;(重要!!!)】中配置了Mybatis-Plus的分页插件,这可以帮助我们进行分页处理;

PS:在介绍Mybatis的时候,【分页插件PageHelper】也介绍了Mybatis的一款分页插件PageHelper;

因为我们分页查询,查的就是图书,所以底层就是book表;

一:正式编码开发;

1.开发实体类:Book类,与底层book表对应;

Book实体类:

     package com.imooc.reader.entity;
 
     import com.baomidou.mybatisplus.annotation.IdType;
     import com.baomidou.mybatisplus.annotation.TableId;
     import com.baomidou.mybatisplus.annotation.TableName;
 
     @TableName("book")
     public class Book {
         @TableId(type = IdType.AUTO)
         private Long bookId;
         private String bookName;
         private String subTitle;
         private String author;
         private String cover;
         private String description;
         private Long categoryId;
         private Float evaluationScore;
         private Integer evaluationQuantity;
 
         public Long getBookId() {
             return bookId;
         }
 
         public void setBookId(Long bookId) {
             this.bookId = bookId;
         }
 
         public String getBookName() {
             return bookName;
         }
 
         public void setBookName(String bookName) {
             this.bookName = bookName;
         }
 
         public String getSubTitle() {
             return subTitle;
         }
 
         public void setSubTitle(String subTitle) {
             this.subTitle = subTitle;
         }
 
         public String getAuthor() {
             return author;
         }
 
         public void setAuthor(String author) {
             this.author = author;
         }
 
         public String getCover() {
             return cover;
         }
 
         public void setCover(String cover) {
             this.cover = cover;
         }
 
         public String getDescription() {
             return description;
         }
 
         public void setDescription(String description) {
             this.description = description;
         }
 
         public Long getCategoryId() {
             return categoryId;
         }
 
         public void setCategoryId(Long categoryId) {
             this.categoryId = categoryId;
         }
 
         public Float getEvaluationScore() {
             return evaluationScore;
         }
 
         public void setEvaluationScore(Float evaluationScore) {
             this.evaluationScore = evaluationScore;
         }
 
         public Integer getEvaluationQuantity() {
             return evaluationQuantity;
         }
 
         public void setEvaluationQuantity(Integer evaluationQuantity) {
             this.evaluationQuantity = evaluationQuantity;
         }
     }
 

说明:

(1) 里面的知识点,在【显示图书类别】已经详细阐述过了,这儿就不重复啰嗦了;

(2) 写类属性的时候,一定要比照着数据表的字段名,按照驼峰命名的规则去写;别写错了;

2.开发Mapper接口:BookMapper;

BookMapper接口:

 
     package com.imooc.reader.mapper;
 
     import com.baomidou.mybatisplus.core.mapper.BaseMapper;
     import com.imooc.reader.entity.Book;
 
     public interface BookMapper extends BaseMapper<Book>{
     }
 

说明:

(1) BookMapper接口继承Mybatis-Plus的BaseMapper接口;

(2) 后面测试的时候,发现这儿报了错;原因是没有指定<Book>这个泛型;可以参考[【org.apache.ibatis.binding.BindingException:Invalid bound statement (not found)】异常;](https://blog.csdn.net/csucsgoat/article/details/122033404”【org.apache.ibatis.binding.BindingException: Invalid bound statement (not found)】异常;”)】;

3.开发mapper接口对应的xml:book.xml;

book.xml:

  <?xml version="1.0" encoding="UTF-8"?>
   <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.imooc.reader.mapper.BookMapper">
     </mapper>

说明:

(1) book.xml的命名空间,要对应上BookMapper接口;

至此,底层的操作数据库的Dao的代码就写好了;然后,就是基于Mybatis-Plus解决分页的问题了;图书分页查询的逻辑代码,我们要在Service中完成;

4.开发service,调用Dao层代码;利用Mybatis-Plus分页插件,编写分页查询逻辑;

(1)BookService接口:定义一个用于分页查询的方法;

     package com.imooc.reader.service;
 
     import com.baomidou.mybatisplus.core.metadata.IPage;
     import com.imooc.reader.entity.Book;
 
     public interface BookService {
         /**
          * 分页查询图书
          * @param page:查询第几页数据;
          * @param rows:每一页显示多少条数据;
          * @return:IPage:分页对象;
          */
         public IPage<Book> paging(Integer page,Integer rows);
     }
 

说明:

(0) Mybatis-Plus的BaseMapper接口中的selectPage()方法就是分页查询方法,该方法返回的就是IPage分页对象;

(1) IPage是Mybatis-Plus提供的分页对象;IPage分页对象中包含了【当前查询出来的、当前页的、数据】,也包含了一系列与分页相关的信息;比如【总页数】等;

(2) IPage在使用的时候,使用了泛型;这儿的是<Book>,意思是查询出来每一条数据都是一个Book对象;

(3) BookService接口中定义的这个分页查询的方法,其返回值是Mybatis-Plus提供的IPage分页对象


(2)BookServiceImpl实现类;(演示了创建实现类的快捷键方式:Alt+Enter)

这样以后,接口的实现类就得到了:

BookServiceImpl:

     package com.imooc.reader.service.impl;
 
     import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
     import com.baomidou.mybatisplus.core.metadata.IPage;
     import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
     import com.imooc.reader.entity.Book;
     import com.imooc.reader.mapper.BookMapper;
     import com.imooc.reader.service.BookService;
     import org.springframework.stereotype.Service;
     import org.springframework.transaction.annotation.Propagation;
     import org.springframework.transaction.annotation.Transactional;
 
     import javax.annotation.Resource;
 
     @Service("bookService")
     @Transactional(propagation = Propagation.NOT_SUPPORTED,readOnly = true)
     public class BookServiceImpl implements BookService {
 
         @Resource
         private BookMapper bookMapper;
         /**
          * 分页查询图书
          *
          * @param page :查询第几页数据;
          * @param rows :每一页显示多少条数据;
          * @return:IPage:分页对象;
          */
         public IPage<Boo> paging(Integer page, Integer rows) {
             Page<Book> p = new Page<Book>(page,rows);
             QueryWrapper<Book> queryWrapper = new QueryWrapper<Book>();
             IPage<Book> pageObject = bookMapper.selectPage(p, queryWrapper);
             return pageObject;
         }
     }
 

说明:

(1) 类内容说明;

(2) Page类可以认为是IPage接口的实现类;

(插).及时的单元测试;

(1)测试类与测试结果;

【Alt+Insert】快捷键生成测试用例;上次是在【显示图书类别】,这儿就不啰嗦了;

BookServiceImplTest测试类:

     package com.imooc.reader.service.impl;
 
     import com.baomidou.mybatisplus.core.metadata.IPage;
     import com.imooc.reader.entity.Book;
     import com.imooc.reader.service.BookService;
     import org.junit.Test;
     import org.junit.runner.RunWith;
     import org.springframework.test.context.ContextConfiguration;
     import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
     import javax.annotation.Resource;
 
     import java.util.List;
 
     import static org.junit.Assert.*;
 
     @RunWith(SpringJUnit4ClassRunner.class)
     @ContextConfiguration(locations = {"classpath:applicationContext.xml"})
     public class BookServiceImplTest {
 
         @Resource
         private BookService bookService;
         @Test
         public void paging() {
             IPage<Book> pageObject = bookService.paging(2, 10);
             List<Book> records = pageObject.getRecords();//获取当前页的数据;
             for (Book book : records) {
                 System.out.println(book.getBookId()+":"+book.getBookName());
             }
 
             System.out.println("总页数:"+pageObject.getPages());//获取总页数
             System.out.println("总记录数:"+pageObject.getTotal());//获取总记录数
         }
     }

说明:

(1) IPage的三个常用方法,这儿演示了一下;

(2) 测试运行结果:


(2)Mybatis-Plus分页时,底层SQL的执行过程分析;

Preparing: SELECTbook_id,book_name,sub_title,author,cover,description,category_id,evaluation_score,evaluation_quantity
 FROM book LIMIT ?,?

可以看到,其首先根据在继承BaseMapper接口配置泛型时配置的Book实体类,拼接字段;然后,在最后有个LIMIT,第一个?代表本次查询的起始位置,后面的?代表本次查询的数据数;


至此,图书分页查询的Dao和Service部分已经写好了;接下来就是如何将这些图书分页数据,动态的加载到页面中;即,就是Controller和前端部分的内容了;

Ajax动态加载图书信息

说明:

(1) 本篇博客任务:图书分页查询:首页加载时,先默认显示第一页数据:Controller部分;前端开发了一部分;

(2)本篇博客前端内容说明:

● 因为,我们希望,在第一次加载index.ftl这个首页时,首页上就默认加载第一页的图书;所以对于这个业务场景来说,我们把这个ajax请求的<script>写在了<head>中,即写在了页面就绪函数中;

● 本篇博客的一个重点是:前端通过Ajax向后端发起请求,然后服务器返回了一些JSON数据,如果我们想把这些JSON数据显示在页面上:就需要,把这些JSON数据转换为HTML,然后动态的加载到对应的数据容器中;(这是在日常开发中,比较普遍采取的做法)

(3) 有关Ajax的内容,可以参考【附加Ajax总结】这篇汇总博客;


1.分析:【前端页面逻辑】与【后台Controller】开发分析:前端通过【Ajax的方式】来和后端交互比较好;

上面我们已经在Service和Dao部分,实现了图书分页查询;然后Service返回的是IPage分页对象; 接下来的任务就是开发Controller和前端了;


(1) 如果前端不使用Ajax的方式来和后端交互:

● 那么,前端就是采用传统的方式来和后端交互了;这也意味着,【图书信息】是随着【index.ftl这个首页的加载】而同时生成的;此时,每当我们点击【点击加载更多】后,为了显示新加载的图书信息,就需要刷新整个index.ftl首页;

● 由此来看,这种方式不太好;

(2) 如果前端使用Ajax的方式来和后端交互:

● 此时当我们点击【点击加载更多】后,index.ftl这个首页不用刷新,Ajax动态加载是在页面进行局部刷新的;

● 即,前端使用Ajax的方式来和后端交互,更加友好;

● 虽然,为了使用Ajax会比较麻烦一些;(我们需要通过JavaScript,来控制请求的提交,和,返回数据的处理)

(3) 有关Ajax的内容,可以参考【附加Ajax总结】这篇汇总博客;

2.开发Controller:调用Service层的分页查询方法,并向前端返回JSON;

BookController:这儿新增的是selectBook()图书分页查询方法;

     package com.imooc.reader.controller;
 
     import com.baomidou.mybatisplus.core.metadata.IPage;
     import com.imooc.reader.entity.Book;
     import com.imooc.reader.entity.Category;
     import com.imooc.reader.service.BookService;
     import com.imooc.reader.service.CategoryService;
     import org.springframework.stereotype.Controller;
     import org.springframework.web.bind.annotation.GetMapping;
     import org.springframework.web.bind.annotation.ResponseBody;
     import org.springframework.web.servlet.ModelAndView;
 
     import javax.annotation.Resource;
     import java.util.List;
 
     @Controller
     public class BookController {
         @Resource
         private CategoryService categoryService;
         @Resource
         private BookService bookService;
 
         /**
          * 显示首页
          * @return
          */
         @GetMapping("/")
         public ModelAndView showIndex() {
             ModelAndView modelAndView = new ModelAndView("/index");
             List<Category> list = categoryService.selectAll();
             modelAndView.addObject("categoryList", list);
             return modelAndView;
         }
 
         /**
          * 图书分页查询
          * @param p:页码(显示第几页)
          * @return 分页对象
          */
         @GetMapping("books")
         @ResponseBody
         public IPage<Book> selectBook(Integer p) {
             if (p == null) {//容错处理,如果前端没有传页码数据p,那么就默认显示第一页
                 p = 1;
             }
             IPage<Book> pageObject = bookService.paging(p,10);
             return pageObject;
         }
     }
 
 

说明:

(1) 调用service的方法,首先需要注入service对象;

(2) selectBook()方法说明;

(3) 当我们Controller中的方法是直接产生响应文本时(可以通过在方法上使用@ResponseBody注解,或者,在Controller类上使用@RestController注解),因为Spring MVC中配置了jackson,所以其可以自动把【方法返回的实体对象】序列化为【JSON字符串】;即,上面方法返回的IPage对象,其会自动把IPage对象序列化为JSON字符串;

可以参考

●【ModelAndView对象

●【RestController注解与路径变量

●【JSON序列化】;

(4) 此时,重启Tomcat,然后请求selectBook()这个方法;

3.index.ftl前端文件,利用Ajax动态获取后端的JSON数据,组织数据;(只开发了一半)

(1)index.ftl首页文件分析;


(2)利用Ajax与后端交互;

在index.ftl中,使用<script>块,增加一个页面就就绪函数;

<!DOCTYPE html>
<html lang="en"><head>
    <meta charset="UTF-8">
    <title>MK书评网</title>
    <meta name="viewport" content="width=device-width,initial-scale=1.0, maximum-scale=1.0,user-scalable=no">
    <link rel="stylesheet" href="./resources/bootstrap/bootstrap.css">
    <link rel="stylesheet" href="./resources/raty/lib/jquery.raty.css">
    <script src="./resources/jquery.3.3.1.min.js"></script>
    <script src="./resources/bootstrap/bootstrap.min.js"></script>
    <script src="./resources/art-template.js"></script>
 
    <style>
        .highlight {
            color: red !important;
        }
        a:active{
            text-decoration: none!important;
        }
    </style>
 
 
    <style>
        .container {
            padding: 0px;
            margin: 0px;
        }
 
        .row {
            padding: 0px;
            margin: 0px;
        }
 
        .col- * {
            padding: 0px;
        }
    </style>
    <script>
        $(function () {
            $.ajax({
                "url" : "/books",
                "data" : {p:1},
                "type" : "get",
                "dataType" :"json",
                success : function (json) {
                    console.log(json);
                    var list = json.records;
                    for (var i = 0; i < list.length; i++) {
                        var book = json.records[i];
                        var html = "<li>" + book.bookName+ "</li>";
                        $("#bookList").append(html);
                    }
                }
            })
        })
    </script>
 
</head>
<body>
<div class="container">
    <nav class="navbar navbar-light bg-white shadow mr-auto">
        <ul class="nav">
            <li class="nav-item">
                <a href="/">
                    <img src="https://m.imooc.com/static/wap/static/common/img/logo2.png" class="mt-1" style="width: 100px">
                </a>
            </li>
 
        </ul>
        <a href="/login.html" class="btn btn-light btn-sm">
            <img style="width: 2rem;margin-top: -5px" class="mr-1" src="./images/user_icon.png">登录
        </a>
    </nav>
    <div class="row mt-2">
 
 
        <div class="col-8 mt-2">
            <h4>热评好书推荐</h4>
        </div>
 
        <div class="col-8 mt-2">
            <span data-category="-1" style="cursor: pointer" class="highlight  font-weight-bold category">全部</span>
            |
            <#list categoryList as category>
                <a style="cursor: pointer" data-category="${category.categoryId}" class="text-black-50 font-weight-bold category">${category.categoryName}</a>
                <#if category_has_next>
                    |
                </#if>
            </#list>
        </div>
 
        <div class="col-8 mt-2">
            <span data-order="quantity" style="cursor: pointer" class="order highlight  font-weight-bold mr-3">按热度</span>
 
            <span data-order="score" style="cursor: pointer" class="order text-black-50 mr-3 font-weight-bold">按评分</span>
        </div>
    </div>
    <div class="d-none">
        <input type="hidden" id="nextPage" value="2">
        <input type="hidden" id="categoryId" value="-1">
        <input type="hidden" id="order" value="quantity">
    </div>
 
    <div id="bookList">
 
        <a href="/book/5" style="color: inherit">
            <div class="row mt-2 book">
                <div class="col-4 mb-2 pr-2">
                    <img class="img-fluid" src="https://img4.mukewang.com/5ce256ea00014bc903600480.jpg">
                </div>
                <div class="col-8  mb-2 pl-0">
                    <h5 class="text-truncate">从 0 开始学爬虫</h5>
                    <div class="mb-2 bg-light small  p-2 w-100 text-truncate">梁睿坤 · 19年资深架构师</div>
                    <div class="mb-2 w-100">零基础开始到大规模爬虫实战</div>
                    <p>
                        <span class="stars" data-score="4.9" title="gorgeous"><img alt="1" src="./resources/raty/lib/images/star-on.png" title="gorgeous"> <img alt="2" src="./resources/raty/lib/images/star-on.png" title="gorgeous"> <img alt="3" src="./resources/raty/lib/images/star-on.png" title="gorgeous"> <img alt="4" src="./resources/raty/lib/images/star-on.png" title="gorgeous"> <img alt="5" src="./resources/raty/lib/images/star-on.png" title="gorgeous"><input name="score" type="hidden" value="4.9" readonly=""></span>
                        <span class="mt-2 ml-2">4.9</span>
                        <span class="mt-2 ml-2">15人已评</span>
                    </p>
                </div>
            </div>
        </a>
        <hr>
        <a href="/book/25" style="color: inherit">
            <div class="row mt-2 book">
                <div class="col-4 mb-2 pr-2">
                    <img class="img-fluid" src="https://img1.mukewang.com/5da923d60001a92f05400720.jpg">
                </div>
                <div class="col-8  mb-2 pl-0">
                    <h5 class="text-truncate">网络协议那些事儿</h5>
 
                    <div class="mb-2 bg-light small  p-2 w-100 text-truncate">Oscar · 一线大厂高级软件工程师</div>
 
 
                    <div class="mb-2 w-100">前后端通用系列课</div>
 
                    <p>
                        <span class="stars" data-score="4.9" title="gorgeous"><img alt="1" src="./resources/raty/lib/images/star-on.png" title="gorgeous"> <img alt="2" src="./resources/raty/lib/images/star-on.png" title="gorgeous"> <img alt="3" src="./resources/raty/lib/images/star-on.png" title="gorgeous"> <img alt="4" src="./resources/raty/lib/images/star-on.png" title="gorgeous"> <img alt="5" src="./resources/raty/lib/images/star-on.png" title="gorgeous"><input name="score" type="hidden" value="4.9" readonly=""></span>
                        <span class="mt-2 ml-2">4.9</span>
                        <span class="mt-2 ml-2">15人已评</span>
                    </p>
                </div>
            </div>
        </a>
 
        <hr>
    </div>
    <button type="button" id="btnMore" data-next-page="1" class="mb-5 btn btn-outline-primary btn-lg btn-block">
        点击加载更多...
    </button>
    <div id="divNoMore" class="text-center text-black-50 mb-5" style="display: none;">没有其他数据了</div>
</div>
 
</body></html>

说明:

(0) 我们这儿修改或新增的内容为:在<head>中,通过<script>增加了一个页面就绪函数;

(1) 因为我们使用Ajax,而这儿我们是通过jQuery来帮助开发ajax代码;所以,首先需要确认,在index.ftl中引入了jQuery;

(2) 因为,我们希望,在第一次加载index.ftl这个首页时,首页上就默认加载第一页的图书;所以,我们把这个ajax请求的<script>写在了<head>中,即写在了页面就绪函数中;

(3) ajax请求代码分析;

如果,启动Tomcat,通过Localhost这个url去访问首页时,上面编写的页面就绪函数,会去访问localhost/book;我们可以看一下IPage对象的Json数据;

(4) ajax请求代码分析:前端通过Ajax向后端发起请求,然后服务器返回了一些JSON数据,如果我们想把这些JSON数据显示在页面上:就需要,把这些JSON数据转换为HTML,然后动态的加载到对应的数据容器中;(这是在日常开发中,比较普遍采取的做法)

(5) 启动Tomcat,观察效果;

(6) 现存问题:

● 前端通过Ajax向后端发起请求,然后服务器返回了一些JSON数据,如果我们想把这些JSON数据显示在页面上:就需要,把这些JSON数据转换为HTML,然后动态的加载到对应的数据容器中;(这是在日常开发中,比较普遍采取的做法)

● 但是,上面的做法比较笨;如果HTML比较复杂,我们在如上面那样在script中拼接HTML,就会很麻烦;

● 为此,我们引入了一个新的小技术:JS的模板引擎:Art-Template;

● 在下篇博客中,我们会演示利用JS的模板引擎,完成负责页面的构建;

JS模板引擎Art-Template入门

说明:

(1) 本篇博客任务:图书分页查询:首页加载时,先默认显示第一页数据:前端部分剩余内容开发(利用Art-Template技术);

(2) 本篇博客合理性说明:【图书分页查询】中,前端通过Ajax向后端发起请求,然后服务器返回了一些JSON数据,我们想把这些JSON数据显示在页面上】→【就需要,把这些JSON数据转换为HTML,然后动态的加载到对应的数据容器中;(这是在日常开发中,比较普遍采取的做法)】→【但是,如果我们在script中拼接HTML,就会很麻烦】→【因此,我们引入了一个新的小技术:JS的模板引擎:Art-Template】;

(3) 本篇博客内容:

● Art-Template简介;利用Art-Template模板引擎,完成【将JSON数据转成HTML的工作】;

● 引入了星型评分插件raty,并进行了简单使用;


一:Art-Template简介;

1.Art-Template引入;

说明:

(1) Art-Template:基于腾讯开源的JS模板引擎;其是一款高性能的JavaScript模板引擎;

(2) Art-Template主要目的:高效的基于模板,生成复杂的HTML片段;

(3) 如在【上篇博客】中:

● 如果是简单的HTML,我们直接拼接字符串就行了,此时完全用不到Art-Template这把牛刀;

● 但是,如果HTML比较复杂时,可以使用Art-Template来帮助我们;

2.Art-Template官网简述;


二:Art-Template使用演示;(接着开发上篇博客【SSM开发书评网13】的内容)

1.第一步:引入Art-Template引擎的js文件;

2.第二步:定义模板;

在<head>中,定义模板:

    <!--定义模板-->
    <script type="text/html" id="tpl">
        <a href="/book/{{bookId}}" style="color: inherit">
            <div class="row mt-2 book">
                <div class="col-4 mb-2 pr-2">
                    <img class="img-fluid" src="{{cover}}">
                </div>
                <div class="col-8  mb-2 pl-0">
                    <h5 class="text-truncate">{{bookName}}</h5>
                    <div class="mb-2 bg-light small  p-2 w-100 text-truncate">{{author}}</div>
                    <div class="mb-2 w-100">{{subTitle}}</div>
                    <p>
                        <span class="stars" data-score="{{evaluationScore}}" title="gorgeous"><img alt="1" src="./resources/raty/lib/images/star-on.png" title="gorgeous"> <img alt="2" src="./resources/raty/lib/images/star-on.png" title="gorgeous"> <img alt="3" src="./resources/raty/lib/images/star-on.png" title="gorgeous"> <img alt="4" src="./resources/raty/lib/images/star-on.png" title="gorgeous"> <img alt="5" src="./resources/raty/lib/images/star-on.png" title="gorgeous"><input name="score" type="hidden" value="{{evaluationScore}}" readonly=""></span>
                        <span class="mt-2 ml-2">{{evaluationScore}}</span>
                        <span class="mt-2 ml-2">{{evaluationQuantity}}</span>
                    </p>
                </div>
            </div>
        </a>
        <hr>
    </script>

说明:

(1) 首先,在<head>中创建<script>,然后把从<div id=“bookList”中复制的HTML片段,复制进这个<script>块中,然后再做的修改;

(2) 修改前后,内容的变化:

(3) 数据从哪儿来的?:

(4) 模板说明;

3.第三步:使用模板,利用template()方法使其与数据结合,生成新的HTML;

在前端通过ajax接收到后端传过来的JSON数据后,通过template方法,将模板和数据结合,生成新的HTML;然后,我们把HTML追加到对应的容器中,就可以了;

说明:

(1) 上面是使用模板,将数据和模板结合,生成HTML,并将HTML追加到<div id=“bookList这个盛放图书的容器中去;

(2) 那么,此时我们就可以将div id=“bookList中原先的写死的那些HTML给删除了;

(3) 此时的index.ftl:

<!DOCTYPE html>
<html lang="en"><head>
    <meta charset="UTF-8">
    <title>MK书评网</title>
    <meta name="viewport" content="width=device-width,initial-scale=1.0, maximum-scale=1.0,user-scalable=no">
    <link rel="stylesheet" href="./resources/bootstrap/bootstrap.css">
    <link rel="stylesheet" href="./resources/raty/lib/jquery.raty.css">
    <script src="./resources/jquery.3.3.1.min.js"></script>
    <script src="./resources/bootstrap/bootstrap.min.js"></script>
    <script src="./resources/art-template.js"></script>
 
    <style>
        .highlight {
            color: red !important;
        }
        a:active{
            text-decoration: none!important;
        }
    </style>
 
 
    <style>
        .container {
            padding: 0px;
            margin: 0px;
        }
 
        .row {
            padding: 0px;
            margin: 0px;
        }
 
        .col- * {
            padding: 0px;
        }
    </style>
    <!--定义模板-->
    <script type="text/html" id="tpl">
        <a href="/book/{{bookId}}" style="color: inherit">
            <div class="row mt-2 book">
                <div class="col-4 mb-2 pr-2">
                    <img class="img-fluid" src="{{cover}}">
                </div>
                <div class="col-8  mb-2 pl-0">
                    <h5 class="text-truncate">{{bookName}}</h5>
                    <div class="mb-2 bg-light small  p-2 w-100 text-truncate">{{author}}</div>
                    <div class="mb-2 w-100">{{subTitle}}</div>
                    <p>
                        <span class="stars" data-score="{{evaluationScore}}" title="gorgeous"><img alt="1" src="./resources/raty/lib/images/star-on.png" title="gorgeous"> <img alt="2" src="./resources/raty/lib/images/star-on.png" title="gorgeous"> <img alt="3" src="./resources/raty/lib/images/star-on.png" title="gorgeous"> <img alt="4" src="./resources/raty/lib/images/star-on.png" title="gorgeous"> <img alt="5" src="./resources/raty/lib/images/star-on.png" title="gorgeous"><input name="score" type="hidden" value="{{evaluationScore}}" readonly=""></span>
                        <span class="mt-2 ml-2">{{evaluationScore}}</span>
                        <span class="mt-2 ml-2">{{evaluationQuantity}}</span>
                    </p>
                </div>
            </div>
        </a>
        <hr>
    </script>
    <script>
        $(function () {
            $.ajax({
                "url" : "/books",
                "data" : {p:1},
                "type" : "get",
                "dataType" :"json",
                success : function (json) {
                    console.log(json);
                    var list = json.records;
                    for (var i = 0; i < list.length; i++) {
                        var book = json.records[i];
                        // var html = "<li>" + book.bookName+ "</li>";
                        var html = template("tpl" , book);
                        console.log(html);
                        $("#bookList").append(html);
                    }
                }
            })
        })
    </script>
 
</head>
<body>
<div class="container">
    <nav class="navbar navbar-light bg-white shadow mr-auto">
        <ul class="nav">
            <li class="nav-item">
                <a href="/">
                    <img src="https://m.imooc.com/static/wap/static/common/img/logo2.png" class="mt-1" style="width: 100px">
                </a>
            </li>
 
        </ul>
        <a href="/login.html" class="btn btn-light btn-sm">
            <img style="width: 2rem;margin-top: -5px" class="mr-1" src="./images/user_icon.png">登录
        </a>
    </nav>
    <div class="row mt-2">
 
 
        <div class="col-8 mt-2">
            <h4>热评好书推荐</h4>
        </div>
 
        <div class="col-8 mt-2">
            <span data-category="-1" style="cursor: pointer" class="highlight  font-weight-bold category">全部</span>
            |
            <#list categoryList as category>
                <a style="cursor: pointer" data-category="${category.categoryId}" class="text-black-50 font-weight-bold category">${category.categoryName}</a>
                <#if category_has_next>
                    |
                </#if>
            </#list>
        </div>
 
        <div class="col-8 mt-2">
            <span data-order="quantity" style="cursor: pointer" class="order highlight  font-weight-bold mr-3">按热度</span>
 
            <span data-order="score" style="cursor: pointer" class="order text-black-50 mr-3 font-weight-bold">按评分</span>
        </div>
    </div>
    <div class="d-none">
        <input type="hidden" id="nextPage" value="2">
        <input type="hidden" id="categoryId" value="-1">
        <input type="hidden" id="order" value="quantity">
    </div>
 
    <div id="bookList">
 
    </div>
    <button type="button" id="btnMore" data-next-page="1" class="mb-5 btn btn-outline-primary btn-lg btn-block">
        点击加载更多...
    </button>
    <div id="divNoMore" class="text-center text-black-50 mb-5" style="display: none;">没有其他数据了</div>
</div>
 
</body></html>

启动Tomcat,演示,观察结果;


三:上面的程序存在【星星评价不能正常显示】的问题:由此引出:星级评分组件:raty;

1.问题演示;

“星星”的产生方式:星级评分组件raty;我们需要了解raty组件,并在项目中设置,已达到星星随评分改变而改变的目的;

2.星级评分组件:raty简介;

我们这儿选择浏览的是raty的jq22的中文版;自然,raty在GitHub上也有的;

raty是一款基于jQuery的星级评分插件;

然后,raty的使用办法;

3.在项目中,使用raty插件,完成星级评分;

(1)首先,引入raty的css和js;

(2)在页面的head的script块中,设置raty图片的位置;

这儿涉及了script的一些内容,自己有点模糊,因此写了一篇博客,做了一点总结和说明;可以参考下【附加script代码写在head或body中的区别页面就绪函数的作用简述】中的内容;

(3)确定raty星型插件的容器:然后在这个容器中,设置了raty的属性【data-score】;

说明:

(1) 我们把<span中原先静态写死的图片给删了;

(4)把【raty星型插件的容器】初始化为与之对应的星型评价组件;

此时的index.ftl:

<!DOCTYPE html>
<html lang="en"><head>
    <meta charset="UTF-8">
    <title>MK书评网</title>
    <meta name="viewport" content="width=device-width,initial-scale=1.0, maximum-scale=1.0,user-scalable=no">
    <link rel="stylesheet" href="./resources/bootstrap/bootstrap.css">
    <link rel="stylesheet" href="./resources/raty/lib/jquery.raty.css">
    <script src="./resources/jquery.3.3.1.min.js"></script>
    <script src="./resources/bootstrap/bootstrap.min.js"></script>
    <script src="./resources/art-template.js"></script>
    <script src="./resources/raty/lib/jquery.raty.js"></script>
 
    <style>
        .highlight {
            color: red !important;
        }
        a:active{
            text-decoration: none!important;
        }
    </style>
 
 
    <style>
        .container {
            padding: 0px;
            margin: 0px;
        }
 
        .row {
            padding: 0px;
            margin: 0px;
        }
 
        .col- * {
            padding: 0px;
        }
    </style>
    <!--定义模板-->
    <script type="text/html" id="tpl">
        <a href="/book/{{bookId}}" style="color: inherit">
            <div class="row mt-2 book">
                <div class="col-4 mb-2 pr-2">
                    <img class="img-fluid" src="{{cover}}">
                </div>
                <div class="col-8  mb-2 pl-0">
                    <h5 class="text-truncate">{{bookName}}</h5>
                    <div class="mb-2 bg-light small  p-2 w-100 text-truncate">{{author}}</div>
                    <div class="mb-2 w-100">{{subTitle}}</div>
                    <p>
                        <span class="stars" data-score="{{evaluationScore}}" title="gorgeous"></span>
                        <span class="mt-2 ml-2">{{evaluationScore}}</span>
                        <span class="mt-2 ml-2">{{evaluationQuantity}}</span>
                    </p>
                </div>
            </div>
        </a>
        <hr>
    </script>
    <script>
        $.fn.raty.defaults.path = "./resources/raty/lib/images"
        $(function () {
            $.ajax({
                "url" : "/books",
                "data" : {p:1},
                "type" : "get",
                "dataType" :"json",
                success : function (json) {
                    console.log(json);
                    var list = json.records;
                    for (var i = 0; i < list.length; i++) {
                        var book = json.records[i];
                        // var html = "<li>" + book.bookName+ "</li>";
                        var html = template("tpl" , book);
                        console.log(html);
                        $("#bookList").append(html);
                    }
                    $(".stars").raty({readOnly:true});
                }
            })
        })
    </script>
 
</head>
<body>
<div class="container">
    <nav class="navbar navbar-light bg-white shadow mr-auto">
        <ul class="nav">
            <li class="nav-item">
                <a href="/">
                    <img src="https://m.imooc.com/static/wap/static/common/img/logo2.png" class="mt-1" style="width: 100px">
                </a>
            </li>
 
        </ul>
        <a href="/login.html" class="btn btn-light btn-sm">
            <img style="width: 2rem;margin-top: -5px" class="mr-1" src="./images/user_icon.png">登录
        </a>
    </nav>
    <div class="row mt-2">
 
 
        <div class="col-8 mt-2">
            <h4>热评好书推荐</h4>
        </div>
 
        <div class="col-8 mt-2">
            <span data-category="-1" style="cursor: pointer" class="highlight  font-weight-bold category">全部</span>
            |
            <#list categoryList as category>
                <a style="cursor: pointer" data-category="${category.categoryId}" class="text-black-50 font-weight-bold category">${category.categoryName}</a>
                <#if category_has_next>
                    |
                </#if>
            </#list>
        </div>
 
        <div class="col-8 mt-2">
            <span data-order="quantity" style="cursor: pointer" class="order highlight  font-weight-bold mr-3">按热度</span>
 
            <span data-order="score" style="cursor: pointer" class="order text-black-50 mr-3 font-weight-bold">按评分</span>
        </div>
    </div>
    <div class="d-none">
        <input type="hidden" id="nextPage" value="2">
        <input type="hidden" id="categoryId" value="-1">
        <input type="hidden" id="order" value="quantity">
    </div>
 
    <div id="bookList">
 
    </div>
    <button type="button" id="btnMore" data-next-page="1" class="mb-5 btn btn-outline-primary btn-lg btn-block">
        点击加载更多...
    </button>
    <div id="divNoMore" class="text-center text-black-50 mb-5" style="display: none;">没有其他数据了</div>
</div>
 
</body></html>

(5)效果,启动Tomcat,观察效果;


至此,【加载首页index.ftl时,就默认加载第一页数据】就开发完了;接下来的任务就是点击【点击加载更多】按钮后,会加载后续的分页内容了;

点击加载更多按钮开发

说明:

(1) 本篇博客任务:图书分页查询:每次点击【点击加载更多】,就查询下一页的数据;如果所有数据全部查完,就显示【没有其他数据了】;

(2) 本篇博客内容:

● 本篇博客的内容,其主要知识点依旧是Mybatis-Plus的分页查询;只是前几篇博客是分页查询的第一页,本篇博客是分页查询的第二、三……页;

● 本篇博客还介绍了【通过页面就绪函数,捕捉按钮的单击事件】,【隐藏域这个小tips的使用】,【代码的及时重构思想】等;(虽然,这些内容,在以前都遇到过……)


一:【点击加载更多……】开发;

1.正式开发;

index.ftl:

<!DOCTYPE html>
<html lang="en"><head>
    <meta charset="UTF-8">
    <title>MK书评网</title>
    <meta name="viewport" content="width=device-width,initial-scale=1.0, maximum-scale=1.0,user-scalable=no">
    <link rel="stylesheet" href="./resources/bootstrap/bootstrap.css">
    <link rel="stylesheet" href="./resources/raty/lib/jquery.raty.css">
    <script src="./resources/jquery.3.3.1.min.js"></script>
    <script src="./resources/bootstrap/bootstrap.min.js"></script>
    <script src="./resources/art-template.js"></script>
    <script src="./resources/raty/lib/jquery.raty.js"></script>
 
    <style>
        .highlight {
            color: red !important;
        }
        a:active{
            text-decoration: none!important;
        }
    </style>
 
 
    <style>
        .container {
            padding: 0px;
            margin: 0px;
        }
 
        .row {
            padding: 0px;
            margin: 0px;
        }
 
        .col- * {
            padding: 0px;
        }
    </style>
    <!--定义模板-->
    <script type="text/html" id="tpl">
        <a href="/book/{{bookId}}" style="color: inherit">
            <div class="row mt-2 book">
                <div class="col-4 mb-2 pr-2">
                    <img class="img-fluid" src="{{cover}}">
                </div>
                <div class="col-8  mb-2 pl-0">
                    <h5 class="text-truncate">{{bookName}}</h5>
                    <div class="mb-2 bg-light small  p-2 w-100 text-truncate">{{author}}</div>
                    <div class="mb-2 w-100">{{subTitle}}</div>
                    <p>
                        <span class="stars" data-score="{{evaluationScore}}" title="gorgeous"></span>
                        <span class="mt-2 ml-2">{{evaluationScore}}</span>
                        <span class="mt-2 ml-2">{{evaluationQuantity}}</span>
                    </p>
                </div>
            </div>
        </a>
        <hr>
    </script>
    <script>
        $.fn.raty.defaults.path = "./resources/raty/lib/images"
        //这个页面就绪函数,用于首页加载时,默认显示第一页数据;
        $(function () {
            $.ajax({
                "url" : "/books",
                "data" : {p:1},
                "type" : "get",
                "dataType" :"json",
                success : function (json) {
                    console.log(json);
                    var list = json.records;
                    for (var i = 0; i < list.length; i++) {
                        var book = json.records[i];
                        // var html = "<li>" + book.bookName+ "</li>";
                        var html = template("tpl" , book);
                        console.log(html);
                        $("#bookList").append(html);
                    }
                    $(".stars").raty({readOnly:true});//显示星型评价组件
                }
            })
        })
 
        //这个页面就绪函数,用于绑定【点击加载更多】按钮的点击事件
        $(function () {
            $("#btnMore").click(function () {  //click()中增加一个匿名函数,用于事件处理
                var nextPage = $("#nextPage").val();//首先获取下一页页号
                $.ajax({
                    "url" : "/books",
                    "data" : {p:nextPage},
                    "type" : "get",
                    "dataType" :"json",
                    success : function (json) {
                        console.log(json);
                        var list = json.records;
                        for (var i = 0; i < list.length; i++) {
                            var book = json.records[i];
                            // var html = "<li>" + book.bookName+ "</li>";
                            var html = template("tpl" , book);
                            console.log(html);
                            $("#bookList").append(html);
                        }
                        $(".stars").raty({readOnly:true});//显示星型评价组件
 
                        if (json.current < json.pages) { //如果当前页小于总页数,说明还有数据:
                            $("#nextPage").val(parseInt(json.current) + 1);//把下一页的隐藏域的值,增加1
                            $("#btnMore").show();//【点击显示更多按钮】按钮应该继续显示;
                            $("#divNoMore").hide();//【没有其他数据了】不应该显示;
                        } else {   //否则,如果当前页不小于总页数,说明已经是最后一页了:
                            $("#btnMore").hide();//【点击显示更多按钮】按钮不应该继续显示了;
                            $("#divNoMore").show();//【没有其他数据了】应该显示;
                        }
                    }
                })
            })
        })
    </script>
 
</head>
<body>
<div class="container">
    <nav class="navbar navbar-light bg-white shadow mr-auto">
        <ul class="nav">
            <li class="nav-item">
                <a href="/">
                    <img src="https://m.imooc.com/static/wap/static/common/img/logo2.png" class="mt-1" style="width: 100px">
                </a>
            </li>
 
        </ul>
        <a href="/login.html" class="btn btn-light btn-sm">
            <img style="width: 2rem;margin-top: -5px" class="mr-1" src="./images/user_icon.png">登录
        </a>
    </nav>
    <div class="row mt-2">
 
 
        <div class="col-8 mt-2">
            <h4>热评好书推荐</h4>
        </div>
 
        <div class="col-8 mt-2">
            <span data-category="-1" style="cursor: pointer" class="highlight  font-weight-bold category">全部</span>
            |
            <#list categoryList as category>
                <a style="cursor: pointer" data-category="${category.categoryId}" class="text-black-50 font-weight-bold category">${category.categoryName}</a>
                <#if category_has_next>
                    |
                </#if>
            </#list>
        </div>
 
        <div class="col-8 mt-2">
            <span data-order="quantity" style="cursor: pointer" class="order highlight  font-weight-bold mr-3">按热度</span>
 
            <span data-order="score" style="cursor: pointer" class="order text-black-50 mr-3 font-weight-bold">按评分</span>
        </div>
    </div>
    <div class="d-none">
        <input type="hidden" id="nextPage" value="2">
        <input type="hidden" id="categoryId" value="-1">
        <input type="hidden" id="order" value="quantity">
    </div>
 
    <div id="bookList">
        <!--这个div,是显示图书的容器-->
    </div>
    <button type="button" id="btnMore" data-next-page="1" class="mb-5 btn btn-outline-primary btn-lg btn-block">
        点击加载更多...
    </button>
    <div id="divNoMore" class="text-center text-black-50 mb-5" style="display: none;">没有其他数据了</div>
</div>
 
</body></html>

说明:

(0) 我们这儿就是编写了一个页面就绪函数,来捕捉“btnMore”这个按钮的单击事件;

(1) 代码逻辑说明:主要内容就是:给【点击加载更多……】绑定单击事件;编写单击事件;然后就是积累下这种解决业务的编程套路;

(2) 现存的一些小问题:

为此,我们可以对代码进行重构和梳理;(对代码及时进行重构和梳理,是个不错的习惯)

2.重构梳理代码;

重构后的index.ftl:

<!DOCTYPE html>
<html lang="en"><head>
    <meta charset="UTF-8">
    <title>MK书评网</title>
    <meta name="viewport" content="width=device-width,initial-scale=1.0, maximum-scale=1.0,user-scalable=no">
    <link rel="stylesheet" href="./resources/bootstrap/bootstrap.css">
    <link rel="stylesheet" href="./resources/raty/lib/jquery.raty.css">
    <script src="./resources/jquery.3.3.1.min.js"></script>
    <script src="./resources/bootstrap/bootstrap.min.js"></script>
    <script src="./resources/art-template.js"></script>
    <script src="./resources/raty/lib/jquery.raty.js"></script>
 
    <style>
        .highlight {
            color: red !important;
        }
        a:active{
            text-decoration: none!important;
        }
    </style>
 
 
    <style>
        .container {
            padding: 0px;
            margin: 0px;
        }
 
        .row {
            padding: 0px;
            margin: 0px;
        }
 
        .col- * {
            padding: 0px;
        }
    </style>
    <!--定义模板-->
    <script type="text/html" id="tpl">
        <a href="/book/{{bookId}}" style="color: inherit">
            <div class="row mt-2 book">
                <div class="col-4 mb-2 pr-2">
                    <img class="img-fluid" src="{{cover}}">
                </div>
                <div class="col-8  mb-2 pl-0">
                    <h5 class="text-truncate">{{bookName}}</h5>
                    <div class="mb-2 bg-light small  p-2 w-100 text-truncate">{{author}}</div>
                    <div class="mb-2 w-100">{{subTitle}}</div>
                    <p>
                        <span class="stars" data-score="{{evaluationScore}}" title="gorgeous"></span>
                        <span class="mt-2 ml-2">{{evaluationScore}}</span>
                        <span class="mt-2 ml-2">{{evaluationQuantity}}</span>
                    </p>
                </div>
            </div>
        </a>
        <hr>
    </script>
    <script>
        $.fn.raty.defaults.path = "./resources/raty/lib/images"
 
        //loadMore():加载更多数据;
        //isReset参数设置为true时:代表查询第一个数据;否则按nextPage查询后续页;
        function loadMore(isReset) {
            if (isReset == true) {
                $("#nextPage").val(1);
            }
            var nextPage = $("#nextPage").val();//首先获取下一页页号
            $.ajax({
                "url" : "/books",
                "data" : {p:nextPage},
                "type" : "get",
                "dataType" :"json",
                success : function (json) {
                    console.log(json);
                    var list = json.records;
                    for (var i = 0; i < list.length; i++) {
                        var book = json.records[i];
                        // var html = "<li>" + book.bookName+ "</li>";
                        var html = template("tpl" , book);
                        console.log(html);
                        $("#bookList").append(html);
                    }
                    $(".stars").raty({readOnly:true});//显示星型评价组件
 
                    if (json.current < json.pages) { //如果当前页小于总页数,说明还有数据:
                        $("#nextPage").val(parseInt(json.current) + 1);//把下一页的隐藏域的值,增加1
                        $("#btnMore").show();//【点击显示更多按钮】按钮应该继续显示;
                        $("#divNoMore").hide();//【没有其他数据了】不应该显示;
                    } else {   //否则,如果当前页不小于总页数,说明已经是最后一页了:
                        $("#btnMore").hide();//【点击显示更多按钮】按钮不应该继续显示了;
                        $("#divNoMore").show();//【没有其他数据了】应该显示;
                    }
                }
            })
        }
        //这个页面就绪函数,用于首页加载时,默认显示第一页数据;
        $(function () {
            loadMore(true);
            // $.ajax({
            //     "url" : "/books",
            //     "data" : {p:1},
            //     "type" : "get",
            //     "dataType" :"json",
            //     success : function (json) {
            //         console.log(json);
            //         var list = json.records;
            //         for (var i = 0; i < list.length; i++) {
            //             var book = json.records[i];
            //             // var html = "<li>" + book.bookName+ "</li>";
            //             var html = template("tpl" , book);
            //             console.log(html);
            //             $("#bookList").append(html);
            //         }
            //         $(".stars").raty({readOnly:true});//显示星型评价组件
            //     }
            // })
        })
 
        //这个页面就绪函数,用于绑定【点击加载更多】按钮的点击事件
        $(function () {
            $("#btnMore").click(function () {  //click()中增加一个匿名函数,用于事件处理
                loadMore();
            })
        })
    </script>
 
</head>
<body>
<div class="container">
    <nav class="navbar navbar-light bg-white shadow mr-auto">
        <ul class="nav">
            <li class="nav-item">
                <a href="/">
                    <img src="https://m.imooc.com/static/wap/static/common/img/logo2.png" class="mt-1" style="width: 100px">
                </a>
            </li>
 
        </ul>
        <a href="/login.html" class="btn btn-light btn-sm">
            <img style="width: 2rem;margin-top: -5px" class="mr-1" src="./images/user_icon.png">登录
        </a>
    </nav>
    <div class="row mt-2">
 
 
        <div class="col-8 mt-2">
            <h4>热评好书推荐</h4>
        </div>
 
        <div class="col-8 mt-2">
            <span data-category="-1" style="cursor: pointer" class="highlight  font-weight-bold category">全部</span>
            |
            <#list categoryList as category>
                <a style="cursor: pointer" data-category="${category.categoryId}" class="text-black-50 font-weight-bold category">${category.categoryName}</a>
                <#if category_has_next>
                    |
                </#if>
            </#list>
        </div>
 
        <div class="col-8 mt-2">
            <span data-order="quantity" style="cursor: pointer" class="order highlight  font-weight-bold mr-3">按热度</span>
 
            <span data-order="score" style="cursor: pointer" class="order text-black-50 mr-3 font-weight-bold">按评分</span>
        </div>
    </div>
    <div class="d-none">
        <input type="hidden" id="nextPage" value="2">
        <input type="hidden" id="categoryId" value="-1">
        <input type="hidden" id="order" value="quantity">
    </div>
 
    <div id="bookList">
        <!--这个div,是显示图书的容器-->
    </div>
    <button type="button" id="btnMore" data-next-page="1" class="mb-5 btn btn-outline-primary btn-lg btn-block">
        点击加载更多...
    </button>
    <div id="divNoMore" class="text-center text-black-50 mb-5" style="display: none;">没有其他数据了</div>
</div>
 
</body></html>

说明:

(1) 重构说明;

(2) 启动Tomcat后,实测没问题;


至此默认首页大部分功能已经完成了,但是还有两个地方需要开发:【按类别筛选】和【按热度或按评分进行排序】;这两个部分内容,在下篇博客中介绍;

类别、热度、评分查询

说明:

(1) 本篇博客任务:【按类别筛选】和【按热度或按评分进行排序】;

(2) 本篇博客内容:

● 用到了QueryWrapper查询构造器的orderByDesc(“字段名”)的倒序排序的查询条件设置;

● 在前端中,灵活使用“隐藏域”进行存值等操作,可以帮助开发;

● 我们在条件判断,或者前后端传值的时候:增加非空的判断,能够提高程序的健壮性;

本篇博客的,按条件动态查询的套路,还是非常值得参考的;

其实,这儿按类别查询,其本质还是基于前面几篇博客的分页查询,只是增加了图书类别和按热度|按评分这些条件;


一:【图书类别超链接】和【按热度|按评分超链接】高亮效果切换,前端设置;

当我们点击【图书类别超链接】和【按热度|按评分超链接】后,我们应该把本次点击的超连接设置为高亮,同时其他超链接设置为灰色;这是前端显示的问题,所以我们只需要设置一下前端就可以了;

1.高亮效果说明;

对于这些高亮标签的切换,需要通过JavaScript来实现;

2.高亮效果实现;

可以看到其SOP是:先移除全部的高亮效果,然后设置基础颜色,然后再把本次点击的超链接设置为高亮;

启动Tomcat,观察其效果,结果是OK的;


二:【图书类别】和【按热度|按评分】开发;

我们点击了【图书类别超链接】和【按热度|按评分超链接】,也就意味着我们选择了某个查询条件,那么我们就要根据这个查询条件去后端查询数据;

1.Service层开发:设置“图书类别”和“按热度|按评分”的条件查询;

(1)首先,在BookService接口的paging()方法中,增加【图书分类编号】和【排序方式】两个参数;

BookService接口:

package com.imooc.reader.service;
 
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.imooc.reader.entity.Book;
 
public interface BookService {
    /**
     * 分页查询图书
     * @param categoryId:分类编号;
     * @param order:排序方式
     * @param page:查询第几页数据;
     * @param rows:每一页显示多少条数据;
     * @return:IPage:分页对象;
     */
    public IPage<Book> paging(Long categoryId,String order,Integer page,Integer rows);
 
}

说明:

(1) 修改的内容:


(2)然后,去修改BookService接口的实现类,BookServiceImpl类;我们需要添加参数,并根据新增的参数,添加查询条件;

BookServiceImpl类:

package com.imooc.reader.service.impl;
 
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.imooc.reader.entity.Book;
import com.imooc.reader.mapper.BookMapper;
import com.imooc.reader.service.BookService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
 
import javax.annotation.Resource;
 
@Service("bookService")
@Transactional(propagation = Propagation.NOT_SUPPORTED,readOnly = true)
public class BookServiceImpl implements BookService {
 
    @Resource
    private BookMapper bookMapper;
    /**
     * 分页查询图书
     * @param categoryId:分类编号;
     * @param order:排序方式
     * @param page :查询第几页数据;
     * @param rows :每一页显示多少条数据;
     * @return:IPage:分页对象;
     */
    public IPage<Book> paging(Long categoryId,String order,Integer page, Integer rows) {
        Page<Book> p = new Page<Book>(page,rows);
        QueryWrapper<Book> queryWrapper = new QueryWrapper<Book>();
        //如果,我们在前台传入了有效的分类编号(即我们点击了“前端”、“后端”、“测试”、“产品”这些超链接)
        if (categoryId != null && categoryId != -1) {
            queryWrapper.eq("category_id", categoryId);
        }
        //如果,我们在前台点击了“按热度”或者“按评分”的超链接;
        if (order != null) {
            if (order.equals("quantity")) {//如果我们点击的是“按热度”;
                //那么我们就按这个评价人数字段"evaluation_quantity",进行降序排列;
                queryWrapper.orderByDesc("evaluation_quantity");
            } else if (order.equals("score")) {//如果我们点击的是“按评分”;
                //那么我们就按这个评价分数字段"evaluation_quantity",进行降序排列;
                queryWrapper.orderByDesc("evaluation_score");
            }
        }
        IPage<Book> pageObject = bookMapper.selectPage(p, queryWrapper);
        return pageObject;
    }
}

说明:

(1) 修改的内容:

(2) “图书类别”的一点说明;

(3) “按热度|按评分”的一点说明;

(4) 其中用到了QueryWrapper查询构造器的orderByDesc(“字段名”)的倒序排序的查询条件设置;


(3)然后,去修改BookServiceImpl类的测试类,看些Service层开发的是否OK;

说明:

(1) 测试结果:

2.Controller层开发:设置“图书类别”和“按热度|按评分”的条件查询;

BookController类:

     package com.imooc.reader.controller;
 
     import com.baomidou.mybatisplus.core.metadata.IPage;
     import com.imooc.reader.entity.Book;
     import com.imooc.reader.entity.Category;
     import com.imooc.reader.service.BookService;
     import com.imooc.reader.service.CategoryService;
     import org.springframework.stereotype.Controller;
     import org.springframework.web.bind.annotation.GetMapping;
     import org.springframework.web.bind.annotation.ResponseBody;
     import org.springframework.web.servlet.ModelAndView;
 
     import javax.annotation.Resource;
     import java.util.List;
 
     @Controller
     public class BookController {
         @Resource
         private CategoryService categoryService;
         @Resource
         private BookService bookService;
 
         /**
          * 显示首页图书类别
          * @return
          */
         @GetMapping("/")
         public ModelAndView showIndex() {
             ModelAndView modelAndView = new ModelAndView("/index");
             List<Category> list = categoryService.selectAll();
             modelAndView.addObject("categoryList", list);
             return modelAndView;
         }
 
         /**
          * 图书分页查询
          * @param categoryId:分类编号;
          * @param order:排序方式
          * @param p:页码(显示第几页)
          * @return 分页对象
          */
         @GetMapping("books")
         @ResponseBody
         public IPage<Book> selectBook(Long categoryId,String order,Integer
 p) {
             if (p == null) {//容错处理,如果前端没有传页码数据p,那么就默认显示第一页
                 p = 1;
             }
             IPage<Book> pageObject = bookService.paging(categoryId,order,p,10);
             return pageObject;
         }
     }
 
 

说明:

(1) 方法增加“图书类别”和“按热度|按评分”参数;而这些参数,是需要前台界面传过来的;

3.前端开发:根据前端界面点击的不同,给后端发送不同的参数;

index.ftl:

<!DOCTYPE html>
<html lang="en"><head>
    <meta charset="UTF-8">
    <title>MK书评网</title>
    <meta name="viewport" content="width=device-width,initial-scale=1.0, maximum-scale=1.0,user-scalable=no">
    <link rel="stylesheet" href="./resources/bootstrap/bootstrap.css">
    <link rel="stylesheet" href="./resources/raty/lib/jquery.raty.css">
    <script src="./resources/jquery.3.3.1.min.js"></script>
    <script src="./resources/bootstrap/bootstrap.min.js"></script>
    <script src="./resources/art-template.js"></script>
    <script src="./resources/raty/lib/jquery.raty.js"></script>
 
    <style>
        .highlight {
            color: red !important;
        }
        a:active{
            text-decoration: none!important;
        }
    </style>
 
 
    <style>
        .container {
            padding: 0px;
            margin: 0px;
        }
 
        .row {
            padding: 0px;
            margin: 0px;
        }
 
        .col- * {
            padding: 0px;
        }
    </style>
    <!--定义模板-->
    <script type="text/html" id="tpl">
        <a href="/book/{{bookId}}" style="color: inherit">
            <div class="row mt-2 book">
                <div class="col-4 mb-2 pr-2">
                    <img class="img-fluid" src="{{cover}}">
                </div>
                <div class="col-8  mb-2 pl-0">
                    <h5 class="text-truncate">{{bookName}}</h5>
                    <div class="mb-2 bg-light small  p-2 w-100 text-truncate">{{author}}</div>
                    <div class="mb-2 w-100">{{subTitle}}</div>
                    <p>
                        <span class="stars" data-score="{{evaluationScore}}" title="gorgeous"></span>
                        <span class="mt-2 ml-2">{{evaluationScore}}</span>
                        <span class="mt-2 ml-2">{{evaluationQuantity}}</span>
                    </p>
                </div>
            </div>
        </a>
        <hr>
    </script>
    <script>
        $.fn.raty.defaults.path = "./resources/raty/lib/images"
 
        //loadMore():加载更多数据;
        //isReset参数设置为true时:代表查询第一个数据;否则按nextPage查询后续页;
        function loadMore(isReset) {
            if (isReset == true) {
                $("#bookList").html("");
                $("#nextPage").val(1);
            }
            var nextPage = $("#nextPage").val();//首先获取下一页页号
            var categoryId = $("#categoryId").val();//获取保存在隐藏域中的,图书分类编号;
            var order = $("#order").val();//获取保存在隐藏域中的,按热度|按评分的排序规则;
            $.ajax({
                "url" : "/books",
                "data" : {p:nextPage,categoryId:categoryId,order:order},
                "type" : "get",
                "dataType" :"json",
                success : function (json) {
                    console.log(json);
                    var list = json.records;
                    for (var i = 0; i < list.length; i++) {
                        var book = json.records[i];
                        // var html = "<li>" + book.bookName+ "</li>";
                        var html = template("tpl" , book);
                        console.log(html);
                        $("#bookList").append(html);
                    }
                    $(".stars").raty({readOnly:true});//显示星型评价组件
 
                    if (json.current < json.pages) { //如果当前页小于总页数,说明还有数据:
                        $("#nextPage").val(parseInt(json.current) + 1);//把下一页的隐藏域的值,增加1
                        $("#btnMore").show();//【点击显示更多按钮】按钮应该继续显示;
                        $("#divNoMore").hide();//【没有其他数据了】不应该显示;
                    } else {   //否则,如果当前页不小于总页数,说明已经是最后一页了:
                        $("#btnMore").hide();//【点击显示更多按钮】按钮不应该继续显示了;
                        $("#divNoMore").show();//【没有其他数据了】应该显示;
                    }
                }
            })
        }
        //这个页面就绪函数,用于首页加载时,默认显示第一页数据;
        $(function () {
            loadMore(true);
            // $.ajax({
            //     "url" : "/books",
            //     "data" : {p:1},
            //     "type" : "get",
            //     "dataType" :"json",
            //     success : function (json) {
            //         console.log(json);
            //         var list = json.records;
            //         for (var i = 0; i < list.length; i++) {
            //             var book = json.records[i];
            //             // var html = "<li>" + book.bookName+ "</li>";
            //             var html = template("tpl" , book);
            //             console.log(html);
            //             $("#bookList").append(html);
            //         }
            //         $(".stars").raty({readOnly:true});//显示星型评价组件
            //     }
            // })
        })
 
        //这个页面就绪函数,用于绑定【点击加载更多】按钮的点击事件
        $(function () {
            //这个函数,用于绑定【点击加载更多】按钮的点击事件
            $("#btnMore").click(function () {  //click()中增加一个匿名函数,用于事件处理
                loadMore();
            })
 
            //这个函数,用于设置分类超链接,高亮与否的设置
            $(".category").click(function () {
                $(".category").removeClass("highlight");//先移除所有的category分类超链接上的“highlight”高亮css;
                $(".category").addClass("text-black-50")//然后,将所有的category分类标签分类超链接的颜色,设置为灰色;
                $(this).addClass("highlight");//捕获当前点击的分类超链接,并添加上“highlight”高亮css;
                var categoryId = $(this).data("category");
                $("#categoryId").val(categoryId);
                loadMore(true);//每次点击图书类别后,则全部重新查询
            })
            //这个函数,用于设置按热度|按评分,高亮与否的设置
            $(".order").click(function () {
                $(".order").removeClass("highlight");//先移除所有的category分类超链接上的“highlight”高亮css;
                $(".order").addClass("text-black-50")//然后,将所有的category分类标签分类超链接的颜色,设置为灰色;
                $(this).addClass("highlight");//捕获当前点击的分类超链接,并添加上“highlight”高亮css;
                var order = $(this).data("order");
                $("#order").val(order);
                loadMore(true);//每次点击按热度|按评分后,则全部重新查询
            })
        })
    </script>
 
</head>
<body>
<div class="container">
    <nav class="navbar navbar-light bg-white shadow mr-auto">
        <ul class="nav">
            <li class="nav-item">
                <a href="/">
                    <img src="https://m.imooc.com/static/wap/static/common/img/logo2.png" class="mt-1" style="width: 100px">
                </a>
            </li>
 
        </ul>
        <a href="/login.html" class="btn btn-light btn-sm">
            <img style="width: 2rem;margin-top: -5px" class="mr-1" src="./images/user_icon.png">登录
        </a>
    </nav>
    <div class="row mt-2">
 
 
        <div class="col-8 mt-2">
            <h4>热评好书推荐</h4>
        </div>
 
        <div class="col-8 mt-2">
            <span data-category="-1" style="cursor: pointer" class="highlight  font-weight-bold category">全部</span>
            |
            <#list categoryList as category>
                <a style="cursor: pointer" data-category="${category.categoryId}" class="text-black-50 font-weight-bold category">${category.categoryName}</a>
                <#if category_has_next>
                    |
                </#if>
            </#list>
        </div>
 
        <div class="col-8 mt-2">
            <span data-order="quantity" style="cursor: pointer" class="order highlight  font-weight-bold mr-3">按热度</span>
 
            <span data-order="score" style="cursor: pointer" class="order text-black-50 mr-3 font-weight-bold">按评分</span>
        </div>
    </div>
    <div class="d-none">
        <input type="hidden" id="nextPage" value="2">
        <input type="hidden" id="categoryId" value="-1">
        <input type="hidden" id="order" value="quantity">
    </div>
 
    <div id="bookList">
        <!--这个div,是显示图书的容器-->
    </div>
    <button type="button" id="btnMore" data-next-page="1" class="mb-5 btn btn-outline-primary btn-lg btn-block">
        点击加载更多...
    </button>
    <div id="divNoMore" class="text-center text-black-50 mb-5" style="display: none;">没有其他数据了</div>
</div>
 
</body></html>

说明:

(1.1) 因为我们在调用后端查询方法时,需要传递“图书类别”和“按热度|按评分”参数,所以(还是前面遇到的开发策略)我们埋了两个隐藏域,用来保存这两个参数;

(1.2) 然后,当我们点击了界面上的图书类别或者按热度|按评分后,我们需要及时的调整对应隐藏域的值;

可以看到,默认情况下,两个隐藏域的数值分别是-1和quantity;当我们点击图书分类和按热度|按评分后,隐藏域的数值也会相应的发生变化;

(2) 然后,我们把隐藏域的“图书类别”和“按热度|按评分”数据,随着ajax请求发送给后端;

(3) 然后,当我们点击“图书类别”和“按热度|按评分”超链接的时候,应该去调用loadMore()方法;并且参数设置为了true,表示一切按照新的条件,重头开始查询;( PS:这儿其实更多是对于这种业务场景的,一种在业务层面上的解决方案;以后,自己开发遇到这种场景时,可以考虑采用这种解决方案

(5) 然后,既然我们当点击“图书类别”和“按热度|按评分”超链接的时候,一切按照新的条件,重头开始查询;又因为我们知道div id=“bookList”容器是盛放图书信息的,而且我们采用的是向这个容器中追加的方式;所以,如果我们是一切重头开始查询的话,我们应该先清空这个容器;( PS:这儿其实更多是对于这种业务场景的,一种在业务层面上的解决方案;以后,自己开发遇到这种场景时,可以考虑采用这种解决方案

(6) 启动Tomcat,观察效果,发现其是OK的;

(7) 这儿的代码和编程套路,还是比较重要的;


至此,【图书列表分页及查询】部分就开发完了,接下来就是开发【图书详情页】了;

图书详情页开发之读取图书信息

说明:

(1) 本篇博客任务:在首页,点击某个图书后,进入该图书的详情页;然后,本篇博客只完成了上面的图书简介;(下面的短评,在下篇博客介绍)

(2) 本篇博客需要注意的点:

● 开发完Service部分后,养成及时测试的好习惯;

● 我们在使用资源时,最好使用绝对路径,而不要使用相对路径;(自然不排除,个别场景需要使用相对路径)


零:前置说明和分析;

1.把detail.ftl详情页的前端文件,引入;


2.主要任务分析;

通过上面分析可以看出,这儿主要的任务是:【当点击首页的某本书,会后端发起“/book/该图书的id”】→【为此,后端就要编写一个,按照图书的id,去查book图书表,获取图书对象的功能逻辑】;


一:正式开发;

1.开发实体类,与底层数据表对应;

图书详情的信息存储在book表中;

2.开发【Mapper接口】;

3.开发mapper接口,对应的【xml】;

4.开发service,调用Dao层代码,完成图书详情的查询;

(1)BookService接口,定义一个【根据图书编号,查询图书对象】的方法;

在BookService接口中,增加一个【根据图书编号,查询图书对象】的方法;

package com.imooc.reader.service;
 
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.imooc.reader.entity.Book;
 
public interface BookService {
    /**
     * 分页查询图书
     * @param categoryId:分类编号;
     * @param order:排序方式
     * @param page:查询第几页数据;
     * @param rows:每一页显示多少条数据;
     * @return:IPage:分页对象;
     */
    public IPage<Book> paging(Long categoryId,String order,Integer page,Integer rows);
 
    /**
     * 根据图书编号,查询图书对象;
     * @param bookId 图书编号;
     * @return 图书对象;
     */
    public Book selectById(Long bookId);
 
}

(2)BookServiceImpl实现类,去实现 【根据图书编号,查询图书对象】方法;

在BookServiceImpl实现类中,我们要去实现 【根据图书编号,查询图书对象】方法;

5.开发Controller,调用Service层的代码;渲染数据,跳转到detail.ftl详情页;

在BookController类中,新增显示详情页的方法:showDedail()方法;

说明:

(1) showDetail()方法和前端的对应关系;

(2) 其中,用到了路径变量,Spring MVC的路径变量,是在【获取路径变量@PathVariable】中第一次介绍和说明的;如有需要,可以快速参考;

(3) 此时的效果:启动Tomcat,观察效果;

然后,放开断点:

(4) 解决(3)中css,js等资源不存在的问题:建议使用绝对路径,而不是相对路径;

问题分析:

解决策略:建议使用绝对路径;

Tomcat重新加载后,效果如下:

此时,只是完成了跳转到detail.ftl页面的功能,detail.ftl页面里面的数据还是写死的,我们需要去detail.ftl中去动态获取后端添加到响应中的数据;

6.detail.ftl前端文件,获取数据;

设置的地方如下:

此时的效果:启动Tomcat,观察效果:数据填充的没问题;


此时图书详情页的有关【图书详情信息】的图书介绍部分已经OK了,但是图书详情页,还有与短评相关的部分,后面我们需要开发这部分内容;

注:图片无法显示请关闭防盗链: 在head下添加代码

 
<meta name="referrer" content="no-referrer" />
 

图书详情页开发之显示评论列表

说明:

(1) 本篇博客任务:在首页,点击某个图书后,进入该图书的详情页;然后,本篇博客要完成的是,显示下面的短评部分;

(2) 本篇博客:

● 一个和 【JS模板引擎Art-Template入门】对比时,业务逻辑和处理方式的对比和分析;

一个新知识,或者说是一个骚操作Mybatis-Plus的【@TableField(exist = false)所谓关联查询时,给对象附加对象的策略:这可以让如Evaluation对象去承载evaluation表中没有对应字段的属性;


零:前置分析和说明;

1.图书的评论信息在evaluation表中;


一:正式开发;

1.开发实体类,与底层数据表对应;

因为我们要操作evaluation表,所以需要创建一个实体类以应对该表;

Evaluation实体类:

 
     package com.imooc.reader.entity;
 
     import com.baomidou.mybatisplus.annotation.IdType;
     import com.baomidou.mybatisplus.annotation.TableId;
     import com.baomidou.mybatisplus.annotation.TableName;
 
     import java.util.Date;
 
     @TableName("evaluation")
     public class Evaluation {
         @TableId(type = IdType.AUTO)
         private Long evaluationId;
         private Long bookId;
         private String content;
         private Integer score;
         private Long memberId;
         private Date createTime;
         private Integer enjoy;
         private String state;
         private String disableReason;
         private Date disableTime;
 
         public Long getEvaluationId() {
             return evaluationId;
         }
 
         public void setEvaluationId(Long evaluationId) {
             this.evaluationId = evaluationId;
         }
 
         public Long getBookId() {
             return bookId;
         }
 
         public void setBookId(Long bookId) {
             this.bookId = bookId;
         }
 
         public String getContent() {
             return content;
         }
 
         public void setContent(String content) {
             this.content = content;
         }
 
         public Integer getScore() {
             return score;
         }
 
         public void setScore(Integer score) {
             this.score = score;
         }
 
         public Long getMemberId() {
             return memberId;
         }
 
         public void setMemberId(Long memberId) {
             this.memberId = memberId;
         }
 
         public Date getCreateTime() {
             return createTime;
         }
 
         public void setCreateTime(Date createTime) {
             this.createTime = createTime;
         }
 
         public Integer getEnjoy() {
             return enjoy;
         }
 
         public void setEnjoy(Integer enjoy) {
             this.enjoy = enjoy;
         }
 
         public String getState() {
             return state;
         }
 
         public void setState(String state) {
             this.state = state;
         }
 
         public String getDisableReason() {
             return disableReason;
         }
 
         public void setDisableReason(String disableReason) {
             this.disableReason = disableReason;
         }
 
         public Date getDisableTime() {
             return disableTime;
         }
 
         public void setDisableTime(Date disableTime) {
             this.disableTime = disableTime;
         }
     }
 

说明:

(1) 这个没什么好说的,前面介绍过好多次了,保证属性和evaluation表的表字段名一致,别写错就行了;

(2) Date类型的属性,其类型是【java.util.Date】,别弄错了;

2.开发【Mapper接口】;

EvaluationMapper接口:

     package com.imooc.reader.mapper;
 
     import com.baomidou.mybatisplus.core.mapper.BaseMapper;
     import com.imooc.reader.entity.Evaluation;
 
     public interface EvaluationMapper extends BaseMapper<Evaluation>{
     }
 

说明:

(1) 千万别忘记书写泛型;

3.开发mapper接口,对应的【xml】;

evaluation.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.imooc.reader.mapper.EvaluationMapper">
</mapper>

说明:

(1) 设置好对应关系就行了;

4.开发service,调用Dao层代码,完成图书短评的查询工作;

(1)EvaluationService接口;

在EvaluationService接口中,定义一个根据图书编号,查询图书短评的方法;

 
     package com.imooc.reader.service;
 
     import com.imooc.reader.entity.Evaluation;
 
     import java.util.List;
 
     public interface EvaluationService {
         /**
          * 按图书编号查询图书的有效短评
          * @param bookId 图书编号
          * @return 短评的List
          */
         public List<Evaluation> selectByBookId(Long bookId);
 
     }
 

(2)EvaluationServiceImpl实现类;

 
     package com.imooc.reader.service.impl;
 
     import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
     import com.imooc.reader.entity.Evaluation;
     import com.imooc.reader.mapper.EvaluationMapper;
     import com.imooc.reader.service.EvaluationService;
     import org.springframework.stereotype.Service;
     import org.springframework.transaction.annotation.Propagation;
     import org.springframework.transaction.annotation.Transactional;
 
     import javax.annotation.Resource;
     import java.util.List;
 
     @Service("evaluationService")
     @Transactional(propagation = Propagation.NOT_SUPPORTED,readOnly = true)
     public class EvaluationServiceImpl implements EvaluationService {
         @Resource
         EvaluationMapper evaluationMapper;
 
         /**
          * 按图书编号查询图书的有效短评
          *
          * @param bookId 图书编号
          * @return 短评的List
          */
         public List<Evaluation> selectByBookId(Long bookId) {
             QueryWrapper<Evaluation> queryWrapper = new QueryWrapper<Evaluation>();
             queryWrapper.eq("bookId", bookId);
             queryWrapper.eq("state", "enable");
             queryWrapper.orderByDesc("create_time");
 
             List<Evaluation> evaluationList =
 evaluationMapper.selectList(queryWrapper);
             return evaluationList;
         }
     }

说明:

(1) 前面说过,【Alt+Enter】可以快速生成接口的实现类;

(2) 其中用到的东西,在前面多次遇到过,这儿再啰嗦一下;

5.开发Controller,调用Service层的代码;渲染数据,跳转到detail.ftl详情页;

6.1.detail.ftl前端文件,获取短评数据:其实是,获取短评数据的大部分内容;(这儿前端在获取数据的时候,有一个关于业务场景的分析和比对,比较重要!!!)

这些写死的<div,都一样,只是里面写死的信息不一样,我们删的只剩一个;

说明:

(1) 我们修改的内容:

其中,FreeMarker设置日期格式可以快速参考【FreeMarker基本语法-取值】;

(2)一个疑问:(比较重要)

问题: 在【JS模板引擎Art-Template入门】中,我们开发首页时,首页的图书信息,我们是这样做的:【前提,我们首先设置一个<div>容器,把这个容器作为盛放图书信息的容器】→【首先,当后端的图书数据过来后,这些数据是List】→【遍历这些List,每获取一条图书数据,就通过Art-Template定义模板,把这条数据渲成HTML片段】→【然后,把这端HTML,追加到那个容器中】;:为什么在前面显示图书信息这篇博客中,我们这样做;而,在这儿,我们显示短评信息时,我们直接<#list>遍历显示就行了?

个人理解:

首先,在前面显示图书信息的那篇博客中,首页上我们每次点击【点击加载更多…】按钮,就在首页上多显示一点数据;即显示图书信息时,图书信息不是一次就显示完的,,所以,我们定义了一个Art-Template模板,每次新图书数据来的时候,就通过这个模板生成一段HTML,然后把这段HTML追加到显示图书的容器中;

然后,本篇博客的显示短评信息,就是一锤子买卖,一次就能把某本图书的所有短评信息全部加载过来;自然,为了解决这个需求,完全不需要定义Art-Template模板,只要<#list>遍历数据,显示就行了;

总结: 通过,这个不同业务情况下的不同的处理策略;应该能够帮助自己提升业务开发和业务决策能力;

6.2.detail.ftl前端文件,获取短评数据:其实是,获取短评数据的会员昵称;(遇到了 Mybatis-Plus的【@TableField(exist = false),这可以让如Evaluation对象去承载evaluation表中没有对应字段的属性;:新知识,比较重要!!!

(1)需求说明;

【如何在获取评论数据时,一起获取该条评论对应的会员昵称信息】,是这儿要解决的问题;

这种类似的场景,似乎在前面介绍Mybatis对象关联查询时,遇到过类似的需求场景;但是,看下 【MyBatis多表级联查询】这篇博客后,就能快速了解Mybatis的关联对象查询到底是什么;

然后,Mybatis的对象关联查询,和这儿使用Mybatis-Plus的解决策略,是不同的;


(2)开发查询member会员表的Dao层;

因为会员信息存储在member表中,所以我们要创建操作member表的一套内容,如下:

至此,我们有了可以获取到member表信息的,Dao层的内容;


(3)利用Mybatis-Plus的【@TableField(exist = false),在Evaluation中添加Member属性

这是关键一步:(先别纠结为什么这么做,先看着;;;一会就能明白)


(4) 在EvaluationServiceImpl中,调用member的Dao层代码,以给Evaluation的Member属性赋值;

然后,我们就能在EvaluationServiceImpl类中,分别调用【操作book表的Dao】和【操作member表的Dao】,以获取【Book对象】和【Member对象】,然后把这两个对象在赋值为Evaluation对象;

这样以后,每一条短评Evaluation对象,其Book属性和Member属性就有了正确的值,我们在detail.ftl中就能获取到该条短评的对应的会员的昵称了;


(5)在detail.ftl中,就可以获取Member信息,即获取会员昵称了;

(下图很容易理解,我就不啰嗦了)


(6)此时的效果;

启动Tomcat,观察效果:OK没问题;


至此,我们就完成了显示某本图书短评的功能;不过这儿仅仅是显示短评;至于给短评点赞、写短评这些内容在后面介绍;

而且想要点赞短评或写短评,需要用户登录;所以,后面会先介绍会员的注册和登录功能;