头条博文 (SSM)

引言

本次项目用到的技术

后端:Spring Boot,Spring MVC,Spring AOP,MyBatis
数据库:MySQL
前端:HTML,CSS,JavaScript,JS-WebAPI,jQuery
测试:IDEA 单元测试,Chrome,Fiddler,Postman
协议:HTTP

本次项目的业务流程

1. 搭建项目环境

2. 设计数据库,并根据数据库设计实体类

3. 做好 Mapper 接口和 xml 文件之间操作数据库的配合

4. 定好前后端交互的思路

5. 实现后端的 Service 层,Contoller 层

6. 编辑前后端代码,实现页面

7. 优化项目

一、搭建项目环境

1. 创建一个 Spring MVC 项目

2. 创建好配置文件( 开发环境 / 运行环境)

连接数据库、开启 MyBatis SQL 打印、配置 " xml 文件 " 保存路径

1-1

3. 实现项目的分层

Controller 作为控制层,用来与前端交互;Service 作为服务层,用来调用接口并处理中间代码;Mapper 作为持久层,用来和数据库交互。

model 包用来放置实体类,common 包用来封装公共代码。

1-2

4. 将纯前端设计的代码放入【resource / static】目录下,并对【target】目录进行删除操作。

注意: 如果不删除【target】目录,可能会出现前端代码不会加载到 JVM 中,这样就会导致 JVM 找不到前端代码,说白了,就是因为文件缓存的问题。

1-3

5. 运行框架提供的 " DemoApplication " 启动类,查看是否无误。

二、设计数据库,并根据数据库设计实体类

通过自己写的 sql 语句,往 MySQL 数据库中,插入【blog 表】、【user 表】

【blog 表】 预期用来存储博客的信息 ( 标题、内容、发布时间 )
【user 表】预期用来存储用户的信息 ( 用户账号、用户密码 )

create database if not exists practice_blog;

use practice_blog;

-- 创建博客表
drop table if exists blog;

create table blog(
    blogID int primary key auto_increment,
    title varchar(50),
    content mediumtext,
    postTime datetime default now(),
    userID int
);

-- 创建用户表
drop table if exists user;

create table user(
    userID int primary key auto_increment,
    username varchar(50) unique,
    password varchar(50)
);

insert into blog values(null, 'Jack 的博客', '中秋快乐,合家团圆!', '2020-08-15 08:20:05', 1);

insert into blog(blogID, title, content, userID) values(null, '莉莉 的博客', 'C++, Java, Python 基础语法', 3);

insert into blog(blogID, title, content, userID) values(null, 'Rose 的博客', '好好学习,天天向上', 2);

insert into blog(blogID, title, content, userID) values(null, '李明 的博客', '坚持锻炼,保持好身体!', 4);


insert into user values(null, 'Jack', '123');

insert into user values(null, 'Rose', '321');

insert into user values(null, '莉莉', '456');

insert into user values(null, '李明', '789');

1-4

Blog 类:

@Data
public class Blog {
    private int blogID;
    private String title;
    private String content;
    private String postTime;
    private int userID;
    private int isAuthor; // 数据库不存储,用于业务逻辑
}

User 类:

@Data
public class User {
    private int userID;
    private String username;
    private String password;
    private int blogCount; // 数据库不存储,用于业务逻辑
}

三、实现 Mapper 接口和 xml 文件

实现 Mapper 接口和 xml 文件,明确增删改查数据库的要求。

BlogMapper 接口 和 " BlogMapper.xml " 文件

1-5

UserMapper 接口 和 " UserMapper.xml " 文件

1-6

四、 定好前后端交互的思路

实现每个页面的思想

五个页面:博客注册页、博客列表页、博客内容页、博客登录页、博客编辑页

博客列表页、博客内容页是通过 ajax 发送的 HTTP 请求,之后在服务器端计算响应的。而注册页、登录页、编辑页,是通过 form 表单的形式来构造 HTTP请求。

必须明确:

  • 列表页和内容页是通过 浏览器输入 URL 路径这种形式来构造 HTTP 请求的。这种方式,绝大多数情况下,都是一个 GET 请求。

  • 登录页和编辑页是通过 form 表单的形式构造 HTTP 请求的,一般和 input 标签相关的提交按钮,都是 POST 方法。

1. 博客注册页

