富文本编辑器wangEditor使用入门

说明:

(1) 为什么要介绍wangEditor?:在【项目演示】中,我们演示了书评网;其中,就包括后台管理系统;而后台管理系统,最重要的就是图书资料的管理;然后,在图书资料管理时,如何对图书信息进行有效的图文编辑是重点;为此,引入了一个JavaScript组件:富文本编辑器wangEditor;

(2)声明: 本篇博客仅仅介绍了wangEditor的最基本使用;不涉及任何与项目业务相关的内容;


一:wangEditor官网简介;

wangEditor组件是国产的,所以阅读起来,没有语言障碍;


二:wangEditor最基本使用:演示;

1.把wangEditor的js文件,添加进项目;

2.前置说明:创建test.ftl和TestController;

我们创建了test.ftl,我们就在test.ftl中编写wangEditor;然后,又创建了TestController;这样以后,当我们启动项目后,通过访问【localhost/test/t1】的时候,就能访问到test.ftl,看到wangEditor的效果了;

3.在test.ftl中编写wangEditor:包括【wangEditor初始化】,【如何读取内容】,【如何写入内容】;(重点,核心!)

test.ftl:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <!-- 引入wangEditor -->
    <script src="/resources/wangEditor.min.js"></script>
</head>
<body>
    <button id="btnRead">读取内容</button>
    <button id="btnWrite">写入方法</button>
    <div id="divEditor" style="width: 800px;height: 600px" ></div>
    <script>
        var E = window.wangEditor;
        var editor = new E("#divEditor");//完成富文本编辑器的初始化
        editor.create();//创建富文本编辑器,显示在页面上
 
        document.getElementById("btnRead").onclick = function () {
            var content = editor.txt.html();//获取编辑器中,现有的HTML内容;
            alert(content);
        };
 
        document.getElementById("btnWrite").onclick = function () {
            var content = "<li style='color: red'>向富文本编辑器中添加的内容,<b>然后这儿是加粗的内容</b></li>";
            editor.txt.html(content);
        };
    </script>
</body>
</html>

说明:

(1) 在当前前端文件中,引入wangEditor的js文件;

(2) wangEditor初始化:

这样有以后,就能在页面上显示出wangEditor富文本编辑器了;但是,此时wangEditor只是富文本编辑工具;为了能够让wangEditor在项目中实用,我们需要【读取wangEditor的内容】和【向wangEditor中添加内容】;

(3) 读取wangEditor中的内容;

html()方法,没有参数时,就是获取元素的html纯文本;(这儿可以参考下【jQuery获取、设置表单】,虽然这儿我们使用的不是jQuery,但是能够看到,这些东西都是相通的;)

(4) 设置wangEditor中的内容;

html()方法,有参数时,就是向元素中写入html纯文本;即,通过上面的内容,可以发现,wangEditor在设计的时候,借鉴了很多jQuery用法;

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

可以看到,wangEditor初始化、写入内容、读取内容,均没问题;


至此,wangEditor的最基本使用就OK了;有了这个基础后,就可以取开发后台的图书管理功能了;

实现wangEditor图片上传

说明:

(1) 本篇博客内容说明:【在后台系统,我们点击新增按钮后,会弹出新增图书对话框】→【该对话框中,包含一个wangEditor富文本编辑器】→【wangEditor富文本编辑器中,可以包含图片】→【我们点击对话框中的“点击提交”按钮后,会把当前图书的信息给提价到服务器】→【不过,本篇博客,只关心如何把wangEditor中的图片上传到服务器;暂不关心图书信息的其他内容】→【完整图书信息提交,的后端逻辑,将在下篇博客中介绍】;

(2) 本篇博客讲到Spring MVC的文件上传:

● Spring MVC在【后台系统四:【新增】功能;(FileUpload组件)】中介绍的Apache Commons FileUpload组件基础上,做了封装;

● 类似的【有关,表单提交格式、或,上传文件的内容】,以前遇到过两次:在【Kaptcha验证码功能应用到注册模块】中通过【data:$(“#frmLogin”).serialize()】,把表单的数据给序列化了;serialize()方法是JQuery中的ajax方法,其作用是编码表单元素集为字符串以便提交;然后,自己想起了【后台系统四:【新增】功能;(FileUpload组件)】这篇博客;这篇博客主要内容是【提交的表单的时候,涉及到了文件上传】,然后我们通过【enctype=“multipart/form-data”】设置表单的编码方式,以便能够上传文件;


一:本篇博客,开发内容说明;

在后台管理系统,当我们点击【添加】按钮后,会出现一个【包含wangEditor富文本编辑器的,弹出表单】;当我们点击【立即提交】后,会把图书的信息提交到服务器;

其中,因为wangEditor是富文本编辑器,其中的内容可能是图文混排内容;所以,如何把其中的图片上传到服务器上,是难点;

因此,我们要针对wangEditor,开发与之对应的【图片上传功能】;


二:前置准备:引入后台首页book.ftl;创建访问入口;观察效果;

1.引入后台管理系统的首页:book.ftl;

book.ftl:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>图书管理功能</title>
    <style>
        #dlgBook{
            padding: 10px
        }
    </style>
    <link rel="stylesheet" href="/resources/layui/css/layui.css">
 
    <script src="/resources/wangEditor.min.js"></script>
 
 
    <script type="text/html" id="toolbar">
        <div class="layui-btn-container">
            <button class="layui-btn layui-btn-sm" id="btnAdd" onclick="showCreate()">添加</button>
        </div>
    </script>
 
