▮全文概述
- 本篇使用springBoot,MVC,AOP,MyBatis搭建了一个简单的网站。博主将一步步的讲解整个网站的开发流程,最后部署到服务器上。
- 此篇是纯粹的项目开发,项目所涉及的知识点都在上一文章中作出详解,跳转链接在底部。
- 全篇有5W字,可能是讲的有些啰嗦了,对此,博主还给出了代码纯享版,只有代码没有讲解的那种,自己可以对照着一起看。链接如下:
---------------分----------割-----------线----------------------------------------------------------------------
目录
请多多指导!!!
---------------分----------割-----------线----------------------------------------------------------------------
▮一、项目功能介绍
首先,先展示网站的各个界面,熟悉熟悉,看不看的懂倒是无所谓。
每个界面的功能都比较少,也不是很好看,因为这只是一个入门级别的网站,偏教学性,就不写的太过于复杂,也不那么在意颜值了。
▪1.1导航栏
每个界面的上面都有一个导航栏,导航栏的右侧会有几个a标签要实现:
- 我的:跳转至“我的博客列表页”
- 编辑:跳转至“编辑博客页”
- 注销:退出当前用户登录,并跳转至“登录页”,并不是注销账号哦
- 主页:跳转至“主页博客列表”
- 注册:跳转至“注册页”
▪1.2 注册
获取用户名,密码,确认密码。先在前端比对密码和确认密码,无误后再将用户名和密码传给后端注册进数据库。
▪1.3 登录
前端获取用户名和密码后发送给后端,后端将请求与数据库中的用户信息进行比对,登录成功则创建对应的session,保存用户信息,作为登录的令牌。
▪1.4主页博客列表
后端从数据库中获取很多博客信息,在前端构建成一个列表。如果数据库的列表很多,那就要“分页”展示了。后端根据页码和页面容量(每页展示几个博客)来返回对应的博客列表;当然,还要给出总页码数,不然的话分页不知道什么时候分到头。
▪1.5 博客详情
在主页点击“查看全文”后,就要从后端加载完整的博客详情和作者信息了。其中博客的内容是用“markdown”的格式来解析的,这个是使用“editor.md”编辑器来实现的。
"editor.md" 是一个基于 JavaScript 的开源 Markdown 编辑器,它提供了一系列功能丰富的编辑器组件,使用户可以方便地编辑和预览 Markdown 格式的文本内容。
▪1.6 编辑博客
点击导航栏上的“编辑”后,就能编辑博客的标题和正文。正文部分的编辑器使用的是“editor.md”提供的编辑器。点击发布文章后,前端将获取标题和正文并发送给后端存进数据库了。
▪1.7 我的博客列表
点击导航栏上的“我的”,就能访问个人界面,从后端数据库加载我的个人信息和我的博客列表。
个人信息与博客详情中的作者信息同源,我的博客列表和主页博客列表同源。都是差不多的代码,稍微改了改而已。
▪1.8 删除博客
点击“删除”后,会先弹出一个确认提示框,确认后前端将向后端发送删除博客的请求。要提一嘴的是,我们采取的是“逻辑删除”博客,而不是“物理删除”。
▪1.9 修改博客
先从后端获取并加载博客的标题和正文,在修改完成后再重新发送给后端,后端在数据库中修改。
▪1.10 注销(退出登录)
点击导航栏的“注销”,确认后往后端发送注销请求,后端将注销对应的session,实现退出登录。
▮二、创建项目
现在开始正式的编写项目代码了。博主使用的是开发工具是“2021社区版的IntelliJ IDEA”,如若不同,那就只能希望读者自行调整了,至于为什么没使用最新版,下文就有介绍。
▪2.1 插件准备
我们通过一个插件来创建Spring Boot项目,如图所示的这个插件,图中的其它插件可以没有。要注意的是,这个插件在最新版的IDEA上是收费的,所以就使用的21版的IDEA。
Spring Initializr and Assistant 是一个用于快速创建 Spring Boot 项目插件。它提供了一个简单易用的界面,让开发人员可以选择所需的依赖项、配置项目的基本设置,并生成一个基于 Spring Boot 的初始项目结构。
▪2.2 新建项目
在添加好上文的插件后,新建项目的选项就会变多,我们选择与图相同的选项
这里只需要修改红字选项
导入依赖,从上到下分别是:
- Lombok(依赖):Lombok是一个 Java 库,它通过注解方式简化了 Java 类的开发
- Spring Boot DevTools(热部署):热部署可以在开发过程中自动重新加载应用程序,节省了手动重启服务器的时间。
- springWeb(spring MVC的依赖)
- MyBatis framerwork(Mybatis依赖)
- MySQL Driver(MySQL驱动依赖):是用于在 Java 程序中连接和操作 MySQL 数据库的软件组件
项目保存路径自行设置
添加Maven框架支持
在.pom文件中去掉报红地方的“.RELWASE”,这大概是IDEA社区版的一个bug,在专业版的IDEA中没有这个问题,我们改正一下就行,无伤大雅。
这样一来,整个项目就创建成功,大概如下。当然,这个时候是无法正确启动项目的,因为导入Mybatis依赖后,项目必须得连接数据库后才能正常启动。
IDEA说不定会弹出这么一个错误,我们只需要点一下“Enable”就行,表示启动Lombok的注释功能。
![]()
▪2.3 创建项目的各级文件目录
项目的文件目录,可以先建好各级目录,也可以边写代码边创建,具体目录如图。
- “advice”:存放与统一处理相关的类
- “common”:存放整个项目公用的类
- “config”“存放跟项目配置有关的类
- “controller””存放控制层相关的类,项目对外开放的API
- “dao”“存放跟操作数据库相关的类
- “model””存放项目基础类
- “service”“存放服务层相关的类,为控制层提供功能
- “util””存放一些自定义工具类
▪2.4 添加资源文件
百度网盘
链接:https://pan.baidu.com/s/1DBdSY9O5hCdQ0t_3Ve9H_w?pwd=hldy
提取码:hldy
本章重后端开发,而不是前端开发,所以跟前端相关的资源文件就直接给给出相关模板了。
下载完后解压到目录“/resources/static”下就行。其中包含html,js,css,图片,editor.md(markdown文本编辑器),结构如图:
- “blog_add”是博客编辑页,
- "blog_content"是博客详情页,
- “blog_edit”是博客修改页,
- “blog_list”是主页博客列表页,
- “longin”是登录界面,
- “myblog_list”是我的博客列表页
- “reg”是注册界面。
▮三、数据库
项目建好后,就准备开始新建数据库了。本项目使用的数据库是MySQL 5.7,如若不同,请读者自行调整。
都说java是面向对象的语言,那就请读者思考思考:一个博客系统需要多少对象呢?、
用户,博客,站内信,板块,等等一系列对象。每个对象还需要各种属性,如用户(user)的:id、账号、密码、昵称、性别、头像、职业、地址、状态码,等等一系列的属性。对于这些对象和属性,设置项目所需就行。我们这只是一个简单的网站项目,我们所需的只有:
用户(user):id,账号,密码,昵称(为了简单,就跟账号共用了),状态码。
博客(blog):id,作者id,标题,创建时间,修改时间,正文,状态码。
状态码(state):用不同的码来表示不同的状态,表示用户被封禁,博客被审核等多种状态。就比如此项目中就是使用状态码来实现逻辑删除,而不是物理删除。
逻辑删除:是指在数据库中标记某条数据为已删除的状态,而不是真正从数据库中删除该数据记录。
▪3.1 新建数据库
在“model”目录下创建一个“db.sql”的文件,db(database)是数据库的简写,这个文件是用来存放数据库的sql语句的。将新建数据库的sql都保存到一个文件里,能方便以后的查看,便于理解数据库。
数据库命名为“blog_system”,库里新建两张表,user表和blog表。这些sql很简单,照着这张表写就行。建好表后,还要添加一些初始数据进去,好用来测试。
•SQL代码:
-- 1.建库
create database if not exists blog_system charset utf8;
-- 2.使用库
use blog_system;
-- 3.1用户表
drop table if exists user;
create table user(
id int primary key auto_increment,
username varchar(20) not null unique,
password varchar(65) not null,
state int not null default 0
);
-- 3.2博客表
drop table if exists blog;
create table blog(
id int primary key auto_increment,
uid int not null,
title varchar(100) not null,
createTime datetime not null,
updateTime datetime not null,
content text not null,
state int not null default 0
);
-- 4.初始化,密码是123,这里写入的是123加密后的密文
insert into user(id, username, password) VALUE (null,'zhangsan','a65a070469a543b8a1ccf68ed8eb6c95$d37c6a2f7b129499c36ba0cfb36351d9');
insert into blog(id, title, content, createTime, updateTime, uid,state) VALUE
(null,'星空中的神奇光点','夜空中繁星点点,每一颗星星都映照出无尽的宇宙之美。其中,最神奇的莫过于流星。流星划破夜空时,如同一道闪电从天而降,瞬间吸引着人们的目光。纵然流星只是短暂的一刹那,但它的意义却不容忽视。它代表着希望和美好,象征着未来的无限可能性。每一颗流星都是宇宙的礼赞,也是我们自己的心愿。让我们仰望星空,期待着下一颗流星的降临,同时也许愿着属于自己的幸福和梦想之星.','2021-12-06 17:10:48','2021-12-06 17:10:48',1,0);
insert into blog(id, title, content, createTime, updateTime, uid,state) VALUE
(null,'小溪的歌声','小溪缓缓流淌,如同一首动听的歌曲,轻轻地响起来。它源自高山之巅,穿过密林流淌,碰撞着石头,发出一阵阵清脆的声音。溪水清澈见底,似乎洗涤了一切的尘埃和污浊,让人感到宁静与舒适。沿着小溪的岸边,鲜花盛开,青草如茵,生机盎然。小溪的歌声让人心旷神怡,仿佛能听到大自然的呼吸和欢笑。我们应该学会倾听小溪的歌声,感受大自然的美妙与宁静。','2021-12-06 17:10:48','2021-12-06 17:10:48',1,0);
insert into blog(id, title, content, createTime, updateTime, uid,state) VALUE
(null,'微笑的力量','微笑是一种奇妙的力量,它可以照亮别人的心灵,也能温暖自己的内心。无论是在喜悦中微笑,还是在困难中坚持微笑,它都能化解痛苦和困境。微笑是一种积极的表达,它传递出乐观与希望,让人感到舒适与快乐。当你微笑时,周围的人也会感到愉悦,这是一种愉悦的传染效应。不论遇到什么困难和挫折,都要保持微笑,相信它能给你带来力量和坚定。让微笑成为你生活中的常态,用它传递快乐和温暖。','2021-12-06 17:10:48','2021-12-06 17:10:48',1,0);
-- 5.展示
select * from user;
select * from blog;
博主强烈建议读者自己写一边sql,就当是对数据库知识的一次复习,不要像前端那样直接复制粘贴。
▪3.2 创建基础类
建好数据库后,我们还要在项目里创建与表对应的java基础类,在“model”目录下新建两个类。代码很简单,就直接给出了。
•代码如下
@Data
public class Blog {
//博客id
private int id;
//博客标题
private String title;
//博客正文
private String content;
//博客创建时间
@JsonFormat( pattern = "yyyy-MM-dd",timezone = "GMT+8")
private LocalDateTime createTime;
//博客修改时间
@JsonFormat( pattern = "yyyy-MM-dd",timezone = "GMT+8")
private LocalDateTime updateTime;
//博客所属作者id
private int uid;
//状态码
private int state;
}
@Data
public class User {
//用户id
private int id;
//用户账号
private String username;
//用户密码
private String password;
//状态码
private int state;
}
@Date:是来自lombok的一个注解,功能就是为所有属性自动创建Set,Get,toString等一系列方法。
@JsonFormat :是 Jackson 库中的一个注解,用于指定在序列化和反序列化 JSON 数据时的日期格式和时区。pattern 参数指定了日期的格式为 "yyyy-MM-dd",timezone 参数指定了时区为 "GMT+8"。
GMT: 是格林尼治标准时间的缩写,是一种在全球范围内使用的时间标准,用作计算其他时区的参考。它是基于伦敦附近的格林尼治天文台的标准时间,+8就是北京的标准时区。
▪3.3 数据库连接
在spring boot项目的配置文件“application.properties”里配置数据库的连接,代码如下
# 配置数据库的连接字符串
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/blog_system?characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=自己设置的密码
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
账号密码填你设置的,而不是我这的123。如果你没设置过账号,默认就是root。
▪3.4 Mapper接口
我们要操作数据的两个表,那就为这两张表相关的操作创建MyBatis的Mapper接口。在目录“dao”下创建userMapper和blogMapper两个接口,涉及到的数据库操作都在这两个操作中来定义。
@Mapper
public interface BlogMapper {
//......
}
@Mapper
public interface UserMapper {
//......
}
为了简便,我使用注解来配置Mapper而不是XML来配置,习惯用XML的可以自行调整,并不是很困难的事情。
▮四、统一返回处理
在网站功能开发之前,我们要先约定前后数据交互的格式。因为现在的开发都是前后端分离的,后端的数据发送给前端前,需要做一个统一包装,包装成同一类型的数据返回给前端。
前后端分离是一种软件开发架构模式,旨在将应用程序的前端(用户界面)和后端(数据处理和业务逻辑)分离开来。
在传统的 Web 开发中,前端和后端通常是紧密耦合的,前端页面和后端逻辑混合在一起。这种方式存在一些问题,例如前端和后端的开发团队需要相互协调和合作,前端开发人员需要了解后端技术,后端开发人员需要了解前端技术,导致开发效率低下,代码维护困难等。
而前后端分离的架构模式将前端和后端完全解耦,使它们成为独立的两个应用程序。在这种架构下,前端通过 API与后端进行通信,获取数据和执行业务逻辑。前端可以使用任何技术栈来实现,如 JavaScript、React、Angular、Vue.js 等,而后端可以使用任何适合的编程语言和框架,如 Java、Python、Node.js 等。他们只需要按照统一的API进行交互即可。
在此,我们要进行“统一返回类型处理” 和 “统一异常处理”。
▪4.1 定义统一返回类型和枚举
- 定义统一返回类型ResultAjax
- 定义返回状态的枚举
- ResultAjax对象生成的工厂方法
第一步,定义统一返回类型ResultAjax。
我们在目录“common”下定义一个统一的返回类——ResultAjax。它包含(状态码,信息描述,数据)三个属性。创建后记得加上@Data标签。
@Data
public class ResultAjax {
//状态码
private int code;
//描述信息
private String msg;
//数据
private Object data;
}
状态码和描述信息来解释此次返回的信息,数据是后端返回给前端的具体数据。其中,状态码和描述信息是可以绑定在一起的,一种状态码对应一种描述信息,这个我们可以使用枚举来实现。
第二步,定义返回状态的枚举。
在目录“common”下新建一个枚举——ResultCode,定义code和message两个属性,将服务器所能出现的情况都枚举出来,实现相关方法:
public enum ResultCode {
//定义枚举
SUCCESS (200,"操作成功"),
FAILED (1000,"操作失败"),
FAILED_PARAMS_VALIDATE (1001,"参数校验失败"),
FAILED_CREATE (1002,"新增失败"),
FAILED_USER_EXISTS (1101,"⽤⼾已存在"),
FAILED_USER_NOT_EXISTS (1102,"⽤⼾不存在"),
FAILED_LOGIN (1103,"⽤⼾名或密码错误"),
ERROR_SERVICES (2000,"服务器内部错误"),
ERROR_IS_NULL (2001,"IS NULL.");
//1.定义属性
private int code; //状态码
private String message; //描述信息
//2. 定义构造
ResultCode(int code, String message) {
this.code = code;
this.message = message;
}
//3. 生成get方法
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
}
设置好枚举后,我们就要转回统一返回处理了。
第三步,ResultAjax对象生成的工厂方法。
服务器所能产生的结果只有成功和失败两类,两类返回的数据有所区别,所以我们用“工厂模式”来生成ResultAjax对象,这种生成对象的方式更加灵活。
•私有的构造方法,参数是枚举和数据:
@Data
public class ResultAjax {
//状态码
private int code;
//描述信息
private String msg;
//数据
private Object data;
//1.私有化构造方法
private ResultAjax(ResultCode resultCode, Object data) {
this.code = resultCode.getCode();
this.msg = resultCode.getMessage();
this.data = data;
}
}
•“成功”的工厂方法,是个类方法:
@Data
public class ResultAjax {
//状态码
private int code;
//描述信息
private String msg;
//数据
private Object data;
//1.私有化构造方法
private ResultAjax(ResultCode resultCode, Object data) {
this.code = resultCode.getCode();
this.msg = resultCode.getMessage();
this.data = data;
}
//2.成功的工厂方法
public static ResultAjax succ(Object data){
return new ResultAjax(ResultCode.SUCCESS,data);
}
public static ResultAjax succ(){
return new ResultAjax(ResultCode.SUCCESS,null);
}
}
- 提供了两个生成方法,因为有的成功响应需要传回数据,而有的不需要,因此就定义两个生成方法。
- 成功状态下的枚举只有一个,所以传参就不用传枚举了,直接内置就行。
•“失败”生成对象方法,同上。只是枚举不内置,多了一个枚举参数:
@Data
public class ResultAjax {
//状态码
private int code;
//描述信息
private String msg;
//数据
private Object data;
//1.私有化构造方法
private ResultAjax(ResultCode resultCode, Object data) {
this.code = resultCode.getCode();
this.msg = resultCode.getMessage();
this.data = data;
}
//2.“成功”的工厂方法
public static ResultAjax succ(Object data){
return new ResultAjax(ResultCode.SUCCESS,data);
}
public static ResultAjax succ(){
return new ResultAjax(ResultCode.SUCCESS,null);
}
//“失败”的工厂方法
public static ResultAjax fail(ResultCode resultCode){
return new ResultAjax(resultCode,null);
}
public static ResultAjax fail(ResultCode resultCode,Object data){
return new ResultAjax(resultCode,data);
}
}
▪4.2 实现统一返回处理
此时,我们已经定义好完整的统一返回类型了,最后,我们还要配置统一返回处理,即实现AOP。
我们在“advice”目录下定义一个类——ResponseAdvice,打上注解@RestControllerAdvice,接着让它实现接口——ResponseBodyAdvice,实现如图的两个方法。
ResponseAdvice:
@Slf4j
@RestControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return false;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
return null;
}
}
ResponseBodyAdvice 是 Spring 框架提供的一个接口,用于对响应体进行全局处理和修改。需要注意的是,ResponseBodyAdvice 只对使用 Spring MVC 的控制器方法返回的响应体生效,对于其他非控制器方法的响应,如静态资源或错误页面,不会被 ResponseBodyAdvice 处理。
其中,supports()方法不用了解太多,只需要知道它的返回类型是Boolean,当你返回false时,不进行统一返回处理,返回true时,进行统一返回处理。所以这里我们选择返回true。
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
beforeBodyWrite(),这个方法的意思是,在响应返回之前进行修改,参数body就是服务器响应要返回的对象,我们要对它进行修改。我们要做的修改就是,把所有响应统一包装成Result类型的对象来返回,以此实现统一返回类型。思路是:判断body的类型,是Result就直接返回,不是就包装成Result。代码如下。
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
//1.body是统一返回类型则直接返回
if(body instanceof ResultAjax){
return body;
}
//2.非统一返回类型则主动包成返回类型返回
return ResultAjax.fail(ResultCode.ERROR_SERVICES,body);
}
instanceof:判断对象类型所使用的关键字。
我们开发网站功能时,返回的类型都主动设为Result。如果出现了非ResultAjax的类型,那大概率就是我们代码写出问题了,所以我们打印一个日志,让我们知道这种情况。代码修改后如下,记得添加注解@Slf4j。
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
//1.body是统一返回类型则直接返回
if(body instanceof ResultAjax){
return body;
}
//2.非统一返回类型则主动包成返回类型返回
log.error("返回类型不为ResultAjax");
return ResultAjax.fail(ResultCode.ERROR_SERVICES,body);
}
至此,已经实现了统一返回处理,给出的完全代码如下:
@Slf4j
@RestControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
//1.body是统一返回类型则直接返回
if(body instanceof ResultAjax){
return body;
}
//2.非统一返回类型则主动包成返回类型返回
log.error("返回类型不为ResultAjax");
return ResultAjax.fail(ResultCode.ERROR_SERVICES,body);
}
}
▮五、统一异常处理
异常是个什么东西,相比各位都比较清除。当异常产生时,如果不捕获处理,程序将因此暂停。之前我们都是异常在哪里出现,就在哪里处理。但在服务器中,异常的出现防不胜防,你不一定知道异常在哪出现,而且哪里出现哪里处理也是一件很麻烦的事情,代码不仅不好读,还写的麻烦。所以我们就把所有异常统一到一个地方处理,称为统一异常处理。
统一异常处理的存在有以下几个主要原因:
提高代码可维护性:通过统一异常处理,可以将异常处理逻辑集中到一个地方,避免在代码的各个地方都编写重复的异常处理代码。这样可以减少代码冗余,提高代码的可维护性和可读性。
统一错误响应:统一异常处理可以帮助我们定义和返回统一的错误响应给客户端。无论是系统级异常还是自定义异常,都可以通过统一异常处理器返回相应的错误信息给客户端。这样可以提供一致的用户体验,使客户端能够更好地理解和处理错误情况。
安全性和信息保护:通过统一异常处理,我们可以屏蔽敏感信息,避免将具体的异常信息暴露给客户端。相反,我们可以返回更加友好和安全的错误信息,以保护系统的安全性和用户的隐私。
异常处理策略的集中管理:统一异常处理可以帮助我们定义和管理异常处理策略。我们可以根据不同的异常类型,采取不同的处理方式,例如记录日志、发送警报、回滚事务等。通过集中管理异常处理策略,可以更好地监控和管理应用程序的异常情况。
总的来说,统一异常处理可以提高代码的可维护性、提供一致的错误响应、增强安全性和信息保护,并集中管理异常处理策略。这些优点使得统一异常处理成为开发中的一项重要实践。
我们在“advice”目录下定义一个类——ExceptionAdvice,给它加上注解@RestControllerAdvice。在这个类中我们定义一个方法并实现——exceptionHandler()。
•完整代码如下:
@Slf4j
@RestControllerAdvice
public class ExceptionAdvice {
@ExceptionHandler(Exception.class)
public ResultAjax exceptionHandler(Exception e){
log.error(e.getMessage());
return ResultAjax.fail(ResultCode.ERROR_SERVICES);
}
}
- 这个方法的参数是一个异常,也就是捕获来的异常;返回值是一个ResultAjax对象,也就是上文提到的统一返回类型。
- 我们为这个方法添加注解@ExceptionHandler(Exception.class),注解里的是要捕获异常的类型,传入“Exception.class”表示这个方法捕获所有的异常类型。
- 在方法内部,我们要打印一个关于异常信息的日志给后端,还要返回一个ResultAjax对象给前端。
至此,统一异常处理结束。
@RestControllerAdvice 是一个注解,用于创建全局性的异常处理器和全局性的数据绑定设置。它结合了 @ControllerAdvice 和 @ResponseBody 的功能,可以在一个类中同时处理异常和返回响应数据。
统一返回类型和统一异常处理都要加上这个注解。
▮六、网站功能开发
每个功能开发的介绍流程是:功能分析-前后端约定-前端代码-后端代码。其中,前后端交互的数据格式都是json,前端发送的请求都是使用的Ajax。
dao层的Mapper接口已经定义,我们接下来在目录controller和service定义几个类
•目录controller:
@Slf4j
@RestController
@RequestMapping("/blog")
public class BlogController {
//BlogService
@Autowired
private BlogService blogService;
//UserService
@Autowired
private UserService userService;
}
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController{
//UserService
@Autowired
private UserService userService;
}
•目录service:
@Slf4j
@Service
public class BlogService {
//BlogMapper
@Autowired
private BlogMapper blogMapper;
}
@Slf4j
@Service
public class UserService {
//UserMapper
@Autowired
private UserMapper userMapper;
}
至此,正式开始功能的开发。
▪6.1 注册
•前后端约定
URL:/user/reg
type:POST
Content-Type: application/json; charset=utf-8
request:username,password
response:Result
- URL:/user/reg 表示该接口的路径是 "/user/reg",即用户注册的路径。
- type:POST 表示该接口使用 HTTP 的 POST 方法进行请求,即客户端向服务器提交数据。
- Content-Type: “application/json; charset=utf-8” 表示请求的数据格式是 JSON 格式,并且字符集是 UTF-8。
- request:username,password 表示请求中需要包含一个用户名和密码,这些信息将被用于用户注册。
- response:Result 表示服务器响应的数据将包含一个 "Result" 的字段,用于表示注册结果。
前后端约定是指前端和后端开发人员之间达成的一系列规定和约定,用于规范双方在开发过程中的交互方式和数据传输格式。这些约定可以确保前后端之间的协作顺利进行,并且能够有效地进行数据传递和处理。
•前端开发(reg.html)
“用户名,密码,确认密码”,这三个是前端需要处理的数据。我们需要进行的操作有:
1.给提交按钮绑定方法
2.提交前检查数据是否为空
3.提交前检查数据是否符合要求
4.提交前比对密码和确认密码
5.提交数据给后端,并根据后端响应作对应处理。
注册的前端代码在“reg.html”中,代码很简单就不解读了,直接进入正题。
第一步,给提交按钮绑定方法。新建js的submit()方法,设置button标签的onclick属性,代码如下:
submit():
<script>
function submit(){
}
</script>
button标签绑定方法submit():
<button id="submit" onclick="submit()">提交</button>
第二步,提交前检查数据是否为空。
js代码如下:
function submit(){
var username = jQuery("#username"); //用户名
var password = jQuery("#password"); //密码
var password2 = jQuery("#password2");//确认密码
//1.非空校验
if(username.val() == ""){
//弹出提示框
alert("请输入用户名")
//跳转到username输入框
username.focus();
return false;
}
if(password.val() == ""){
//弹出提示框
alert("请输入密码")
//跳转到username输入框
password.focus();
return false;
}
if(password2.val() == ""){
//弹出提示框
alert("请输入确认密码")
//跳转到username输入框
password2.focus();
return false;
}
}
- 使用jQuery(#id)来得到对应标签的对象,判断val是否为空,如果为空就弹出“请输入”的提示框,并且焦点指向对应输入框,最后退出方法。
alert():界面弹出一个提示框,提示括号内的字符串
focus():焦点转到此标签上。就是点击一下输入框
第三步,提交前检查数据是否符合要求 。
博主要求账号和密码不能有空格。对此,可以使用trim()方法来实现。这个方法的作用就是去除字符串里的空格,我们可以通过比对 “去空格前” 和 “去空格后” 的字符串来判断字符串是否存在空格。
//检验输入是否存在空格
if((username.val() != username.val().trim())
||(password.val() != password.val().trim())
||(password2.val() != password2.val().trim())){
alert("请不要输入空格");
return false;
}
第四步,提交前比对密码和确认密码。
if(password.val() != password2.val()){
alert("两次输入的密码不一致");
return false;
}
第五步,提交数据给后端,并根据后端响应作对应处理。
这里我们使用ajax来发送请求给后端。我们要设置请求的url,type,data(发送的数据),success(发送成功的回响函数)。我们要根据“前后端交互约定”来进行构建。
约定如下:
URL:/user/reg
type:POST
Content-Type: application/json; charset=utf-8
request:username,password
response:Result
ajax如下:
jQuery.ajax({
url:"/user/reg",
type:"POST",
data:{
"username":username.val(),
"password":password.val()
},
//4.展示后端响应结果
success:function(res){
if(res.code == 200){
alert("注册成功");
//跳转页面
location.href = "login.html";
}else{
alert("系统错误"+res.msg);
}
}
})
- success后接的是一个方法体。
- res是一个参数,是后端返回的统一返回对象。当它的状态码code为200时,表示操作成功,状态码200是之前规定好的枚举,忘了的可以回头再看看。
- 操作成功后,我们就弹出“注册成功”提示框,接着跳转至登录页面。location.href = "login.html"就是一个跳转页面指令。
完整代码如下:
<button id="submit" onclick="submit()">提交</button>
function submit(){
var username = jQuery("#username");
var password = jQuery("#password");
var password2 = jQuery("#password2");
//1.非空校验
if(username.val() == ""){
//弹出提示框
alert("请输入用户名")
//跳转到username输入框
username.focus();
return false;
}
if(password.val() == ""){
//弹出提示框
alert("请输入密码")
//跳转到username输入框
password.focus();
return false;
}
if(password2.val() == ""){
//弹出提示框
alert("请输入确认密码")
//跳转到username输入框
password2.focus();
return false;
}
//检验输入是否存在空格
if((username.val() != username.val().trim())
||(password.val() != password.val().trim())
||(password2.val() != password2.val().trim())){
alert("请不要输入空格");
return false;
}
//2.校验两次输入密码是否一致
if(password.val() != password2.val()){
alert("两次输入的密码不一致");
return false;
}
//3.发送数据到后端
jQuery.ajax({
url:"/user/reg",
type:"POST",
data:{
"username":username.val(),
"password":password.val()
},
//4.展示后端响应结果
success:function(res){
if(res.code == 200){
alert("注册成功");
//跳转页面
location.href = "login.html";
}else{
alert("系统错误"+res.msg);
}
}
})
}
ajax按json字符串的格式来构建。jQuery.ajax()方法的参数是一个json字符串。
{ url:……, type:……, data:……, success:…… }
发给后端的数据是一个query string。这是用抓包工具抓到的请求
服务器响应的是一个json字符串
•后端开发
根据约定,我们已知前端发来的数据有用户名和密码,我们接下来要进行的操作有:
1.接收并检验来自前端的数据
2.定义密码工具类PasswordUtils
3.对密码进行加盐加密
4.添加一个新用户到数据库
5.返回响应给前端
第一步,接收并检验来自前端的数据。
始终记得一点,不能相信来自前端的数据。因为有心之人可以通过各种软件来向服务器发送请求,而不是只通过前端。所以无论如何,后端也要再对数据进行一次检验。所以后端开发的大体思路就是:接收数据,检验数据,处理数据,返回结果。而前端开发的大体思路就是:获取数据,检验数据,发送后端,处理后端响应
@RestController
@RequestMapping("/user")
public class UserController{
@RequestMapping("/reg")
public ResultAjax reg(User user){
//1.参数检验
if (user == null || !StringUtils.hasLength(user.getUsername())
|| !StringUtils.hasLength((user.getPassword()))) {
return ResultAjax.fail(ResultCode.FAILED_PARAMS_VALIDATE);
}
}
}
- 对于用户名和密码,我们要检验它是否为空,要进行“str != null && !str.isEmpty()”判断。这个我们可以使用一个工具类StringUtils里的hasLength()方法来实现。
- 这个方法的URL路径为“/user/reg”,我们使用一个User对象来接收用户名和密码,方法最后要返回一个ResultAjax对象。
- 当参数异常时,我们就直接返回一个代表错误的ResultAjax。
StringUtils.hasLength() 是一个常用的字符串工具方法,它用于判断一个字符串是否具有长度(即非空且包含至少一个字符)。该方法通常用于检查字符串是否为空或仅包含空格字符。它返回一个布尔值,如果字符串不为null且长度大于0,则返回true;否则返回false。
第二步,定义密码工具类PasswordUtils。
我们先创建一个密码工具类,在里面定义好加密和解密的方法,再通过使用这个工具类的加密方法来对密码进行加密。
• PasswordUtils,在“common”目录中,定义加密和解密两个方法:
public class PasswordUtils {
//1.加密方法
public static String encrypt(String password){
}
//2.解密方法
public static boolean dcript( String password,String dbPassword){
}
}
我们使用MD5来加密密码,这是一个不可逆且只有唯一结果的算法。通俗的讲就是,输入的同一密码加密出的密文都是一样的,但无法通过密文逆推出密码。
虽说密码不可通过密文逆推,但你可别认为密码就无法破译。计算机有种魅力叫做暴力遍历,别人暴力遍历各种密码组合从而得到对应的密文,将原密码和密文保存至数据库中,这种数据库叫做“彩虹表”,别人只需要通过一次select语句就能根据你的密文得到原密码。
彩虹表是一种用于破解密码哈希算法的预计算技术,通过事先计算和存储大量的密码和对应的哈希值之间的映射关系,从而可以在一定程度上破解密码。
彩虹表的原理是通过事先计算和存储大量的密码和对应的哈希值之间的映射关系。这些映射关系被存储在一个巨大的表格中,称为彩虹表。当需要破解密码时,可以通过在彩虹表中查找相应的哈希值,从而找到对应的密码。
为了避免这种情况,我们使用“加盐”策略。这个策略就是往你的源密码中插入一些别人难以猜到的字符串,从而构造出新的密码送去加密,加入不同的字符串就得到不同的密文,很像炒菜加盐这种过程,不同的盐就得到不同味道的菜。
加盐(Salting)是一种增强密码哈希算法安全性的技术。在密码哈希过程中,加盐是指在原始密码之前或之后添加一个随机生成的字符串,称为盐(Salt)。盐的作用是增加密码哈希的随机性和复杂性,使得相同的密码在哈希后生成的哈希值不同。
我们通过UUID来获得盐,UUID是通用唯一标识符,通俗的讲,你能得到一组唯一的编号作为标识符,别人无法再通过UUID得到与你相同的编号,你的编号就是唯一的,在全球范围都生效。
UUID(Universally Unique Identifier)是一种标识符,用于在计算机系统中唯一地标识实体或对象。它是一个128位的数字,通常以字符串的形式表示。UUID的生成算法保证了在理论上几乎不可能重复生成相同的UUID。
我们将得到的盐和我们的密码拼接成一个新的字符串来进行加密,得到我们的密文,并且返回这次加密的盐,作为用户表的一项属性保存起来。以后我们可以通过这份盐,在登录验证时,与密码一同进行加密来得到正确是密文。
加密代码如下:
public static String encrypt(String password){
//1.得到盐
String salt = UUID.randomUUID().toString().replace("-","");
//2.MD5加密得到密文
String dpPassword = DigestUtils.md5DigestAsHex((salt+password).getBytes(StandardCharsets.UTF_8));
//3.返回“盐+密文”的拼接
return salt+"$"+dpPassword;
}
- UUID是一个36位的编号,如:f1f45db2-e754-4d66-85c1-9de0e5b7c4bd。其中有四位‘-’字符。我们用replace()去掉四位‘-’得到32位的字符串作为我们的盐salt。
- DigestUtils.md5DigestAsHex():这是DigestUtils类中的一个静态方法,会按照MD5来加密给定输入数据。由于只能加密字节数组,所以我们要用getBytes()将字符串转成字节数组。
- (salt+password):这里使用字符串拼接操作符将盐值和密码拼接在一起,作为加密的输入数据。
- .getBytes(StandardCharsets.UTF_8):这是将拼接后的字符串转换为字节数组的操作。使用UTF-8字符编码将字符串转换为字节数组。
- 得到密文后我们把盐和密文拼接起来,中间用$来分隔。这样一来,我们就可以直接把盐保存到用户表的密码属性中,不用再创建新的属性了。注意:MD5加密后得到的密文也是32位数字和字母组成的字符串,加上32位的盐和1位的分隔符$,总共65位的长度。所以我们用户表password的类型设的是varchar(65)。
好,加密结束后,我们来讲解解密。因为MD5加密的不可逆,我们无法将密文逆推为源密码。但因为MD5同一密码加密出的密文一样,所以我们可以将登录输入的密码进行加密,拿去比对密文是否相同即可。
解密的代码如下:
public static boolean dcript( String password,String dbPassword){
//1.切割出盐和加密后的密文
String[] salt_SP = dbPassword.split("\\$");
//2.加密“盐+登录密码”得到密文
String passwordEncrypt = DigestUtils.md5DigestAsHex((salt_SP[0]+password).getBytes(StandardCharsets.UTF_8));
//3.比对前端传入的密文跟数据库中的密文SrcPassword
if(passwordEncrypt.equals(salt_SP[1])){
return true;
}
return false;
}
- 因为$是java中的转义字符,所以我们要用‘\’让他不转义,但‘\’也是java中的转义字符,所以我们要用两个'\\'来不转义。所以“\\$”只是为了表达一个不转义的‘$’而已。
- salt_SP:这是一个长度为2的字符串数组,元素0是salt,元素1是SrcPassword
为什么加盐能破除彩虹表?
首先,彩虹表是不可能暴力遍历所有的密码组合的。上百个字符,从1位到2位到n位的排列组合,不是人类目前能够排列组合出来的,更别说黑客使用的彩虹表了,所以黑客只能遍历出有限的密码组合。但盐这个东西是你所独有的东西,黑客想不到,也排列组合不出你的盐。就比如UUID,UUID都是唯一值,黑客只能靠暴力遍历得到它,遍历出来的也只是UUID所有组合的汪洋大海中的一勺,因此黑客很难在彩虹表里遍历到你加盐后的密文。
相反,你的未加密密码就很好遍历到。因为大伙的密码普遍不会设的太复杂,多为数字和拼音,位数也就十来位左右,复杂了用户自己都记不住。所以说,彩虹表里已经有了绝大部分人密码加密后的密文。如果不给密码加盐,那破译你密码的密文轻而易举。
至此,密码工具类已经完成,我们就可以使用这个类来加密我们接下来的密码。
第三步,对密码进行加盐加密。
加密代码如下:
@RequestMapping("/reg")
public ResultAjax reg(User user){
//1.参数检验
if (user == null || !StringUtils.hasLength(user.getUsername())
|| !StringUtils.hasLength((user.getPassword()))) {
return ResultAjax.fail(ResultCode.FAILED_PARAMS_VALIDATE);
}
//2.密码加密
user.setPassword(PasswordUtils.encrypt(user.getPassword()));
}
第四步,添加一个新用户到数据库。
•(UserMaper)insertUser():
@Mapper
public interface UserMapper {
@Insert("insert into user(username,password) values(#{username},#{password})")
int insertUser(User user);
}
- 用户id是自增主键,由数据库自行赋值,不用我们管。
- 且此方法返回的值是影响的行数,因为我们只添加一个用户,所以影响的行数是1。
@Insert注解是MyBatis持久层框架中的一个注解,用于在数据库中执行插入操作。它通常与Mapper接口的方法结合使用,用于指定插入数据的SQL语句和参数映射。
•(UserService)insertUser():
@Service
public class UserService {
//user表的映射
@Autowired
private UserMapper userMapper;
public int insertUser(User user){
return userMapper.insertUser(user);
}
}
•(UserController)reg():
@RequestMapping("/reg")
public ResultAjax reg(User user){
//1.参数检验
if (user == null || !StringUtils.hasLength(user.getUsername())
|| !StringUtils.hasLength((user.getPassword()))) {
return ResultAjax.fail(ResultCode.FAILED_PARAMS_VALIDATE);
}
//2.密码加密
user.setPassword(PasswordUtils.encrypt(user.getPassword()));
//3.执行添加SQL
int result = userService.insertUser(user);
}
第五步,返回响应给前端。
@RequestMapping("/reg")
public ResultAjax reg(User user) throws Exception {
//1.参数检验
if (user == null || !StringUtils.hasLength(user.getUsername())
|| !StringUtils.hasLength((user.getPassword()))) {
return ResultAjax.fail(ResultCode.FAILED_PARAMS_VALIDATE);
}
//2.密码加密
user.setPassword(PasswordUtils.encrypt(user.getPassword()));
//3.执行添加SQL
int result = userService.insertUser(user);
//4.返回响应给前端
if(result != 1){
log.error("数据库添加用户失败");
return ResultAjax.fail(ResultCode.FAILED_USER_EXISTS);
}
return ResultAjax.succ(result);
}
- 影响的行数为1时表示正常,不为1时就要返回错误结果了。
至此注册功能开发完成,我们可以来验证一下。运行程序,在浏览器中输入网址“localhost:8080/user/reg.html”,就可以访问到注册界面了。我们注册一个xiaoming,就可以看到注册成功并且跳转到登陆界面。
我们还可以在数据库中搜查,可以看到,用户表中已经有了小明。
▪6.2 登录
实现注册功能后,我们来实现登陆功能。现在介绍的就不会像注册那么详细了,毕竟太繁琐了也不好看。
•前后端约定
URL:/user/login
type:POST
Content-Type: application/json; charset=utf-8
request:username,password
response:Result
•前端开发(login.html)
- 绑定提交方法,获取并检验账号密码
- 发送数据到后端
- 处理来自后端的响应
第一步,绑定提交方法,获取并检验账号密码。
•绑定doLogin():
<div class="row">
<button id="submit" onclick="doLogin()">提交</button>
</div>
function doLogin(){
var username = jQuery("#username");
var password = jQuery("#password");
//2.1.校验参数
if(username.val() == ""){
//弹出提示框
alert("请输入用户名")
//跳转到username输入框
username.focus();
return false;
}
if(password.val() == ""){
//弹出提示框
alert("请输入密码")
//跳转到username输入框
password.focus();
return false;
}
//2.2.发送数据到后端
jQuery.ajax({
url:"/user/login",
type:"POST",
data:{
"username":username.val(),
"password":password.val()
},
//2.3.处理响应
success:function (res){
if(res.code == 200){
location.href = "blog_list.html"
}else {
alert(res.msg);
}
}
});
}
- 在登陆成功后,就要跳转到“主页博客列表页”了,我们使用location.href来实现。
location.href
是 JavaScript 中用于获取或设置当前页面的 URL 的属性。当用于获取时,
location.href
返回当前页面的完整 URL。例如:console.log(location.href); // 输出当前页面的完整 URL
当用于设置时,
location.href
可以用于导航到一个新的 URL。例如:location.href = "https://www.example.com"; // 导航到指定的 URL
通过设置
location.href
的值,可以实现页面的跳转或重定向。
•后端开发
- 接受前端数据并检验
- 检验用户名和密码
- 新建session
- 返回响应给前端
第一步,接受前端数据并检验。
login():
@RequestMapping("/login")
public ResultAjax login(User user, HttpServletRequest request){
//1.参数检验
if (user == null || !StringUtils.hasLength(user.getUsername())
|| !StringUtils.hasLength((user.getPassword()))) {
return ResultAjax.fail(ResultCode.FAILED_PARAMS_VALIDATE,"账号或密码错误");
}
}
第二步,检验用户名和密码。
这里的检验思路并不是得到用户对象,比对用户名和密码;而是,根据传入的用户名在数据库中搜查用户,有用户就是对,没就是错,接着再将传入密码和数据库中密码送去比对。
@RequestMapping("/login")
public ResultAjax login(User user, HttpServletRequest request){
//1.参数检验
if (user == null || !StringUtils.hasLength(user.getUsername())
|| !StringUtils.hasLength((user.getPassword()))) {
return ResultAjax.fail(ResultCode.FAILED_PARAMS_VALIDATE,"账号或密码错误");
}
//2.账号密码比对
//能不能根据传入用户名在数据库中查找到用户对象
User userDB = userService.selectByUsername(user.getUsername());
if(userDB == null){
return ResultAjax.fail(ResultCode.FAILED_LOGIN);
}
//数据库用户对象的密码跟传入密码是否对的上
if(!PasswordUtils.dcript(user.getPassword(),userDB.getPassword())){
return ResultAjax.fail(ResultCode.FAILED_LOGIN);
}
}
- passwordUtils.dcrip()是在“注册”处的密码工具类中写的,忘了的可以回去看看。
•(UserMapper)selectByUsername():
@Select("select * from user where username=#{username}")
User selectByUsername(@Param("username") String username);
@Select注解通常与持久层框架(如MyBatis)一起使用。它的作用是将一个方法与数据库查询语句进行关联,使得方法能够执行相应的查询操作。
•(UserService)selectByUsername():
public User selectByUsername(String username){
return userMapper.selectByUsername(username);
}
第三步,新建session。
我们要多加一个参数HttpRequestServlet,使用getSession(true)新建会话,再将用户对象存进会话。
@RequestMapping("/login")
public ResultAjax login(User user, HttpServletRequest request){
//1.参数检验
if (user == null || !StringUtils.hasLength(user.getUsername())
|| !StringUtils.hasLength((user.getPassword()))) {
return ResultAjax.fail(ResultCode.FAILED_PARAMS_VALIDATE,"账号或密码错误");
}
//2.账号密码比对
//能不能根据传入用户名在数据库中查找到用户对象
User userDB = userService.selectByUsername(user.getUsername());
if(userDB == null){
return ResultAjax.fail(ResultCode.FAILED_LOGIN);
}
//数据库用户对象的密码跟传入密码是否对的上
if(!PasswordUtils.dcript(user.getPassword(),userDB.getPassword())){
return ResultAjax.fail(ResultCode.FAILED_LOGIN);
}
//3.新建会话
HttpSession session = request.getSession(true);
session.setAttribute(AppVar.SESSION_USER,userDB);
}
key我们使用一个全局变量来定义。在“common”目录下定义一个类——AppVar。在里面定义一个“SESSION_USER”。
public class AppVar {
//1.会话的key值
public static final String SESSION_USER = "USER_KEY";
//2.正文剪切出简介的最大长度
public static final int SUB_CONTENT_LENGTH = 80;
}
第四步,返回响应给前端。
@RequestMapping("/login")
public ResultAjax login(User user, HttpServletRequest request){
//1.参数检验
if (user == null || !StringUtils.hasLength(user.getUsername())
|| !StringUtils.hasLength((user.getPassword()))) {
return ResultAjax.fail(ResultCode.FAILED_PARAMS_VALIDATE,"账号或密码错误");
}
//2.账号密码比对
//能不能根据传入用户名在数据库中查找到用户对象
User userDB = userService.selectByUsername(user.getUsername());
if(userDB == null){
return ResultAjax.fail(ResultCode.FAILED_LOGIN);
}
//数据库用户对象的密码跟传入密码是否对的上
if(!PasswordUtils.dcript(user.getPassword(),userDB.getPassword())){
return ResultAjax.fail(ResultCode.FAILED_LOGIN);
}
//3.新建会话
HttpSession session = request.getSession(true);
session.setAttribute(AppVar.SESSION_USER,userDB);
//4.返回响应回前端
return ResultAjax.succ(null);
}
至此,登陆功能就开发完成。
▪6.3 主页博客列表
现在开始开发主页博客列表。可以看到主要有两个地方需要开发,一是博客列表,二是分页功能。这两个都比较麻烦,待博主下文满满到来。
•前后端约定
URL:/blog/blog_list
type:POST
Content-Type: application/json; charset=utf-8
request:page,psize
response:Result(List<Blog>,pCount)
- page是当前页码
- psize是每页容量
- List<Blog>是博客列表
- pCount是总页数。
•前端开发(blog_list.html)
1.得到当前页码
2.发送页码给前端,并处理来自后端的响应
3.处理最下方的切页按钮
第一步,得到当前页码。
页码存储在URL的query string中,我们使用“URLSearchParams”来得到链接中的键值对。
//页码
var page = 0; //页码
var pSize = 10;//每页容量
var pCount = 0;//总页码
page = new URLSearchParams(location.search).get("page");
if(page == null || page <= 0){
page = 1;
}
- location.search 是 JavaScript 中用于获取当前页面 URL 中query string部分的属性。它返回一个字符串,包含 "?" 后面的所有查询参数。
- new URLSearchParams(location.search) 创建了一个 URLSearchParams 对象,用于解析查询参数字符串。
- get("page") 是 URLSearchParams 对象的方法,用于获取指定名称的查询参数的值。在这里,我们指定获取名为 "page" 的查询参数的值。
- 将整个代码放在一起,它的作用是将当前页面 URL 中的查询参数解析为一个 URLSearchParams 对象,并从中获取名为 "page" 的查询参数的值,然后将该值赋给变量 page。
- 因为初始情况下,是从登录页跳转至此界面的,跳转的URL中是不含query string的。所以最后使用if语句来检验是否存在query string,不存在则赋予默认值1,表示当前是第一页。
第二步,发送页码给前端,并处理来自后端的响应。
发送给后端的数据只有两个,页码和每页容量,后端将根据这两个参数返回对应页码\的博客列表。
•html标签:
<!-- 每一篇博客包含标题, 摘要, 时间 -->
<div class="blog" >
<div class="title">我的第一篇博客</div>
<div class="date">2021-06-02</div>
<div class="desc">正文</div>
<a href="blog_content.html?id=1" class="detail">查看全文 >></a>
</div>
<div class="blog">
<div class="title">我的第一篇博客</div>
<div class="date">2021-06-02</div>
<div class="desc">正文</div>
<a href="blog_content.html?id=1" class="detail">查看全文 >></a>
</div>
- 根据此案例来构建前端界面,记得将案例代码给注释或删除
//2.从服务器得到数据,加载博客列表
jQuery.ajax({
url:"/blog/blogList",
type:"GET",
data:{
"page":page, //当前页码
"pSize":pSize//容量
},
success:function (res){
if(res.code == 200){
//得到总页数
pCount = res.data.pCount;
//显示关于页码的信息
jQuery("#page").text(" 当前是"+page+"页,总共有"+pCount+"页");
//在id为'.blog-list'的标签下拼接博客列表
var blogList = document.querySelector('.blog-list');
//拼接博客列表
for (var blog of res.data.list){
//-------------------------------------标---记---开---头------------------------------------------------------------------
/* @Desc : 组装blog标签插入blog-list标签
* 先创建div标签,再修改为指定类型,接着添入值,最后插入到上级标签
**/
//创建blog标签
let blogDiv = document.createElement("div");
blogDiv.className = 'blog';
//创建标题标签并添入值
let titleDiv = document.createElement('div');
titleDiv.className = 'title';
titleDiv.innerHTML = blog.title;
//创建时间标签并添入值
let dateDiv = document.createElement('div');
dateDiv.className = 'date';
dateDiv.innerHTML = blog.updateTime;
//创建正文内容标签并添入值
let contentDiv = document.createElement('div');
contentDiv.className = 'desc';
contentDiv.innerHTML = blog.content
//创建超链接标签并添入值
let a = document.createElement("a");
a.href = '/blog_content.html?id='+ blog.id;
a.innerHTML = '查看全文 >>';
//将所有新标签插入blog标签,使所有新建的标签成为一个整体,囊括在blog标签里
blogDiv.appendChild(titleDiv);
blogDiv.appendChild(dateDiv);
blogDiv.appendChild(contentDiv);
blogDiv.appendChild(a);
//blog标签插入blog-list标签,插入进HTMl
blogList.appendChild(blogDiv);
//-------------------------------------标---记---结---尾------------------------------------------------------------------
}
} else {
alert("系统异常"+res.msg);
}
}
})
- 首先,通过 for...of 循环遍历 res.data.list 数组中的每个博客对象。这个循环的目的是处理每个博客对象的信息。
- 在循环的每一次迭代中,首先创建一个 类型为“blog”的div 标签,并将其赋给变量 blogDiv。这个 div 标签用于包裹每个博客的信息。
- 创建一个类型为title的 div 标签,并将其赋给变量 titleDiv。设置 titleDiv 的类名为 'title',然后将博客对象的 title 属性的值赋给 titleDiv 的 innerHTML 属性,即设置标题的内容。
- 创建一个类型为date的 div 标签,并将其赋给变量 dateDiv。设置 dateDiv 的类名为 'date',然后将博客对象的 updateTime 属性的值赋给 dateDiv 的 innerHTML 属性,即设置时间的内容。
- 创建一个 类型为desc的div 标签,并将其赋给变量 contentDiv。设置 contentDiv 的类名为 'desc',然后将博客对象的 content 属性的值赋给 contentDiv 的 innerHTML 属性,即设置正文内容的内容。
- 创建一个 a 标签,并将其赋给变量 a。设置 a 的 href 属性为: “/blog_content.html?id=” + “博客的 id 属性的值”,用于生成博客内容页的链接。然后设置超链的显示文本为 '查看全文 >>' 。
- 将 titleDiv、dateDiv、contentDiv 和 a 这些新创建的标签依次插入到 blogDiv 标签中,使它们成为 blogDiv 的子元素,从而形成一个整体的博客信息结构。
- 将 blogDiv 标签插入到名为 blogList 的上级标签中,完成整个博客信息的拼接和插入操作。
注意:这里展示的是博客列表的构建方法之一,后文还有一种更为简单更为常用的方法,可以直接跳转至“我的博客列表”处查看。
第三步 ,处理最下方的切页按钮。给几个a标签绑定对应的函数:
<div class="blog-pagnation-wrapper">
<button class="blog-pagnation-item" onclick="startPage()">首页</button>
<button class="blog-pagnation-item" onclick="prePage()">上一页</button>
<button class="blog-pagnation-item" onclick="nextPage()">下一页</button>
<button class="blog-pagnation-item" onclick="endPage()">末页</button>
<span id="page"></span>
</div>
•js代码如下:
function prePage(){
//页码不能小于等于1
if(page <= 1){
alert("已到首页");
return;
}
//跳转至首页,并在URL拼接page
location.href="/blog_list.html?page="+(parseInt(page)-1);
}
function nextPage(){
//页码不能大于等于总页码
if(page >= pCount){
alert("已到尾页");
return;
}
location.href="/blog_list.html?page="+(parseInt(page)+1);
}
function startPage(){
location.href="/blog_list.html?page=1";
}
function endPage(){
location.href="/blog_list.html?page="+pCount;
}
parseInt() 是 JavaScript 中的一个内置函数,用于将字符串转换为整数。因为js是一种动态类型语言,变量的类型可以在运行时改变,在使用page+1时会把page当作字符串与1拼接;所以需要使用pareInt()来避免这种情况。
•后端开发
- 接收并检验参数
- 得到总页码数
- 得到分页后的列表
- 集成响应给前端
第一步,接收并检验参数。
@RequestMapping("/blogList")
public ResultAjax blogList(Integer pSize,Integer page){
//1.检验参数
if (pSize == null || pSize <= 0
|| page == null || page <= 0) {
return ResultAjax.fail(ResultCode.FAILED_PARAMS_VALIDATE);
}
}
页大小pSize和页码page不能小于等于0。
第二步,得到总页码数。
•controller层代码:
@RequestMapping("/blogList")
public ResultAjax blogList(Integer pSize,Integer page){
//1.检验参数
if (pSize == null || pSize <= 0
|| page == null || page <= 0) {
return ResultAjax.fail(ResultCode.FAILED_PARAMS_VALIDATE);
}
//2.计算总页数,调用service层
int count = blogService.blogAllCount();
int pCount = count / pSize;
if(count % pSize != 0){
pCount++;
}
}
博客总数/页大小的向上取整就是总页数。
•server层代码:
public int blogAllCount(){
return blogMapper.blogAllCount();
}
•dao层代码:
@Select("select COUNT(*) from blog where state=0")
int blogAllCount();
- COUNT(*) 是一个聚合函数,在 SQL 中用于统计指定列(或表达式)的非空值的数量。
- 因为数据库采取的是逻辑删除,所以要检验博客的状态state是否为0(存在)。
第三步:得到分页后的列表。
•controller层代码:
@RequestMapping("/blogList")
public ResultAjax blogList(Integer pSize,Integer page){
//1.检验参数
if (pSize == null || pSize <= 0
|| page == null || page <= 0) {
return ResultAjax.fail(ResultCode.FAILED_PARAMS_VALIDATE);
}
//2.计算总页数,调用service层
int count = blogService.blogAllCount();
int pCount = count / pSize;
if(count % pSize != 0){
pCount++;
}
//3.获取博客,根据页码和容量
List<Blog> list = blogService.selectAll(pSize,page);
}
•server层代码:
public List<Blog> selectAll(int pSize,int page){
List<Blog> list = blogMapper.selectAll(pSize,pSize*(page-1));
subContent(list);
return list;
}
- 从数据库得到博客列表后还需对博客正文进行裁剪,作为博客简介。
subContent():
private void subContent(List<Blog> list){
for(Blog blog:list){
//截取前80字符
if(blog.getContent().length()> AppVar.SUB_CONTENT_LENGTH){
//用截取后的简介替代正文
blog.setContent(blog.getContent().substring(0,AppVar.SUB_CONTENT_LENGTH));
}
}
}
•dao层代码:
@Select("select * from blog where state=0 order by updateTime desc limit #{pSize} offset #{page}")
List<Blog> selectAll(@Param("pSize")int pSize,@Param("page")int page);
- 在SQL中,使用“ limit m offset n ”来实现分页查询,m表示要查出多少列数据,n表示从第几个数据开始查。所以,此limit查询语句中,m是pSize,n是pSize*(page-1)。要查第n页的博客,那就要从第n-1页后的博客开始查,第n-1页的博客总数就是pSize*(page-1),这是一个很好推导的公式。
- “order by updateTime”,按照博客的修改时间来降序排列
为什么要在service计算limit的m,n?:因为数据库的资源比服务器的资源宝贵。数据库通常只有一个且不好扩展,而服务器的升级就相对简单;所以,尽可能的在服务完成参数的运算,再传给数据库去执行。
第四步,集成响应给前端。
@RequestMapping("/blogList")
public ResultAjax blogList(Integer pSize,Integer page){
//1.检验参数
if (pSize == null || pSize <= 0
|| page == null || page <= 0) {
return ResultAjax.fail(ResultCode.FAILED_PARAMS_VALIDATE);
}
//2.计算总页数,调用service层
int count = blogService.blogAllCount();
int pCount = count / pSize;
if(count % pSize != 0){
pCount++;
}
//3.获取博客,根据页码和容量
List<Blog> list = blogService.selectAll(pSize,page);
//4.将数据集合进map返回
HashMap<String,Object> hashMap = new HashMap<>();
hashMap.put("list",list);
hashMap.put("pCount",pCount);
return ResultAjax.succ(hashMap);
}
此次返回的数据有两个:总页数和page页博客列表。对此,我们使用一个map将数据集合成一个整体发送给前端。
▪6.4 博客详情
博客详情需要两样东西:文章信息和作者信息 。其中作者信息只有作者名和文章总数,其它信息暂不操作,使用默认。
•前后端约定
URL:blog/content
type:POST
Content-Type: application/json; charset=utf-8
request:id(博客id)
response:Result(Blog,name,count)
Blog是博客对象,内含博客信息。name是作者名,count是文章总数,这两个属于作者信息。
•前端开发(blog_content.html)
- 从URL获取博客id
- 获取作者信息和博客信息并加载
第一步,从URL获取博客id。
var blogId = new URLSearchParams(location.search).get("id");
- location.search:location 是一个包含有关当前 URL 的信息的对象。search 是 location 对象的属性,表示 URL 中的查询参数部分,即 ? 后面的内容。例如,对于 URL: https://example.com/page?id=123&name=John,location.search 的值是: ?id=123&name=John。
- new URLSearchParams(location.search):URLSearchParams 是一个 JavaScript 内置的类,用于解析和操作 URL 查询参数。通过将 location.search 传递给 URLSearchParams 构造函数,我们创建了一个 URLSearchParams 对象,用于处理查询参数。
- .get("id"):get() 是 URLSearchParams 对象的方法,用于获取指定查询参数的值。在这个例子中,我们使用 .get("id") 来获取名为 "id" 的查询参数的值。
第二步,获取作者信息并加载。
ajax请求:
jQuery.ajax({
url:"blog/content",
type:"GET",
data:{
id:blogId
},
success:function (res){
if(res.code==200){
//作者名
let name = res.data.name;
//文章总数
let count = res.data.count;
//博客信息
let blog = res.data.blog;
//修改作者信息
jQuery("#name").html(name);
jQuery("#count").html(count);
//插入博客信息
var content = "";
//3.1.拼接博客正文标签
content += '<div>';
content += '<h3>'+blog.title+'</h3>';
content += '<div class="date">'+blog.updateTime+'</div>';
content += '<div id="editorDiv"></div>';
content += '</div>';
//3.2.插入标签到界面
jQuery('#blog-content').html(content);
//3.3.使用editormd来解析markdown
//-------------------------------------标---记---特---殊------------------------------------------------------------------
/* @Desc : 使用editormd解析markdown文本
* editormd是editormd编辑器的对象,内含许多操作方法
* markdownToHTML(html标签id,{解析格式: 需解析的字符串}),
* 将'需解析字符串'按照'解析格式'解析到'id'对应的html标签
**/
editormd.markdownToHTML('editorDiv',{markdown: blog.content});
//----------------------------------------------------------------------------------------------------------------------
}else {
alert("系统错误:"+res.msg);
}
}
})
- 自定义博客标签的各个部分,再将各标签拼成一个字符串,最后再插入界面。这是一种简便易懂的插入新标签的方式。
- 名为 editormd 的库中的 markdownToHTML() 方法将 Markdown 格式的博客内容转换为 HTML,并将生成的 HTML 内容插入到具有 id 为 editorDiv 的 <div> 元素中。
- editormd:这是一个库或对象,提供了处理 Markdown 的功能;
- markdownToHTML():这是 editormd 对象中的一个方法,用于将 Markdown 格式的文本转换为 HTML;
- 'editorDiv':这是一个字符串,表示要将生成的 HTML 内容插入的目标 <div> 元素的 id。在这个例子中,目标元素的 id 是 editorDiv;
- {markdown: blog.content}:markdown 是一个选项,表示要以Markdown的格式解析文本 。blog.content 是一个变量,表示博客的内容,它包含了 Markdown 格式的文本。
html标签:
<!-- 作者信息相关的标签 -->
<div class="card">
<img src="img/avatar.png" class="avtar" alt="">
<h3 id="name">小可爱</h3>
<a href="http:www.github.com">github 地址</a>
<div class="counter">
<span>文章</span>
<span>分类</span>
</div>
<div class="counter">
<span id="count"></span>
<span>1</span>
</div>
</div>
<!-- 博客内容相关的标签 -->
<div id="blog-content">
<!-- 博客标题 -->
<h3>我的第一篇博客</h3>
<!-- 博客时间 -->
<div class="date">2021-06-02</div>
<!-- 博客正文 -->
<div id="editorDiv">
</div>
</div>
作者信息只修改两处标签,名称和文章统计。而博客内容是前端给出的示例代码,记得要注销掉或删除
•后端开发
- 接收博客id并返回博客信息和作者信息
第一步,接收博客id并返回作者信息。
controller层:
@RequestMapping("/content")
public ResultAjax blogContent(Integer id) {
//1.参数检验
if(id == null || id <= 0){
return ResultAjax.fail(ResultCode.FAILED_PARAMS_VALIDATE,"参数异常");
}
//2.获取博客对象
Blog blog = blogService.selectOne(id);
//3.获取作者信息,根据作者id
int uid = blog.getUid();
String name = userService.selectById(uid).getUsername();
int count = blogService.count(uid);
//4.集合结果返回给前端
HashMap<String,Object> hashMap = new HashMap<>();
hashMap.put("name",name);
hashMap.put("count",count);
hashMap.put("blog",blog);
return ResultAjax.succ(hashMap);
}
Blog的uid属性记录着作者的id,可以据此来查询作者信息和文章总数。
UserService层:
public User selectById(int id){
return userMapper.selectById(id);
}
BlogService层:
public Blog selectOne(int id){
return blogMapper.selectOne(id);
}
public int count(int uid){
return blogMapper.count(uid);
}
dao(UserMapper)层:
@Select("select * from user where id=#{id} and state=0")
User selectById(@Param("id")int id);
dao(BlogMapper)层:
@Select("select * from blog where id=#{id} and state=0")
Blog selectOne(@Param("id") int id);
@Select("select COUNT(*) from blog where uid=#{uid} and state=0")
int count(@Param("uid") int uid);
▪6.5 编辑博客
两个重点:一是加载editor.md编辑器,二是获取文章标题和正文发送给后端。编辑器是一个很复杂的东西,不好实现,但可以使用现成的一些开源编辑器,比如:editor.md,这是个开源的编辑器,官网是:Editor.md - 开源在线 Markdown 编辑器。
•前后端约定
URL:/blog/add
type:POST
Content-Type: application/json; charset=utf-8
request:title,content
response:Result(null)
•前端开发(blog_add.html)
- 加载editor.md编辑器
- 给提交按钮绑定submit()方法
- 获取标题和title发送给后端并处理响应
第一步,加载editor.md编辑器。
html标签:
<!-- 创建编辑器标签 -->
<div id="editorDiv">
<textarea id="editor-markdown" style="display:none;"></textarea>
</div>
textarea标签是用来存放文本的标签,编辑器会复制文本域的文本到这个标签中,此标签设为不可见,因为这个标签只是我们获取文本域文本的一个媒介,并不发挥其它作用。
js代码:
//编辑器对象
var editor;
//编辑器初始化方法
function initEdit(md){
// 编辑器设置
editor = editormd("editorDiv", {
// 这里的尺寸必须在这里设置. 设置样式会被 editormd 自动覆盖掉.
width: "100%",
// 高度 100% 意思是和父元素一样高. 要在父元素的基础上去掉标题编辑区的高度
height: "calc(100% - 50px)",
// 编辑器中的初始内容
markdown: md,
// 指定 editor.md 依赖的插件路径
path: "editor.md/lib/",
saveHTMLToTextarea: true //
});
}
//初始化
initEdit("# 在这里写下一篇博客"); // 初始化编译器的值
- var editor:声明一个变量 editor,用于存储编辑器对象。
- function initEdit(md):定义一个名为 initEdit 的函数,接受一个参数 md,表示初始的 Markdown 内容。
- editor = editormd("editorDiv", {}:通过调用 editormd 方法创建编辑器对象,并将其赋值给 editor 变量。"editorDiv" 是指编辑器将被插入到 ID 为 "editorDiv" 的标签中。
- width: "100%",:设置编辑器的宽度为 "100%"。
- height: "calc(100% - 50px)",:设置编辑器的高度为父元素高度减去 50px。
- markdown: md,:指定编辑器的初始内容为参数 md 的值,也是方法传入的参数。
- path: "editor.md/lib/",:指定 editor.md 依赖的插件路径为 "editor.md/lib/"。
- saveHTMLToTextarea: true:设置保存 HTML 内容到文本区域为 true。文本域里的文本将会保存保存到“textarea标签”中,我们靠这个标签来获得编辑器里的文本。
- initEdit("# 在这里写下一篇博客");:调用 initEdit 函数并传入初始的 Markdown 内容 "# 在这里写下一篇博客",用于初始化编辑器的值。
第二步,给按钮绑定submit()标签。
html标签:
<button onclick="submit()">发布文章</button>
第三步,获取标题和title发送给后端并处理响应。
submit():
function submit(){
let title = jQuery("#title");
let content = jQuery("#editor-markdown");
//3.1.参数检验
if(title.val() != title.val().trim()){
alert("标题不能使用空格");
title.focus();
return;
}
if(title.val() == null || title.val().length==0){
alert("标题不能为空");
title.focus();
return;
}
//3.2.发送请求
jQuery.ajax({
url:"/blog/add",
type:"POST",
data:{
"title":title.val(),
"content":content.val()
},
//3.3.处理服务器响应
success:function (res){
if(res.code == 200){
alert("博客添加成功") ;
location.href="myblog_list.html";
}else{
alert("系统错误: "+res.msg);
}
}
})
}
- 发送数据前,要对获取到的数据进行检验
•后端开发
- 接收并检验参数
- 补充blog的剩余属性
- 添加博客进数据库并返回响应给前端
第一步,接收并检验参数。
@RequestMapping("/add")
public ResultAjax blogAdd(Blog blog,HttpServletRequest request){
//1.参数检验
if(blog.getTitle() == null || !StringUtils.hasLength(blog.getTitle())
|| blog.getContent() == null || !StringUtils.hasLength(blog.getContent())){
return ResultAjax.fail(ResultCode.FAILED_PARAMS_VALIDATE,"参数为空");
}
}
第二步,补充blog的剩余属性。因为前端只传了标题和正文两个属性,其余的属性则要补充,如:作者id,创建时间,修改时间,状态码。
@RequestMapping("/add")
public ResultAjax blogAdd(Blog blog,HttpServletRequest request){
//1.参数检验
if(blog.getTitle() == null || !StringUtils.hasLength(blog.getTitle())
|| blog.getContent() == null || !StringUtils.hasLength(blog.getContent())){
return ResultAjax.fail(ResultCode.FAILED_PARAMS_VALIDATE,"参数为空");
}
//2.补充用户uid
User user = SessionUtils.getUser(request);
if(user == null){
return ResultAjax.fail(ResultCode.FAILED_USER_EXISTS,"账号异常");
}
blog.setUid(user.getId());
//3.补充创建时间,修改时间,状态码
blog.setCreateTime(LocalDateTime.now());
blog.setUpdateTime(LocalDateTime.now());
blog.setState(0);
//4.添加blog进数据库
//5.返回响应给前端
return ResultAjax.succ(blogService.insert(blog));
}
- 作者id我们通过访问当前session里的user对象来获得
- LocalDateTime.now():是方法用于获取当前时间的时间戳
- SessionUtil.getUser(request)中的SessionUtil类是我们自定义的一个会话工具类,因为项目中还有许多地方需要访问会话,所以就自定义了此工具类。
SessionUtils(目录util下):
public class SessionUtils {
public static User getUser(HttpServletRequest request){
//1.获取但不新建会话
HttpSession session = request.getSession(false);
//2检验会话
if(session == null || session.getAttribute(AppVar.SESSION_USER) == null){
return null;
}
//3.返回会话里保存的user对象
return (User) session.getAttribute(AppVar.SESSION_USER);
}
}
第三步,添加博客进数据库并返回响应给前端。
controller层:
@RequestMapping("/add")
public ResultAjax blogAdd(Blog blog,HttpServletRequest request){
//1.参数检验
if(blog.getTitle() == null || !StringUtils.hasLength(blog.getTitle())
|| blog.getContent() == null || !StringUtils.hasLength(blog.getContent())){
return ResultAjax.fail(ResultCode.FAILED_PARAMS_VALIDATE,"参数为空");
}
//2.补充用户uid
User user = SessionUtils.getUser(request);
if(user == null){
return ResultAjax.fail(ResultCode.FAILED_USER_EXISTS,"账号异常");
}
blog.setUid(user.getId());
//3.补充创建时间,修改时间,状态码
blog.setCreateTime(LocalDateTime.now());
blog.setUpdateTime(LocalDateTime.now());
blog.setState(0);
//4.添加blog进数据库
//5.返回响应给前端
return ResultAjax.succ(blogService.insert(blog));
}
BlogService:
public int insert(Blog blog){
return blogMapper.insert(blog);
}
BlogMapper:
@Insert("insert into blog(title, content,updateTime,createTime, uid, state)
values (#{title},#{content},#{updateTime},#{createTime},#{uid},#{state})")
int insert(Blog blog);
▪6.6 我的博客列表
需要用户信息和所属博客列表。相关代码之前都写过只需要稍微改改就行 ,如“我的博客列表”比“主页博客列表”多了修改和删除标签,我们加上就行。
•前后端约定
URL:/blog/myBlogList
type:GET
Content-Type: application/json; charset=utf-8
request:null
response:Result(User(无密码),List(Blog))
- 请求为空是因为用户的相关信息都存在Cookie中,通过session就能得到。
- 后端传回来的User信息不能带上密码,记得在后端处理这点
•前端开发(myblog_list.html)
- 获取用户信息和博客列表并加载
第一步,获取用户信息和博客列表并加载。相关代码都是之前写过的,稍稍修改就行
ajax请求:
jQuery.ajax({
url:"/blog/myBlogList",
type:"GET",
success:function (res){
if(res.code == 200){
//博客列表
let list = res.data.list;
//用户名
let name = res.data.name;
//用户博客总数
let count = res.data.count;
//用户信息
jQuery("#name").html(name);
jQuery("#count").html(count);
//博客列表
var blogList = "";
//3.1.当博客列表为空时加载的默认内容
if(list == null || list.length == 0){
blogList += "<h3 style='margin-left: 300px;margin-top: 100px'>暂无博客,请<a href='blog_add.html'>添加</a>!</h3>"
}
//3.2.拼接用户所属博客列表
for(let blog of list){
//-------------------------------------标---记---开---始------------------------------------------------------------------
/* @Desc : 组装blog标签插入blog-list标签
* blogList是一个字符串,在里面拼接一个html格式的字符串
* 标签.html()往标签里插入一个html格式的字符串
**/
blogList += '<div class="blog">';
blogList += '<div class="title">'+blog.title+'</div>';
blogList += '<div class="date">'+blog.updateTime+'</div>';
blogList += '<div class="desc">\n'+blog.content+'</div>';
blogList += '<a href="/blog_content.html?id='+blog.id+'" class="detail">查看全文 >></a> '
blogList += '<a href="/blog_edit.html?id='+blog.id+'" class="detail">修改 >></a> '
//-------------------------------------标---记---开---始------------------------------------------------------------------
/* @Desc : a标签调用js函数,能够保留参数
* javascript:function(),a标签调用js函数的格式
* dle(blog.id),传入到函数的参数是可以保存的,在a标签调用此函数时,会把参数传入到方法内
**/
blogList += '<a href="javascript:del('+blog.id+')" class="detail">删除 >></a>'
//-------------------------------------标---记---结---尾------------------------------------------------------------------
blogList += '</div>';
}
jQuery("#blog-list").html(blogList);
//-------------------------------------标---记---结---尾------------------------------------------------------------------
}else{
alert("系统错误"+res.msg);
}
}
})
- 个人的博客列表是有可能为空的,此时就要加上一个默认HTML
- 比起“主页博客列表”,需要多加修改和删除两个超标签。
- “修改”:超链接是跳转至“blog_edit.html”界面
- “删除”:超链接是触发del()方法。需要注意的是,del方法所需的参数可以在a标签创建的时候就预留,等到方法执行的时候传入。
html标签:
<!-- 用户信息 -->
<div class="card">
<img src="img/avatar.png" class="avtar" alt="">
<h3 id="name">小可爱</h3>
<a href="http:www.github.com">github 地址</a>
<div class="counter">
<span>文章</span>
<span>分类</span>
</div>
<div class="counter">
<span id="count"></span>
<span>1</span>
</div>
</div>
<!-- 博客列表 -->
<div id="blog-list">
<!-- 每一篇博客包含标题, 摘要, 时间 -->
<div class="blog">
<div class="title">我的第一篇博客</div>
<div class="date">2021-06-02</div>
<div class="desc">从今天起, 我要认真敲代码. </div>
<a href="blog_content.html?id=1" class="detail">查看全文 >></a>
<a href="blog_content.html?id=1" class="detail">修改 >></a>
<a href="blog_content.html?id=1" class="detail">删除 >></a>
</div>
</div>
•后端开发
- 获取作者信息
- 获取博客列表
- 集合结果返回给前端
第一步,获取作者信息。从session中获取用户对象,因为登录的时候保存了session。
@RequestMapping("/myBlogList")
public ResultAjax myBlogList(HttpServletRequest request){
//1.得到登录用户对象
User user = SessionUtils.getUser(request);
//2.检验用户会话是否正常
if(user == null){
return ResultAjax.fail(ResultCode.FAILED_USER_EXISTS,"账号异常");
}
}
第二步,获取博客列表。
BlogController:
@RequestMapping("/myBlogList")
public ResultAjax myBlogList(HttpServletRequest request){
//1.得到登录用户对象
User user = SessionUtils.getUser(request);
//2.检验用户会话是否正常
if(user == null){
return ResultAjax.fail(ResultCode.FAILED_USER_EXISTS,"账号异常");
}
//3.得到博客列表
int uid = user.getId();
List<Blog> list = blogService.selectMyAll(uid);
}
BlogService:
public List<Blog> selectMyAll(int uid){
List<Blog> list = blogMapper.selectMyAll(uid);
subContent(list);
return list;
}
- 从数据库得到博客列表后还需对博客正文进行裁剪,作为博客简介。
subContent:
private void subContent(List<Blog> list){
for(Blog blog:list){
//截取前80字符
if(blog.getContent().length()> AppVar.SUB_CONTENT_LENGTH){
//用截取后的简介替代正文
blog.setContent(blog.getContent().substring(0,AppVar.SUB_CONTENT_LENGTH));
}
}
}
BlogMapper:
@Select("select * from blog where uid=#{uid} and state=0
order by updateTime desc")
List<Blog> selectMyAll(@Param("uid") int uid);
- 博客列表按修改时间来降序排序
第三步,集合结果返回给前端。
@RequestMapping("/myBlogList")
public ResultAjax myBlogList(HttpServletRequest request){
//1.得到登录用户对象
User user = SessionUtils.getUser(request);
//2.检验用户会话是否正常
if(user == null){
return ResultAjax.fail(ResultCode.FAILED_USER_EXISTS,"账号异常");
}
//3.得到博客列表,用户名,博客总数
int uid = user.getId();
List<Blog> list = blogService.selectMyAll(uid);
String name = user.getUsername();
int count = blogService.count(uid);
//4.集合结果到map中
Map<String,Object> map = new HashMap<>();
map.put("list",list);
map.put("name",name);
map.put("count",count);
//3.返回响应给前端
return ResultAjax.succ(map);
}
▪6.7 修改博客
修改博客也很简单,先获取博客的信息,再发送新的博客信息到后端
•前后端约定
获取博客正文的约定:
URL:/blog/edit?id=(blogId)
type:GET
Content-Type: application/json; charset=utf-8
request:null
response:Result(Blog)
- 使用URL的query string来传输数据。
发送修改博客的约定 :
URL:/blog/update
type:POST
Content-Type: application/json; charset=utf-8
request:title,content
response:Result(null)
•前端开发(blog_edit.html)
- 加载editor.md编辑器
- 获取博客信息并加载到编辑器中
- 为提交按钮绑定提交方法
第一步,加载editor.md编辑器。
js代码:
var editor;
function initEdit(md){
// 编辑器设置
editor = editormd("editorDiv", {
// 这里的尺寸必须在这里设置. 设置样式会被 editormd 自动覆盖掉.
width: "100%",
// 高度 100% 意思是和父元素一样高. 要在父元素的基础上去掉标题编辑区的高度
height: "calc(100% - 50px)",
// 编辑器中的初始内容
markdown: md,
// 指定 editor.md 依赖的插件路径
path: "editor.md/lib/",
saveHTMLToTextarea: true //
});
}
html标签:
<!-- 创建编辑器标签 -->
<div id="editorDiv">
<textarea id="editor-markdown" style="display:none;"></textarea>
</div>
第二步 ,获取博客信息并加载到编辑器中。
js代码:
//3.从服务器得到博客信息,加载进编辑页
jQuery.ajax({
url:"blog/edit"+location.search,
type:"GET",
success:function (res){
if(res.code == 200){
var blog = res.data;
jQuery("#title").val(blog.title);
initEdit(blog.content);
}else {
alert("系统错误"+res.msg);
}
}
})
- 使用query string来传输数据。
- 调用initEdit方法来加载正文到编辑器中
第三步,为提交按钮绑定提交方法。
html标签,按钮绑定提交方法:
<div class="title">
<input type="text" placeholder="在这里写下文章标题">
<button onclick="mysub()">发布文章</button>
</div>
js代码。定义mysub()方法,先获取标题和正文,检验无误后发送到后端:
function mysub(){
//1.获取标题和正文
var title = jQuery("#title");
var content = jQuery("#editor-markdown");
//2.参数检验
if(title.val() != title.val().trim()){
alert("标题不能使用空格");
title.focus();
return;
}
if(title.val() == null || title.val().length==0){
alert("标题不能为空");
title.focus();
return;
}
//3.发送请求到服务器
jQuery.ajax({
url:"/blog/update",
type:"POST",
data:{
"title":title.val(),
"content":content.val(),
"id":new URLSearchParams(location.search).get("id")
},
success:function (res){
if(res.code == 200){
alert("博客修改成功") ;
location.href="myblog_list.html";
}else{
alert("系统错误: "+res.msg);
}
}
})
}
•后端开发
- 返回博客信息
- 修改博客信息
第一步,返回博客信息。
BlogController:
@RequestMapping("/edit")
public ResultAjax blogEdit(Integer id){
//1.参数检验
if(id == null || id <= 0){
return ResultAjax.fail(ResultCode.FAILED_PARAMS_VALIDATE,"参数异常");
}
//2.获取博客对象并返回
Blog blog = blogService.selectOne(id);
return ResultAjax.succ(blog);
}
BlogService和BlogMapper的代码之前写过,就不再重复展示。
第二步,修改博客信息。
BlogController:
@RequestMapping("/update")
public ResultAjax blogUpdate(Blog blog,HttpServletRequest request){
//1.参数检验
if(blog.getTitle() == null || !StringUtils.hasLength(blog.getTitle())
|| blog.getContent() == null || !StringUtils.hasLength(blog.getContent())
|| blog.getId() <= 0){
return ResultAjax.fail(ResultCode.FAILED_PARAMS_VALIDATE,"参数异常");
}
//2.得到当前登录用户
User user = SessionUtils.getUser(request);
if(user == null){
return ResultAjax.fail(ResultCode.FAILED_USER_EXISTS,"账号异常");
}
//3.补充blog属性
blog.setUpdateTime(LocalDateTime.now());
blog.setUid(user.getId());//把当前登录用户的id加入前端传来的blog里,送到sql那里去进行归属检验
//4..判断文章归属,并执行修改SQL
//5..返回响应给前端
return ResultAjax.succ(blogService.update(blog));
}
- 为什么需要用户id呢, 明明之前存过uid:这是一道检验,检验是否是当前登录用户的操作。因为前端的mysub()里的ajax请求是可以伪造的,通过Postman或浏览器的控制台等进行一些违规操作。
通过浏览器自带的控制台调用mysub方法。如果方法有参数的话,那就可以伪造参数。
直接通过Postman伪造ajax请求,如果不对当前操作的用户身份进行检验,那就可以直接篡改博客数据。
BlogService:
public int update(Blog blog){
return blogMapper.update(blog);
}
BlogMapper:
@Update("update blog set title=#{title},content=#{content},updateTime=#{updateTime}
where id=#{id} and uid=#{uid}")
int update(Blog blog);
- uid=#{uid}:检验用户身份
▪6.8 删除博客
•前后端约定
URL:/blog/delete
type:POST
Content-Type: application/json; charset=utf-8
request:id(博客id)
response:Result(null)
•前端开发(myblog_list.html)
- 绑定触发方法
- 定义请求方法del()
第一步,绑定触发方法。
html标签,在构建博客列表时绑定了触发方法:
blogList += '<a href="javascript:del('+blog.id+')" class="detail">删除 >></a>'
第二步,定义请求方法del()。
del():
function del(id){
//4.1.确认"删除"
if(!confirm("确定删除此博客?")){
return;
}
//4.2.发送请求
jQuery.ajax({
url: "/blog/delete",
type: "POST",
data:{
"id":id
},
success:function (res){
if(res.code==200){
alert("删除成功");
location.href="myblog_list.html";
}else {
alert("系统异常"+res.msg);
}
}
})
}
- 参数id:在构建博客列表的时候就已经预设。
- confirm():与alert一样弹出提示框,只不过这个提示框是有确认和取消选项,点击确认则返回true,反之false。
•后端开发
BlogController:
@RequestMapping("/delete")
public ResultAjax blogDelete(Integer id,HttpServletRequest request){
//1.参数校验
if(id == null || id <= 0){
return ResultAjax.fail(ResultCode.FAILED_PARAMS_VALIDATE,"参数异常");
}
//2.得到当前登录用户用户检验用户身份
User user = SessionUtils.getUser(request);
if(user == null){
return ResultAjax.fail(ResultCode.FAILED_USER_EXISTS,"账号异常");
}
//3.删除博客返回响应给前端
return ResultAjax.succ(blogService.delete(id,user.getId()));
}
- 与前面的修改一样,要进行用户身份检验。
BlogService:
public int delete(int id,int uid){
return blogMapper.delete(id,uid);
}
BlogMapper:
@Update("update blog set state=1 where id=#{id} and uid=#{uid}")
int delete(@Param("id")int id,@Param("uid")int uid);
- 前面说过,博客删除采取的是“逻辑删除”,将状态码改为1就行。
▪6.9 注销(退出登录)
这里的注销不是指销毁账户,而是退出登录。这个我们通过删除session就能实现。
•前后端约定
URL:/user/logout
type:POST
Content-Type: application/json; charset=utf-8
request:null
response:Result(null)
通过Cookie和Session来获得用户信息。
•前端开发(logout.js)
- 定义注销方法
- 多页面引入注销方法
第一步,定义注销方法。要知道,注销功能是放在导航栏上的,很多界面都有这个功能;所以,我们在目录“js”下创建一个js文件“logout.js”,在此文件里定义注销方法。
logout():
function logout(){
if(!confirm("退出当前账户?")){
return;
}
jQuery.ajax({
url:"/user/logout",
type: "POST",
success:function (res){
if(res.code == 200){
location.href="/login.html";
}else {
alert("系统异常: "+ res.msg);
}
}
})
}
第二步,多页面引入注销方法。除了“login.html”和"reg.html"外,所有的标签都要引入“logout.js"并给注销标签绑定此方法。
引入js:
<script src="/js/logout.js"></script>
绑定方法:
<div class="nav">
<img src="img/logo2.jpg" alt="">
<span class="title">我的博客系统</span>
<!-- 用来占据中间位置 -->
<span class="spacer"></span>
<a href="blog_list.html">主页</a>
<a href="blog_add.html">编辑</a>
//此处绑定
<a href="javascript:logout()">注销</a>
</div>
•后端开发
UserController:
@RequestMapping("/logout")
public ResultAjax logout(HttpServletRequest request){
//1.得到当前登录会话
HttpSession session = request.getSession(false);
if(session == null || session.getAttribute(AppVar.SESSION_USER) ==null){
return ResultAjax.succ("已退出会话");
}
//2.置空会话的value
session.setAttribute(AppVar.SESSION_USER,null);
//3.返回响应结果给前端
return ResultAjax.succ("退出成功");
}
- 无需操作数据库,所以没有dao和service。
- 将会话里保存的User对象给置空,就完成了对用户的注销
▪6.10 登录拦截器
•登录拦截器的介绍
在进入网站的各个界面时,我们都要检查一下当前用户的登录状况。我们为什么要这样做呢?假如用户打开了两个界面:我的博客列表和主页博客列表。用户在 “我的博客列表页” 退出登录后用户仍可以通过未关闭的“博客列表”去操作用户的博客。这是不合理的,明明用户已经退出登录了,此时其它界面不能再操作用户的博客.
所以我们就需要 “检查登录状态”。除了注册和登录界面外,其它所有的界面在加载时都要有“检查登录状态”这一步骤。我们可以对这些界面进行统一处理,设置登录拦截器。
•登录拦截器的实现
- 定义登录拦截器
- 实现前置处理方法
- 将登录拦截器配置到项目中
- 设置拦截的注册表
第一步,定义登录拦截器。
我们在目录config下创建一个LoginInterceptor类(登录拦截器)并实现接口HandlerInterceptor(拦截处理器)。给这个类打上注解@Component,然后重写方法preHandle()。
第二步,实现前置处理方法。
- 删除preHandle()的return 语句
- 这方法有三个参数,我们认识其中的两个参数即可(请求,响应)
- 这个方法的返回值类型是boolean,我们返回true就代表着验证通过,返回false就代表不通过被拦截。
通过检查是否存在此请求的session来判断当前是否是”已登录“的状态。
@Component
public class LoginInterceptor implements HandlerInterceptor {
/*------------------------------------------------------------------------------------------------------------------
* @Desc : 定义前置方法,即定义拦截器
* @Param : [request, response, handler]
* @Return: boolean
**/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1.得到当前请求会话
HttpSession session = request.getSession(false);
//2.检验会话是否存在,则返回true,表示可以通过
if(session != null && session.getAttribute(AppVar.SESSION_USER) != null){
return true;
}
//3.不存在则跳转至登录界面,并返回false,表示不可通过
response.sendRedirect("login.html");
return false;
}
}
第三步,将登录拦截器配置到项目中并设置拦截的注册表。
我们定义好了一个登录拦截器,接下来就是要将他配置到项目当中。我们在目录“config”下定义一个AppConfig类(项目配置)并实现接口WebMvcConfigurer。添加注解@Configuration,并重写addInterceptors()方法。
@Configuration:标识这个类是一个配置类,用于配置应用程序的相关设置。
WebMvcConfigurer接口:是Spring MVC框架提供的一个配置接口,用于自定义和扩展Spring MVC的配置。该接口定义了一系列方法,可以在应用程序中进行自定义配置,包括拦截器、视图解析器、资源处理器、消息转换器等。
@Configuration
public class AppConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
//......
}
}
- 这个方法无返回值,参数是一个登陆拦截器的注册表,可以通过这个参数来设置拦截器的各项属性。
- 我们这里只设置三个属性:要加入的拦截器对象是什么?拦截那些URL?不拦截那些URL?代码也很简单,如下
@Configuration
public class AppConfig implements WebMvcConfigurer {
//注入自定义的登录拦截器
@Autowired
LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//添加自定义登录拦截器
registry.addInterceptor(loginInterceptor)
//添加要拦截的url,“/**”表示拦截所有
.addPathPatterns("/**")
//添加不拦截的url
.excludePathPatterns("/user/reg") //注册请求
.excludePathPatterns("/user/login") //登录请求
.excludePathPatterns("/login.html") //登录界面
.excludePathPatterns("/reg.html") //注册界面
.excludePathPatterns("/css/*") //所有的css文件
.excludePathPatterns("/js/*") //所有的js文件
.excludePathPatterns("/editor.md/*")//editor.md编辑器
.excludePathPatterns("/img/*"); //所有的图片文件
}
}
- @Autowired注解:用于自动注入LoginInterceptor对象,即将LoginInterceptor作为一个Bean注入到AppConfig中。
- addInterceptors方法:重写了WebMvcConfigurer接口中的addInterceptors方法,用于添加拦截器。
- registry.addInterceptor(loginInterceptor):添加自定义的登录拦截器loginInterceptor。
- .addPathPatterns("/**"):添加要拦截的URL路径,这里使用"/**"表示拦截所有路径。
- .excludePathPatterns():添加不需要拦截的URL路径,这些路径不会被登录拦截器拦截。
拦截白名单 :可能有人会好奇,为什么要“拦截所有后再添加白名单”。要知道,来自前端的URL请求是千奇百怪的,别人可以通过“Postman”等软件来自定义构建URL访问服务器,而不是只通过网站来访问服务器,这些请求很多都是无用或者恶意的,所以我们才要拦截所有URL,然后设置可访问的白名单。
▮配置项目至服务器
服务器推荐使用云服务器,比如阿里云或腾讯云。这些云服务器都有学生优惠,基本上都是打一二折或直接免费,自己可以去找一下相关的活动。
接下来开始配置,相关操作有:
- 购买云服务器
- 下载Xshell并连接云服务器
- 配置linux服务器
- 项目打包
- 在云服务器启动项目
- 配置服务器的安全组
第一步,购买云服务器。
以阿里云为例,购买“轻量应用服务器”,选择linux操作系统。
- 实例类型:就选服务器实例
- 地域:选国内的服务器
- 镜像选择:系统镜像-CentOS-版本选择7.x
- 套餐配置:自选,通常是最低配
- 数据盘:选择最小的20G
- 购买时长:自选
- 购买数量:注意哈,你买两个一月是给你两个时长为一月的云服务器,而不是给你一个时长为两月的云服务器
CentOS(Community Enterprise Operating System)是一种基于Linux的开源操作系统,它是以Red Hat Enterprise Linux(RHEL)为基础构建的。CentOS提供了一个稳定、安全且可靠的操作系统平台,适用于服务器环境和企业级应用。
现在,就可以在“工作台”看到自己购买的云服务器了,要记下服务器的公网IP
接着点击“重置密码”,自行设置,要记住你设置的账号密码
第二步,下载Xshell并连接云服务器。
Xshell是一款功能强大的SSH(Secure Shell)客户端软件,用于远程登录和管理Linux/Unix服务器。它提供了一个图形化界面,使用户可以通过SSH协议安全地连接到远程服务器,并执行命令、传输文件等操作。
下载免费版的Xshell:家庭/学校免费 - NetSarang Website (xshell.com)
安装好后启动Xshell连接云服务器
输入用户名和密码
取消勾选X11,不取消的话可能会很卡。
配置好后,点击连接,当Xshell里出现以下时,则说明连接成功
第三步,配置linux服务器。
我们这是一个java程序,自然要先安装jdk和mySQL。一般情况下,我们还要安装Tomcat,但SpringBoot项目内置了Tomcat,所以我们就不用再安装了。
在linux中,复制的快捷键是“Ctrl+insert”,粘贴是“Shift+insert”。
•安装JDK8,通过yum来安装:
yum install java-1.8.0-openjdk-devel.x86_64
选择y,然后等待下载就行
•安装MySQL,请参考此文章进行操作,完全照着此文章的来就行:CentOS 7 通过 yum 安装 MariaDB - 知乎 (zhihu.com)
MariaDB是一个开源的关系型数据库管理系统(RDBMS),它是MySQL数据库的一个分支。它由MySQL的原始开发者创建,并且与MySQL兼容,因此可以无缝替代MySQL使用。
当安装好,进行到如图代码时,将之前保存的db.sql里的sql语句复制进去执行。
最终效果如此
至此,数据库已经配置完成,“Ctrl+D”退出数据库。
第四步,项目打包。
首先,修改配置文件中的数据库配置,将密码置空,因为在云服务器上的数据库没有设置密码
spring.datasource.password=
接着, 使用maven自带的插件打包
然后,在target目录下找到jar包
至此,打包完成。
第五步,在云服务器启动项目。
首先,yum下载lrzsz:
yum install lrzsz
lrzsz是一组用于在Linux和Unix系统上进行文件传输的命令行工具。它包括以下两个工具:
rz:rz命令用于从本地计算机向远程计算机上传文件。它允许你通过终端窗口选择本地文件并将其传输到远程计算机。rz命令会在远程计算机上启动ZModem协议,以便与本地计算机进行文件传输。
sz:sz命令用于从远程计算机向本地计算机下载文件。它允许你通过终端窗口选择远程文件并将其传输到本地计算机。sz命令也使用ZModem协议进行文件传输。
接着,将jar包拖进xshell的窗口,实现传输,传输成功后用‘命令“ll”查看。
然后,使用命令在后台启动项目
nohup java -jar blog_system-0.0.1-SNAPSHOT.jar &
最后,显示如图就代表启动项目成功
第六步,配置服务器的安全组。
上述操作完成后,还不能访问我们云服务器上的项目。因为云服务器的安全组还未配置,防火墙还未给外界开放端口。
首先在阿里云的控制台找到:云服务器实例 - 防火墙(安全组)- 添加规则
在“入方向”点击“手动添加”,填入如图
- 注意,“端口范围”添的是你设置的端口,如果你没设置,那默认就是8080。我这里的58080是我自己设置过的端口
▮至此,项目已经结束。
在浏览器输入“你的公网IP/项目端口号/login.html”,就可以访问到你的项目了,在电脑和手机的浏览器上都可以,在你自己的设备和别人的设备上都可以访问到。
---------------分----------割-----------线----------------------------------------------------------------------
▮后续分支
▪上一节点