9-1

2. 博客列表页

9-2

3. 博客内容页

9-3

4. 博客登录页

9-4

判定用户有没有登录

9-5

注销操作

9-6

显示用户/作者信息

9-7

5. 博客编辑页

9-8

删除博客功能

9-9

五、实现后端的 Service 层,Contoller 层

Blog 对应的是博客表,关于 Blog 的类,都是操作博客表。

客户端 => BlogController => BlogService => BlogMapper => xml 文件 => 数据库

User 对应的是用户表,关于 User 的类,都是操作用户表。

客户端 => UserController => UserService => UserMapper => xml 文件 => 数据库

1-7

六、编辑前后端代码,实现页面

1. 博客注册页

(1) 作用:博客注册页是用于实现用户注册的页面,它不直接出现在用户面前,而是通过点击登录页面的【注册账号】这个链接,来进行跳转。

(2) 约定 POST 请求 的路径:" /register"

(3) 前端代码:通过 form 表单进行 HTTP 请求的提交,在提交的过程中,需要带上 【username】 、【password1】、【password2】这三个参数,以便于服务器端进行验证。

(4) 单独准备一个 UserVerify 实体类,用于接收前端的这三个参数。

@Data
public class UserVerify {
    private String username;
    private String password1;
    private String password2;
}

(5) 服务器端代码:创建一个 register 方法放在 UserController 类中,来实现 HTTP 响应,并往数据库的 user 表中插入新用户。

首先我们应该做的,就是进行判空操作。

/**
 * 1. 注册用户
 */
@ResponseBody
@RequestMapping("/register")
public String register(UserVerify userVerify) {

    // 判空操作
    if (!StringUtils.hasLength(userVerify.getUsername())) {
        return "<h3> 您未输入用户名 </h3>" + "<a href='javascript:history.go(-1);'>返回</a>";
    }

    if (!StringUtils.hasLength(userVerify.getPassword1())) {
        return "<h3> 您未输入密码 </h3>" + "<a href='javascript:history.go(-1);'>返回</a>";
    }
    ......
    ......
    ......
}

(6) 注意事项:

① 注册的用户名与数据库中的用户名不能重合。
② 重复输入一次密码,以此保障注册的密码不会失败。
③ 注册完成后,应该给用户一个提示,是否需要进行跳转登录页。

展示效果:

1-8

2. 博客列表页

(1) 作用:博客列表页主要用来展示所有博客的摘要

(2) 约定 GET 请求 的路径:" /blog_list"

(3) 前端代码:通过 ajax 来构造请求,思想:先按照纯前端代码创建相应的节点,之后再挂在 DOM 树上。

(4) 服务器端代码:创建一个 displayBlogs 方法放在 BlogController 类中,来实现 HTTP 响应,只要用户登录成功后,就会展示博客列表页。

(5) 由于在博客列表页,我们只是展示所有博客的摘要,所以在服务层中,通过字符串截取的方式,来控制页面为用户展示的博客字数。这样一来,控制层在返回数据的时候,只是一个简化的 json 数据了。

@Service
public class BlogService {

    @Resource
    private BlogMapper blogMapper;

    // 1. 查询所有博客
    // 在这里的逻辑,我们将展示的摘要进行了简化
    public List<Blog> getAllBlogs(){
        List<Blog> blogs1 = blogMapper.getAllBlogs();
        List<Blog> blogs2 = new ArrayList<>();
        for (int i = 0; i < blogs1.toArray().length; i++) {
            Blog blog = new Blog();
            blog.setBlogID(blogs1.get(i).getBlogID());
            blog.setTitle(blogs1.get(i).getTitle());

            // 由于这里显示的是博客内容的摘要,所以,
            // 我们约定: 当字符的数量大于 10个的时候,我们通过截取字符串的形式,来放入 blog 对象中
            String content = blogs1.get(i).getContent();
            if (content.length() > 10) {
                content = content.substring(0, 10) + ".......";
            }
            blog.setContent( content );
            blog.setPostTime(blogs1.get(i).getPostTime());
            blog.setUserID(blogs1.get(i).getUserID());
            blogs2.add(blog);
        }
        return blogs2;
    }
}

抓包结果:( HTTP 响应的正文 )

1-9