</head>
<body>
 
 
<div class="layui-container">
    <blockquote class="layui-elem-quote">图书列表</blockquote>
	<!-- 数据表格 -->
    <table id="grdBook" lay-filter="grdBook"></table>
</div>
<!--表单内容-->
<div id="dialog" style="padding: 10px;display: none">
    <form class="layui-form" >
        <div class="layui-form-item">
			<!-- 图书类别 -->
            <select id="categoryId" name="categoryId" lay-verify="required" lay-filter=
			"categoryId">
                <option value=""></option>
                <option value="1">前端</option>
				<option value="2">后端</option>
				<option value="3">测试</option>
				<option value="4">产品</option>
            </select>
 
        </div>
        <div class="layui-form-item">
			<!-- 书名 -->
            <input type="text" id="bookName" name="bookName" required lay-verify="required" placeholder="请输入书名"
                   autocomplete="off" class="layui-input">
        </div>
 
 
        <div class="layui-form-item">
			<!-- 子标题 -->
            <input type="text" id="subTitle" name="subTitle" required lay-verify="required" placeholder="请输入子标题"
                   autocomplete="off" class="layui-input">
        </div>
 
        <div class="layui-form-item">
			<!-- 作者 -->
            <input type="text" id="author" name="author" required lay-verify="required" placeholder="请输入作者信息"
                   autocomplete="off" class="layui-input">
        </div>
 
        <div style="margin-top: 30px;font-size: 130%">图书介绍(默认第一图将作为图书封面)</div>
        <div class="layui-form-item" >
			<!-- wangEditor编辑器 -->
            <div id="editor" style="width: 100%">
 
            </div>
        </div>
		<!-- 图书编号 -->
        <input id="bookId" type="hidden">
		<!-- 当前表单操作类型,create代表新增 update代表修改 -->
        <input id="optype"  type="hidden">
        <div class="layui-form-item" style="text-align: center">
			<!-- 提交按钮 -->
            <button class="layui-btn" lay-submit="" lay-filter="btnSubmit">立即提交</button>
        </div>
    </form>
</div>
<script src="/resources/layui/layui.all.js"></script>
<script>
 
    var table = layui.table; //table数据表格对象
    var $ = layui.$; //jQuery
    var editor = null; //wangEditor富文本编辑器对象
    //初始化图书列表
    table.render({
        elem: '#grdBook'  //指定div
        , id : "bookList" //数据表格id
        , toolbar: "#toolbar" //指定工具栏,包含新增添加
        , url: "/management/book/list" //数据接口
        , page: true //开启分页
        , cols: [[ //表头
            {field: 'bookName', title: '书名', width: '300'}
            , {field: 'subTitle', title: '子标题', width: '200'}
            , {field: 'author', title: '作者', width: '200'}
            , {type: 'space', title: '操作', width: '200' , templet : function(d){
					//为每一行表格数据生成"修改"与"删除"按钮,并附加data-id属性代表图书编号
                    return "<button class='layui-btn layui-btn-sm btn-update'  data-id='" + d.bookId + "' data-type='update' onclick='showUpdate(this)'>修改</button>" +
                        "<button class='layui-btn layui-btn-sm btn-delete'  data-id='" + d.bookId + "'   onclick='showDelete(this)'>删除</button>";
                }
            }
        ]]
    });
	//显示更新图书对话框
	//obj对应点击的"修改"按钮对象
    function showUpdate(obj){
		//弹出"编辑图书"对话框
        layui.layer.open({
            id: "dlgBook", //指定div
            title: "编辑图书", //标题
            type: 1,
            content: $('#dialog').html(), //设置对话框内容,复制自dialog DIV
            area: ['820px', '730px'], //设置对话框宽度高度
            resize: false //是否允许调整尺寸
        })
 
        var bookId = $(obj).data("id"); //获取"修改"按钮附带的图书编号
        $("#dlgBook #bookId").val(bookId); //为表单隐藏域赋值,提交表单时用到
 
        editor = new wangEditor('#dlgBook #editor'); //初始化富文本编辑器
        editor.customConfig.uploadImgServer = '/management/book/upload' //设置图片上传路径
        editor.customConfig.uploadFileName = 'img'; //图片上传时的参数名
        editor.create(); //创建wangEditor
        $("#dlgBook #optype").val("update"); //设置当前表单提交时提交至"update"更新地址
            layui.form.render();
		//发送ajax请求,获取对应图书信息
        $.get("/management/book/id/" + bookId , {} , function(json){
			//文本框回填已有数据
            $("#dlgBook #bookName").val(json.data.bookName);//书名
            $("#dlgBook #subTitle").val(json.data.subTitle); //子标题
            $("#dlgBook #author").val(json.data.author);//作者
            $("#dlgBook #categoryId").val(json.data.categoryId); //分类选项
            editor.txt.html(json.data.description); //设置图文内容
            layui.form.render();//重新渲染LayUI表单
        } , "json")
 
 
 
    }
	//显示新增图书对话框
    function showCreate(){
		//弹出"新增图书"对话框
        layui.layer.open({
            id: "dlgBook",
            title: "新增图书",
            type: 1,
            content: $('#dialog').html(),
            area: ['820px', '730px'],
            resize: false
        })
		//初始化wangEditor
        editor = new wangEditor('#dlgBook #editor');
        editor.customConfig.uploadImgServer = '/management/book/upload';//设置图片上传地址
        editor.customConfig.uploadFileName = 'img';//设置图片上传参数
        editor.create();//创建wangEditor
 
        layui.form.render(); //LayUI表单重新
        $("#dlgBook #optype").val("create");//设置当前表单提交时提交至"create"新增地址
 
    };
 
	//对话框表单提交
    layui.form.on('submit(btnSubmit)', function(data){
		//获取表单数据
        var formData = data.field;
 
		//判断是否包含至少一副图片,默认第一图作为封面显示
        var description = editor.txt.html();
        if(description.indexOf("img") == -1){
            layui.layer.msg('请放置一副图片作为封面');
            return false;
        }
		//获取当前表单要提交的地址
		//如果是新增数据则提交至create
		//如果是更新数据则提交至update
        var optype = $("#dlgBook #optype").val();
 
        if(optype == "update"){
			//更新数据时,提交时需要附加图书编号
            formData.bookId=$("#dlgBook #bookId").val();
        }
		//附加图书详细描述的图文html
        formData.description = description;
		//向服务器发送请求
        $.post("/management/book/" + optype , formData , function(json){
            if(json.code=="0"){
				//处理成功,关闭对话框,刷新列表,提示操作成功
                layui.layer.closeAll();
                table.reload('bookList');
                layui.layer.msg('数据操作成功,图书列表已刷新');
            }else{
				//处理失败,提示错误信息
                layui.layer.msg(json.msg);
            }
        } ,"json")
        return false;
    });
	//删除图书
    function showDelete(obj){
		//获取当前点击的删除按钮中包含的图书编号
        var bookId = $(obj).data("id");
		//利用layui的询问对话框进行确认
        layui.layer.confirm('确定要执行删除操作吗?', {icon: 3, title:'提示'}, function(index){
 
				//确认按钮后发送ajax请求,包含图书编号
				$.get("/management/book/delete/" + bookId, {}, function (json) {
					if(json.code=="0"){
						//删除成功刷新表格
						table.reload('bookList');
						//提示操作成功
						layui.layer.msg('数据操作成功,图书列表已刷新');
						//关闭对话框
						layui.layer.close(index);
					}else{
						//处理失败,提示错误信息
						layui.layer.msg(json.msg);
					}
				}, "json");
 
        });
 
    }
 