(6) 由于页面的内容过长,这就可能导致溢出页面,或者说,内容溢出版心,此时,我们就可以通过设置 CSS 文件,并引入 HTML,来弥补这一缺点。

如下代码:当内容溢出页面的时候,就会自动设置滑动栏,这很实用。

overflow: auto;

展示效果:

2-1

3. 博客内容页

(1) 作用:博客内容页用来展示某个用户的某篇文章

(2) 约定 GET 请求 的路径:" /blog_content "

(3) 前端代码:通过 ajax 来构造请求,思想:先按照纯前端代码创建相应的节点,之后再挂在 DOM 树上。

(4) 服务器端代码:创建一个 displayUserBlog 方法放在 BlogController 类中,来实现 HTTP 响应。

(5) 博客列表页和博客内容页之间的配合:只要用户在博客列表页点击【查看全文】按钮的时候,就会跳转到内容页。

此功能在博客列表页中,通过 blogID 参数来实现指定跳转。在 ajax 中,我们利用 JS-WebAPI 来直接拼接节点。

// 跳转博客内容页的链接
let linkA = document.createElement('a');
linkA.innerHTML = '查看全文 &gt;&gt';
linkA.href = 'blog_content.html?blogID=' + blog.blogID;

展示效果:

2-2

4. 博客登录页

(1) 作用:博客登录页是用于实现用户登录的页面,它可以判断用户名和密码各自是否正确。

(2) 约定 POST 请求 的路径:" /login"
我们打开写死的博客登录页面,点击【登录】,浏览器自然就发送了 POST 请求,因为我们将【登录】放在了 form 表单下,通过 input 标签实现的。

(3) 前端代码:通过 form 表单进行 HTTP 请求的提交,在提交的过程中,需要带上 【username】 和【password】这两个参数,以便于服务器端进行验证。

(4) 服务器端代码:创建一个 login 方法放在 UserController 类中,来实现 HTTP 响应。只要用户登录成功后,就会跳转到博客列表页。

(5) 博客登录页这里,并不需要展示什么,所以,此页面也无需通过 ajax 的方式进行构造 HTTP 请求,直接通过 form 表单的形式提交请求,这会让整个登录流程变得更加简单。此外,这里的 if 语句进行判断,我们应该考虑到所有的意外情况与不合理的情况

(6) 登录页面的时候,我们可以利用 session 机制。

① 若登录成功,就将 session 会话创建出来,并将当前登录的用户,以 Java 对象的方式放入会话 session 中,后面再次使用博客系统的时候,服务器就能够通过会话中的对象,知晓当前登录者是哪个用户。

② 若登录失败,就不将 session 会话创建出来。我们也可以反过来思考:若 session 会话未被创建出来,那就意味着登录失败。

③ session 机制就和之前的 ServletContext 机制差不多,我们可以将其想象成一个冰箱,随拿随放。

2-3

(1) 判定登录与注销操作

鉴于上面的思想,我们不仅可以用当前的 session 会话机制判断博客列表页,也可以用它来判断博客内容页,博客发布页的用户登录情况。此外,通过会话机制,也能够应用于注销操作。

所以,我们对上面的代码改进一下,封装一个 Check 类,让上面所说的页面都能够通过这个 Check 类来判断当前用户是否登录了。

public class Check {
    public static User CheckLogin(HttpServletRequest req) {
        HttpSession httpSession = req.getSession(false);
        if (httpSession == null) {
            // 会话未创建出来,意味着用户未登录
            return null;
        }
        User loginUser = (User) httpSession.getAttribute("user");
        if (loginUser == null) {
            // 就算会话创建出来了,但是里面没有对象,也意味着用户未登录
            return null;
        }
        return loginUser;
    }
}

注意 if 语句的顺序,为什么先要判定 httpSession 存在与否呢?这是为了防止空指针异常。【 若 httpSession 为 null,那么,httpSession.getAttribute(“user”) 这行代码就会出现空指针异常 】

这很好理解:当冰箱都没有的时候,我们怎么从冰箱拿东西呢?所以说,一定是先有冰箱了,我们才能从里面拿东西,才能往里面放东西。

2-4

注意事项:

这里需要注意一件事情,当用户没有登录的时候,就访问了博客列表页或博客内容页,应该马上让 HTML 页面再次重定向至登录页面。而在当前的 ajax 方式,我们让前端去处理重定向操作了。这里并不建议在后端进行重定向操作,为什么呢?

因为,使用 ajax 这种方式构造 HTTP 请求的时候,服务器端只负责在 HTTP 响应中写入数据,比方说:它应该写入状态码、正文 body、body 格式等等…而前端就应该根据拿到的 HTTP 响应的数据,决定实现什么样的前端页面与样式,所以说,在这里,所有展现给用户看的内容和格式,都应该由前端负责。 我们在后端设置一个 403 这样的权限状态码,就能够告知 ajax 的 error 函数,进行重定向了。

然而,如果我们通过服务器端的重定向操作来实现也不是不可以,但会出现一定的问题,例如:重定向之后的页面,展示的效果可能不是一个 HTML 页面,而是一些未经处理的文本数据。也就是说,我们在方法上已经加上了一个 " @ResponseBody " 注解,当登录不成功的时候,我们需要返回一些数据。 实际上,后端返回 json 数据的场景居于大多数,真正处理用户视觉效果的,全部是前端。

① 正确的前后端判定代码

服务器端自定义写入状态码:

403 这个状态码表示的是访问被拒绝,页面通常需要用户具有一定的权限才能访问。
(403 Forbidden)

// 验证用户有没有登录
User loginUser = Check.CheckLogin(request);
if (loginUser == null) {
    System.out.println("当前用户未登录");
    // 若登录不成功,设置状态码为 403,前端的回调 error 函数就能够直接重定向页面了
    response.setStatus(403);
    return null;
}

2-5

前端重定向操作:

error: function() {
    // 此处表示前端形式的重定向, 相当于后端的 resp.sendRedirect()方法
    location.assign('blog_login.html');
}

我们前面提到,error 这个回调函数,表示的是:当发送 HTTP 请求失败的时候,就会被浏览器执行。显然,它会执行了除 【200】 额外的一些状态码,所以,在 error 中的业务逻辑,我们就可以实现一个重定向操作。

(2) 登录用户与文章作者的数据信息

当用户登录成功后,首先跳转到的是博客列表页,那么,博客列表页就应该显示当前用户的一些信息:头像、昵称、文章总数…接着,若用户点击【查看全文】后,就可以跳转到某一篇博客全文,此时,页面显示的应该是作者信息。

鉴于此,

(1) 将博客列表页展示当前登录者的用户信息
(2) 将博客内容页展示文章作者的用户信息

这里就不再展示代码了,它的思想其实和上面的博客内容页的 ajax 思想是相同的,先选中 DOM 树的节点,再对其标签的内容进行更改。同样地,后端应该利用 blogID 这个参数和 session 会话机制,来确定谁是作者,谁是当前登录的用户。

展示效果:

2-6

5. 博客编辑页

(1) 作用:博客编辑页是实现让用户用来撰写博客的,它可以在浏览器上进行提交,而后,服务器经过一些处理,让博客的一些数据放入数据库中。

(2) 约定 POST 请求 的路径:" /blog_writing"
我们打开写死的博客编辑页面,点击【发布文章】,浏览器自然就发送了 POST 请求,因为我们将【发布文章】放在了 form 表单下,通过 input 标签实现的。

(3) 前端代码:通过 form 表单进行 HTTP 请求的提交,在提交的过程中,需要带上 【title】 和【content】这两个参数,以便于服务器端进行验证。
然而,这里的代码较为少见,因为当前是根据 jQuery 提供的依赖,才会有这个编辑页面的展示效果,所以,写死的 HTML页面同时需要配合 JS 代码,并且需要基于 【editor.md】的一些写法规则

(4) 服务器端代码:创建一个 blogWriting 方法放在 BlogController 类中,来实现 HTTP 响应,并传入博客到数据库中。由于前端只传来 " 标题 " 和 " 正文 " 这两个参数,所以,userID 需要我们通过获取登录者的信息才能拿到。

(5) 博客编辑页这里,和博客登录页是一样的思路,并不需要展示什么,所以,此页面并不需要基于 ajax 构造请求。此外,这里的 if 语句进行判断,我们应该考虑到所有的意外情况与不合理的情况。

(6) 由于编辑博客的时候,它是依据 markdown 的语法规则,可以让一些字体变成我们想要的格式,例如:一级标题,二级标题,加粗,删除线等等…
我们当前的 CSDN 就是拥有这样的规则。