</script>
</body>
</html>

说明:

(1) 这个book.ftl,和前面的【绘制后台首页UI布局】类似,都是使用LayUI的;

(2) 这个前端文件中,很多前端的功能都已经编写了; 我们与之对应的编写后端的功能即可;

(3) book.ftl,就是后台系统的图书管理首页;在book.ftl这一个界面上,我们可以完成对图书信息的增删改查操作;

2.创建MBookController类,编写访问book.ftl的入口;

MBookController:

 
     package com.imooc.reader.controller.management;
 
     import org.springframework.stereotype.Controller;
     import org.springframework.web.bind.annotation.GetMapping;
     import org.springframework.web.bind.annotation.RequestMapping;
     import org.springframework.web.servlet.ModelAndView;
 
     @Controller
     @RequestMapping("/management/book")
     public class MBookController {
         @GetMapping("/index.html")
         public ModelAndView showBook() {
             return new ModelAndView("/management/book");
         }
     }
 

说明:

(1) MBookController类说明:

3.启动Tomcat,访问后台的图书管理首页,去看下book.ftl;

当我们点击【添加】按钮后,会弹出【新增图书的对话框】;


三:wangEditor官方文档,对【上传图片】的文档说明;


四:正式开发wangEditor的【上传图片】功能;

1.后台管理系统的首页:book.ftl分析;

(1)book.ftl静态页面部分;

(2)book.ftl动态部分;

通过分析book.ftl,可知wangEditor图片文件上传地址为:【/management/book/upload】;即,我们应该在后端编写一个处理wangEditor图片上传的接口,而且该接口的url需要是【/management/book/upload】;

那么,我们在后端就要开发相应的接口;

2.在Spring MVC项目中,配置引入【Common FileUpload组件】,并配置:以使得Spring MVC具备文件上传的能力;

Spring MVC底层文件上传,是依赖于Apache提供的Common FileUpload组价;这部分,需要可以参考 【后台系统四:【新增】功能;(FileUpload组件)】中的内容;

(1)在pom.xml中,引入Common FileUpload组价的依赖;

 
 <!--Common FileUpload组价依赖-->
<dependency>
    <groupId>commons-fileupload</groupId>
   <artifactId>commons-fileupload</artifactId>
   <version>1.4</version>
</dependency>
 

老生常谈:引入新依赖后,如有需要,别忘了,把这个依赖添加到发布中去;


(2)在applicationContext.xml中配置,激活Spring MVC的文件上传处理功能;

 
         <!--激活Spring MVC的文件上传(FileUpload)处理功能-->
         <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
             <property name="defaultEncoding" value="UTF-8"/>
         </bean>