而在之前的博客列表页显示博客的时候,它是一种素的、原始的文字。就算经过博客发布了,但展示给用户看的时候,并没有经过博客编辑器处理,所以,同样地,我们为博客列表页引入 【editor.md】这样的依赖,并通过 JS 代码,让文字变成处理后的结果。

实现思想:

markdown 的原始内容,放在上面的 div 中,我们可以将这个 div 中的内容通过 markdownToHTML 函数进行替换。

展示效果:

2-7

展示效果:

2-8

删除博客功能

删除博客,通过前端的 a 标签构造 DELETE 请求,之后在服务器端进行处理。

思想:在博客内容页,若当前登录用户和博客作者是同一个人,即可以删除;若不是同一个人,则不能删除,只能观看。

七、优化项目

对项目的一些功能进行统一处理,如:统一用户的验证问题、统一的异常处理。

2-9

1. 使用 Spring 拦截器来进行用户验证

自定义一个 LoginInterceptor 登录拦截器,在 preHandle 方法中,若返回 true,表示用户已经登录,则前端可以继续访问其他页面;若返回 false,表示用户还未登录,即拦截成功,此时我们应该重定向至登录页面。

@Component
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HttpSession httpSession = request.getSession(false);
        if (httpSession != null && httpSession.getAttribute("user") != null) {
            // 表示用户已经登录
            return true;
        }
        // 代码走到这里,说明用户还未登录
        response.sendRedirect("blog_login.html");
        return false;
    }
}

添加拦截器至整个 Spring Boot 项目之中,并设置拦截规则,我们期望不对注册页面、登录页面、以及一些静态文件进行拦截。

@Configuration
public class AddConfiguration implements WebMvcConfigurer {

    @Resource
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor).
                addPathPatterns("/**"). // 拦截所有 URL 路径
                excludePathPatterns("/login"). // 排除登录路径
                excludePathPatterns("/blog_login.html"). // 排除登录的静态页面
                excludePathPatterns("/**/*.js"). // 排除所有的 js 文件
                excludePathPatterns("/**/*.css").
                excludePathPatterns("/**/*.md").
                excludePathPatterns("/register").
                excludePathPatterns("/blog_register.html").
                excludePathPatterns("/**/*.png"). // 排除所有的 png 图片
                excludePathPatterns("/**/*.jpg");
    }
}

2. 统一异常处理

自定义一个返回的对象,供前端拿到数据,并约定若 state 为 -1,那么就重定向至一个写死的异常页面,供用户观看。

后端 (统一异常处理):

@RestControllerAdvice
public class MyExceptionAdvice {

    @ExceptionHandler(Exception.class)
    public HashMap<String, String> ExceptionAdvice(Exception e, HttpServletResponse response) throws IOException {
        // 构造返回对象
        HashMap<String, String> result = new HashMap<>();
        result.put("state", "-1");
        result.put("data", e.getMessage());

        System.out.println("发现异常: ");
        e.printStackTrace(); // 供后端程序员发现异常位置
        return result;
    }
}

前端 (exception.html):

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>页面丢失</title>
</head>
<body>
    <h3>服务器端发现异常,正在联系后台紧急处理,请耐心等待...</h3>
    <a href='blog_list.html'>点击返回主页</a>
</body>
</html>

展示结果:

3-1

测试

测试、调试过程其实也花了我很长时间。

后端:

Spring Boot 项目提供了一个非常优秀的单元测试,它可以不污染数据库,达到方法级别的测试,这很方便程序员对于项目的某个地方进行调试。

在本次项目中,我利用单元测试,主要测试了数据库的 CURD 操作。有好几次我发现浏览器控制台总是出现 " 500 " 状态码的情况,找了后端,看了很多英文报错也找不到原因,后来逐一排查,才发现是 SQL 语句报错。当时就是利用了 SpringBootTest 找到的问题,很方便,也不影响后端代码。

前端:

前端主要通过浏览器的控制台发现问题,它的调试和我们平时使用 IDEA 终端调试代码很相似,点到哪都可以停顿。

此外,利用 Fiddler 抓包,postman 构造 HTTP 请求,也能够很好地模拟出与项目相关的前后端交互过程。这样也能够不影响代码的编辑。

总代码

3-3

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

十七ing

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值