其中设置“defaultEncoding”属性为UTF-8,意思是:在表单提交时,表单内容按UTF-8编码;这可以解决中文乱码问题;

结合【后台系统四:【新增】功能;(FileUpload组件)】,能够感受到,Spring MVC在前面基础内容的基础上,封装了文件上传功能;


至此,我们在Spring MVC项目中,就引入了FileUpload组件;然后,又因为Spring MVC基于Upload组件等原先的基础知识,作了进一步的封装;

所以,接下来,我们就可以在项目中,去进行文件上传了;

3.在MBookController类中,增加upload()方法,实现wangEditor中图片的上传;

 
     @PostMapping("/upload")
         @ResponseBody
         public Map upload(@RequestParam("img") MultipartFile file, HttpServletRequest request) throws IOException {
             //得到图片文件上传目录
             String uploadPath = request.getServletContext().getResource("/").getPath() + "/upload/";
             //得到文件名,为了防止文件重名,以当前的毫秒作为文件名
             String fileName = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date());
             //获取原始文件的扩展名
             String suffix = file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf("."));
             file.transferTo(new File(uploadPath + fileName + suffix));
 
             Map result = new HashMap();
             result.put("errno", 0);
             result.put("data", new String[]{"/upload/" + fileName + suffix});
             return result;
         }

说明:

(1) 【upload()方法的url】需要和【前端设置的wangEditor的图片提交地址】保持一致;

(2) 因为wangEditor,需要后端返回JSON字符串,以说明服务器是否处理成功;所以,upload()的返回值我们定成了Map;

(3) 使用MultipartFile接收wangEditor传过来的文件,而且需要设置一些@RequestParam;

(4) 在获取图片上传的地址时,需要用到HttpServletRequest原生对象,在参数中写上它;

(5) 拼凑图片文件上传地址,并将文件保存到指定的目录下;

transferTo()方法会抛异常,我们继续向上抛就行;

(6) 由于wangEditor上传图片时,当我们后端处理完了之后,需要通知wangEditor后台处理成功了;为此,我们需要按照wangEditor的要求,返回结果;

(7) 在【后台系统四:【新增】功能;(FileUpload组件)】中,我们要想上传文件,我们把前端<form表单的enctype属性,设置为了”multipart/form-data”;与这儿对比一下,能够感受到,Spring MVC在这些久知识上,做了封装;

4.启动Tomcat,观察效果;

而且,查看网页源代码,可以看到图片地址:

自然,在后端的相应地址上,是有这个图片文件的:

说明:

(1) 在啰嗦一下,上面的来龙去脉:


自此,新增图书中,比较复杂的wangEditor上传图片就OK了;下篇博客将要完全实现图书的新增功能;

完成图书的新增功能

说明:

(1) 本篇博客内容说明:

(2) 本博客的几个点:

● java中一款不错的HTML解析器:jsoup;

● 其实,目前来看,只要【和前端对接好接口,设置好url,明确了来回的数据格式】,单纯的开发后端的CRUD,其实工作量不大; 由此能够感受到,作为主栈是后端的开发者来说,CRUD比较简单;真正复杂和牛批的是:【底层数据库优化】、【并发】、【分布式】、【算法】、【引擎】、【设计模式】、【中间件】、【目前成熟框架的源码】、【计算机组成原理】、【网络协议】、【大数据】、【系统架构优化】等;

● 通过本篇博客的案例,也多少能明白,图文混排的内容,在后端是如何处理的、在数据库中是如何存储的;


一:前置分析

然后,需要注意,我们要求wangEditor最少需要传入一张图片;而且,其中的第一张图会默认作为该图书的封面;


之所以是【localhost/management/book/create】这个地址:下面给出说明:


表单提交的数据分析:

● 当我新增图书时,evaluation_score和evaluation_quantity都是0,这个很容易理解;

● 我们要求wangEditor最少需要传入一张图片;而且,其中的第一张图会默认作为该图书的封面;而wangEditor中的内容就是description,其实一段HTML片段;所以,description这个HTML片段中的,第一个img就是cover,这个需要我们在后端处理一下;为了能更好的解决这个需求,引入了java中一款不错的HTML解析器:jsoup;


二:后台接口开发;

1.在BookService接口中,定义一个新增图书的方法:createBook()方法;

2.在BookServiceImpl中,去实现createBook()方法;

附:jsoup简介,在工程中引入jsoup;

访问jsoup的官网:【https://jsoup.org/】;

在pom.xml中,引入jsoup依赖;

<!--java的HTML解析器:jsoup;-->
<dependency>
    <groupId>org.jsoup</groupId>
    <artifactId>jsoup</artifactId>
    <version>1.12.1</version>
</dependency>
 

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

3.在MBookController类中,创建新增图书的方法:createBook()方法;

 
     /**
          * 新增图书
          * @param book
          * @return
          */
         @PostMapping("/create")
         @ResponseBody
         public Map createBook(Book book) {
             Map result = new HashMap();
             try {
                 book.setEvaluationQuantity(0);
                 book.setEvaluationScore(0f);
                 //获取图书的description属性值,并解析
                 Document document = Jsoup.parse(book.getDescription());
                 //获取图书description属性中的,第一个图片的元素对象
                 Element img = document.select("img").first();
                 //获取改图片元素的,src属性值
                 String cover = img.attr("src");
                 //给book对象的cover属性,赋值
                 book.setCover(cover);
 
                 bookService.createBook(book);
                 result.put("code", 0);
                 result.put("msg", "success");
             } catch (BussinessException ex) {
                 ex.printStackTrace();
                 result.put("code", ex.getCode());
                 result.put("msg", ex.getMsg());
             }
             return result;
         }

说明:

(1) url要对的上;

(2) 前端表单的输入项,就是针对Book来的,而且其命名符合Book属性;所以,后端可以用Book实体类去接收;

(3) 然后,就是补全Book的属性:其中用到了java中一款不错的HTML解析器:jsoup;

(4) 然后,就是调用BookService中定义的createBook方法;并根据前端的要求返回对应的code和mag信息;

(5) 还有一点就是,BookService的createBook()方法没有抛BussinessException异常,但为什么Controller这儿要捕获这个异常呐?:这一点前面说过,BookService的createBook()方法现在没抛BussinessException异常,不代表以后新业务需要来的时候不抛;;;所以,Controller这儿算是提前准备了一手;

4.运行效果;


继续执行:可以看到Book的主键ID也回填了:(背后,是Mybatis-Plus已经帮我们做了;)


放开断点,让其继续执行:底层数据库也没问题;


然后,在前台系统中,也能看到我们新增的那本图书;


至此,图书的新增功能就OK了;下篇博客要做的就是,在后台系统首页上,展示图书的数据表格;

后台图书列表显示

说明:

(1) 本篇博客内容说明:基于分页查询的策略,查询book表,并将查询结果显示在前端的table表格上;

(2) 本篇博客的分页查询:

● 后端,使用的是在【SSM整合Mybatis-Plus】中配置了Mybatis-Plus的分页插件;在【SSM整合Mybatis-Plus】中,第一次演示了Mybatis-Plus分页插件的使用;

● 前端,使用的是LayUI表格;关于layui表格的内容,第一次遇到是在【包括$(““)[0].reset();layui表格的设置,layui中弹出框附带数据的技巧等】;但不幸的是,layui已经停止维护了;目前主流开发中,前端也基本不使用LayUI;所以,本篇博客中的layui内容,没必要深究;能做到理解了就行;

(3) 本篇博客其实不难,还好,只要按照标准的SOP开发就行了;


一:前置说明;

1.layui的声明;

在 【包括$(““)[0].reset();layui表格的设置,layui中弹出框附带数据的技巧等)】我们详细介绍过LayUI表格的内容;(只是,当时在介绍layui表格的时候,没有开启分页;)

但是,layui项目已经停止维护;同时,在实际开发中,也很少使用layui,所以对于layui的内容没必要深究;同时,自己作为一个主栈是后端的开发者来说,目前没有必要深究前端的内容;而且,前端文件book.ftl,已经帮我们该准备的都准备好了;

所以,在这儿针对layui表格,只需要大致明白以下几点就够了:

(1) 首先,添加一个基础组件:table;(这个table后续会被转化为数据表格)

(2) 然后,初始化数据表格(对(1)中的table进行处理):此处主要是,初始化表格,设置表的列和数据的对应关系;

可以看到,其中的page属性我们设置为了true,即开启了属性;

通过访问前台book.ftl,也可以看到,layui表格发起了一个ajax请求:因为我们layui开启了分页,所以后面跟了两个参数【page】和【limit】;

上面的url对应的后端接口,还没有开发,我们接下来需要去开发;

2.后端的(基于Mybatis-Plus)的分页方法;

前面在开发前台系统的时候,我们在BookService中已经定义了一个分页方法;

而且,在BookServiceImpl中实现的时候,categoryId,order也是可以为null的;即paging()方法普适性很强;(由此其对参数的处理方式,可以看到,开发前台系统的时候,就预料到后台的需求了……这得需要开发经验和功力~~):

 
         /**
          * 分页查询图书
          * @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;
         }
 

Mybatis-Plus的分页查询对象IPage很给力;

在【走了一遍Mybatis-Plus的流程】中,第一次演示了Mybatis-Plus分页插件的使用;如有需要,可以去参考;


二:正式开发;

1.在MBookController中,开发接收前台layui表格ajax请求的分页处理方法:list()方法;

 
         @GetMapping("/list")
         @ResponseBody
         public Map list(Integer page, Integer limit) {
             if (page == null) {
                 page = 1;
             }
             if (limit == null) {
                 limit = 10;
             }
             IPage<Book> pageObject = bookService.paging(null, null, page, limit);
             Map result = new HashMap();
             result.put("code", 0);
             result.put("msg", "success");
             result.put("data", pageObject.getRecords());
             result.put("count", pageObject.getTotal());
             return result;
         }

说明:

(1) url要对上;

(2) 接收前台参数时,参数要对应上;

(3) 为了增加程序的健壮性,我们增加了对page和limit参数的判空处理;即,万一前台传过来的这俩数为null时,我们就给其赋默认值;

(4) n以为BookServiceImpl中的paging()方法的categoryId和order参数可以为空;而且,我们后台的这儿,也完全没有也不需要这个两个参数,所以我们在调用pagin()方法的时候,这个两个参数都设为了null;;(这也体现了paging()方法的通用性)

(5) 通过在【(包括$(““)[0].reset();layui表格的设置,layui中弹出框附带数据的技巧等)】可知,layui表格对后端返回数据的格式有要求;如有需要,可以去那篇博客中去参考;

2.启动Tomcat,观察效果;

通过结果,可以看到layui不仅表格显示数据很给力,其开启分页后,也很智能;

至此,后台图书列表分页显示,就开发完了;接下来就是开发【修改】和【删除】按钮对应的功能了;


三:【修改】和【删除】按钮,提前分析;

在下篇博客我们将实现【修改】功能;

图书修改更新功能

说明:

(1) 本篇博客内容:【修改】功能;

(2) 几点说明:

● 本篇博客前端部分,涉及到了大量的layui的内容;没必要深究,但要尽量做到了能不留盲点的自洽;

● 本篇博客讲到了更新操作的套路:【先查询原始数据】→【然后,在原始数据基础上,进行对应属性的调整】→【然后,再拿这个调整后的原始对象,去更新】;这个十分重要;


一:前置内容说明;

1.【修改】部分,主要包含两部分内容;

(1)第一部分: 点击【修改】按钮后,根据id去查询图书信息,回填到弹出的表单中;


(2)第二部分:当我们修改完了,点击【立即提交】后,向后端的update接口,提交数据,去更新

2.前端内容分析;

(1) 第一部分: 点击【修改】按钮后,根据id去查询图书信息,回填到弹出的表单中;


(2)第二部分:当我们修改完了,点击【立即提交】后,向后端的update接口,提交数据

前端的内容分析好了,接下来就是在后端开发与之对应的接口了;


二:正式开发;

1.完成上面【第一部分】的内容:在MBookController中,创建一个根据图书id查询图书信息的方法:selectById()方法;

 
         @GetMapping("/id/{id}")
         @ResponseBody
         public Map selectById(@PathVariable("id") Long bookId) {
             Book book = bookService.selectById(bookId);
             Map result = new HashMap();
             result.put("code", 0);
             result.put("msg", "success");
             result.put("data", book);
             return result;
         }

说明:

(1) url要对应上,而且其需要使用路径变量去接收url中的参数;

有关路径变量,第一次遇到是在【RestController注解与路径变量】(使用@PathVariable注解来获取路径变量值);”)】;如有需要,可以去参考;

(2) 后端接口,根据前端需求返回数据就行了;

(3) 重启Tomcat,观察效果;

2.完成上面【第二部分】的内容:图书更新;

(1)在BookService接口中,定义更新图书的方法:updateBook()方法;


(2)在BookServiceImpl类中,去实现更新图书的方法:updateBook()方法;


(3)在MBookController中,创建更新的方法:updateBook()方法;(这儿,讲到了更新操作的一般操作策略,十分重要!!!)

 
         @PostMapping("/update")
         @ResponseBody
         public Map updateBook(Book book) {
             Map result = new HashMap();
             try {
                 Book rawBook = bookService.selectById(book.getBookId());
                 rawBook.setBookName(book.getBookName());
                 rawBook.setSubTitle(book.getSubTitle());
                 rawBook.setAuthor(book.getAuthor());
                 rawBook.setCategoryId(book.getCategoryId());
                 rawBook.setDescription(book.getDescription());
                 Document document = Jsoup.parse(book.getDescription());
                 String cover = document.select("img").first().attr("src");
                 rawBook.setCover(cover);
 
                 bookService.updateBook(rawBook);
                 result.put("code", 0);
                 result.put("msg", "success");
             } catch (BussinessException ex) {
                 ex.printStackTrace();
                 result.put("code", ex.getCode());
                 result.put("msg", ex.getMsg());
             }
             return result;
         }

说明:

(1) url要写对;

(2) 接收数据和返回数据,要符合前端要求;

(3) 更新的正确做法:

首先,不能直接拿前端传过来的对象去更新;

正确的做法是:先根据【前端传过来的对象的信息】,去数据库中查询【原始的对象信息】;然后,根据【前端传过来的对象的信息】,去更新【原始的对象信息】;然后,在拿这个修改过的原始对象,去更新数据库;

如果,我们直接拿前端传过来的对象去更新,轻则程序报错,重则数据产生混乱;


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


至此,图书修改功能就完成了;下篇博客将介绍图书删除功能;

图书删除功能

说明:

(1) 本篇博客内容:【删除】功能;

(2) 本篇博客的内容,很简单,没什么好说的;


一:正式开发;

1.在BookService接口中,定义删除图书的方法:delete()方法;

2.在BookServiceImpl实现类中,去实现删除图书的方法:delete()方法;

 
 
       /**
          * 删除图书(包括book表的图书信息,evaluation的评论信息,member_read_state表的阅读状态信息)
          *
          * @param bookId
          */
         @Transactional
         public void deleteBook(Long bookId) {
             //删除book表中的图书信息
             bookMapper.deleteById(bookId);
             //删除member_read_state表的阅读状态信息
             QueryWrapper<MemberReadState> mrsQueryWrapper = new QueryWrapper<MemberReadState>();
             mrsQueryWrapper.eq("book_id", bookId);
             memberReadStateMapper.delete(mrsQueryWrapper);
             //删除evaluation的评论信息
             QueryWrapper<Evaluation> evaQueryWrapper = new QueryWrapper<Evaluation>();
             evaQueryWrapper.eq("book_id", bookId);
             evaluationMapper.delete(evaQueryWrapper);
 
         }

说明:

(1) 删除方法,需要开启事务;

(2) 分别根据bookId删除,book表、evaluation表、member_read_state表的内容;

(3) 需要操作哪个表的时候,把该表对应的Mapper对象注入即可;

3.在MBookController中,创建删除图书的方法:deleteBook()方法;

 
         @GetMapping("/delete/{id}")
         @ResponseBody
         public Map deleteBook(@PathVariable("id") Long bookId) {
             Map result = new HashMap();
             try {
                 bookService.deleteBook(bookId);
                 result.put("code", 0);
                 result.put("msg", "success");
             } catch (BussinessException ex) {
                 ex.printStackTrace();
                 result.put("code", ex.getCode());
                 result.put("msg", ex.getMsg());
             }
             return result;
         }

说明:

(1) 这个方法没什么好说的,url和前端对应上;用到了路径变量;然后调用service层的方法;然后,根据前端的需求返回对应的信息即可;

4.启动Tomcat,观察效果;

然后,我们点击【删除】按钮;

其实,通过系统的日志,也能看到上述过程:(这儿就不啰嗦了)


至此,后台系统的图书增删改查,就都完成了;这些内容,十分基本,是必须要掌握的;


二:集成后台首页index.ftl;以及剩余任务说明……

1.把后台首页index.ftl,引入工程;

index.ftl:

 
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>MK书评网数据管理系统</title>
    <link rel="stylesheet" href="/resources/layui/css/layui.css">
</head>
 
<body class="layui-layout-body">
<!-- Layui后台布局CSS -->
<div class="layui-layout layui-layout-admin">
    <!--头部导航栏-->
    <div class="layui-header">
        <!--系统标题-->
        <div class="layui-logo" style="font-size:18px">MK书评网数据管理系统</div>
        <!--右侧当前用户信息-->
        <ul class="layui-nav layui-layout-right">
            <li class="layui-nav-item">
                <a href="javascript:void(0)">
                    <!--图标-->
                    <span class="layui-icon layui-icon-user" style="font-size: 20px">
                    </span>
                    <!--用户信息-->
                    admin
                </a>
            </li>
            <!--注销按钮-->
            <li class="layui-nav-item"><a href="/management/logout">注销</a></li>
        </ul>
    </div>
    <!--左侧菜单栏-->
    <div class="layui-side layui-bg-black">
        <!--可滚动菜单-->
        <div class="layui-side-scroll">
            <!--可折叠导航栏-->
            <ul class="layui-nav layui-nav-tree">
 
 
                    <li class="layui-nav-item layui-nav-itemed">
                        <a href="javascript:void(0)">数据管理</a>
                        <dl class="layui-nav-child module" data-node-id="xxx">
                            <dd><a href="图书管理页.html" target="ifmMain">图书管理</a></dd>
                            <dd><a href="短评管理.html" target="ifmMain">短评管理
                                </a></dd>
                        </dl>
                    </li>
 
 
 
            </ul>
        </div>
    </div>
    <!--主体部分采用iframe嵌入其他页面-->
    <div class="layui-body" style="overflow-y: hidden">
        <iframe name="ifmMain" style="border: 0px;width: 100%;height: 100%" src="图书管理页.html"></iframe>
    </div>
    <!--版权信息-->
    <div class="layui-footer">
        Copyright © imooc. All Rights Reserved.
    </div>
</div>
<!--LayUI JS文件-->
<script src="/resources/layui/layui.all.js"></script>
<script>
    //将所有功能根据parent_id移动到指定模块下
    layui.$(".function").each(function () {
        var func = layui.$(this);
        var parentId = func.data("parent-id");
        layui.$("dl[data-node-id=" + parentId + "]").append(func);
    });
    //刷新折叠菜单
    layui.element.render('nav');
</script>
</body>
</html>

2.创建ManagementController,编写后台首页的入口;

ManagementController:

 
     package com.imooc.reader.controller.management;
 
     import org.springframework.stereotype.Controller;
     import org.springframework.web.bind.annotation.GetMapping;
     import org.springframework.web.bind.annotation.RequestMapping;
     import org.springframework.web.servlet.ModelAndView;
 
     /**
      * 后台管理系统控制器
      */
     @Controller
     @RequestMapping("/management")
     public class ManagementController {
         @GetMapping("/index.html")
         public ModelAndView showIndex() {
             return new ModelAndView("/management/index");
 
         }
     }
 

说明:

(1) 这儿很简单,就是提供了后台首页index.ftl的访问入口;

(2) 只需要注意下一下,这类的url是【“/management”】,这可以很好的和前台系统区分开;

3.启动Tomcat,观察效果;

4.设置index.ftl,让其主体部分,显示图书管理页

5.重启Tomcat,观察效果:OK;


至此,MK书评网的,前后台基本开发完成了;

只是,还剩余【短评管理】和【后台登录】待开发……

SSM开发书评网总结

本篇博客是对【SSM开发书评网】的总结&归纳;SSM是真正贴合实际工作的第一个项目;是比较重要的;

说明:

(1) 目前觉得,就目前本专栏介绍了的内容来说,真正困难的是【数据库表设计】和【系统功能模块和结构设计】;


一:项目准备;

1. 在开发一个系统前,必须要有十分严格和明确的需求说明书;即,开发前,一定要搞清楚需求。

2. 开发前,要明确,采用什么技术栈。

这个项目主要使用SSM + Mybatis-Plus + layui + BootStrap + FreeMarker;

3. 创建与配置工程的步骤如下:

(1)创建工程:【创建一个就Maven的工程】→【把该工程设置为webapp工程】→【在IDEA中配置Tomcat】;

(2)配置Spring和Spring MVC:【引入spring-webmvc,FreeMarker,Jackson依赖】→【配置DispatcherServlet】→【启用Spring MVC注解开发模式】→【配置请求与响应字符集】→【配置FreeMarker】→【配置JSON序列化组件Jackson】;

(3)Spring与Mybatis整合:【引入spring-jdbc,mybatis,mybatis-spring,mysql-connector-java,druid依赖】→【配置数据源与druid连接池】→【配置SQLSessionFactoryBean对象】→【配置Mapper扫描器】→【创建mybatis-config.xml配置文件】;

(4)整合其他组件:【整合JUnit单元测试】→【整合logback日志】→【配置声明式事务】;

(5)SSM整合Mybatis-Plus:【引入Mybatis-Plus依赖】→【修改SQLSessionFactory的实现类】→【在mybatis-config.xml中,增加Mybatis-Plus的分页插件信息】;

(6)然后,因为项目前端部分需要使用BootStrap和LayUI等;所以,需要在项目中需要引入基础资源有:jQuery,BootStrap,LayUI,raty,Art-Template等;

上面的内容虽然麻烦,但很简单,跟着SOP做就行了;

4. 在数据库中创建一个逻辑空间;并根据项目业务需求,建表;

这一步需要相当的功力,目前自己似乎并不能很好的完成这个工作;


二:个人总结;

1. 遇到的小组件有:JS模板引擎Art-Template;星型评分插件raty;Kaptcha验证码组件; wangEditor富文本编辑器;java中一款不错的HTML解析器:jsoup;

2. IDEA的几个快捷键:

创建【接口实现类】的快捷方式:【Alt + Enter】;

跳转到接口实现类的方法上:【Ctrl + Alt + 左键点击方法】;

创建测试类的快捷键:【Ctrl + Alt + T】

3. Mybatis-Plus很给力,但是对于一些复杂的操作,Mybatis-Plus是不行的;此时,我们还需要使用Mybatis的方式去解决;

4. 究竟后端方法是【渲染数据跳转页面】还是【返回JSON数据】以及【返回JSON数据的格式】,这是由前端的要求决定的;而前端的编写又是由业务逻辑的需求决定的;

作为主栈是后端的开发者来说,只要和前端对好接口,就可以把主要精力放在后端的开发上;

5. Mybatis-Plus的分页查询,很给力;其中的QueryWrapper查询构造器比较重要;

6. 在前端中,灵活使用“隐藏域”进行存值等操作,可以帮助开发;(这条还好,自己主栈不是前端;目前做到了解即可)

7. 我们在条件判断,或者前后端传值的时候(Controller接收前端的参数):增加非空的判断(如果前端传递值为空,就赋给默认值),能够提高程序的健壮性;

8. 自定义异常,作为一种【自己定义的警报器】还是很给力的;

9. 在Service部分,对于可能出现异常的地方,我们要主动捕获异常;

10. 即使Service部分没有抛出异常,为了全面考虑,我们在Controller调用的时候,也可以去捕获异常;(因为此时Service的方法没抛异常,不代表以后需要增加新逻辑的时,不会抛出异常)

11. 每次开发完Service或者Dao后,及时的测试很有必要;

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

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

14. 在登录和注册功能处,使用了Kaptcha验证码组件;

15. 介绍了【注册】这种业务逻辑的开发套路;

16. 介绍了【登录】这种业务逻辑的开发套路;

17. 一种开发倾向:当我们的业务需要多表查询的时候,我们一般不在Dao层面使用多表查询的SQL语句来解决这个问题,而是在Service业务层面来化解,从而使得我们我们每次操作数据库的语句都是针对单表的;(PS:但是这点,我感觉不靠谱;)

18. Controller,Service,Dao在调用时,可以灵活一点,别那么死板,即希望具有一定的灵活性;(即AController可以去调用BService中定义的方法);

但是,究竟(一个要写在Controller中的)方法要写在哪个Controller中,这个能力需要慢慢加强;

19. 在Service层中控,即使是更新、删除、修改的方法,最好也要返回对应的更新、 删除 、修改的对象;

20. 重复点赞问题,自己想了一个笨笨的策略;但是,在实际项目中,如何解决这个业务,还不知道;

21. 讲到了Spring-Task定时任务的简单使用,其中也包括了Cron表达式和@Scheduled任务调度注解;

22. 介绍了wangEditor富文本编辑器;主要内容是,wangEditor富文本编辑器的图片上传;

23. 也展示了【layui表格 + Mybatis-Plus】,实现分页查询:使用体验不错;

24. 本篇博客讲到了更新操作的正确做法:【先查询原始数据】→【然后,在原始数据基础上,进行对应属性的调整】→【然后,再拿这个调整后的原始对象,去更新】;这点十分重要;

想到其他的,随时补充……