博客系统(前后端分离)

前面已经学习如何使用Servlet实现前后端交互,现在就可以使用Servlet把之前的博客系统修改为动态页面

动态页面可以使用模板引擎实现,这是由服务器渲染:服务器来构建出完整的页面并返回给浏览器

但是现在主流的方式是使用前后端分离实现,这是由浏览器渲染:服务器不返回完整网页,只是返回必要的数据,页面利用ajax,请求服务器中的接口来获取数据,然后由页面自己构建.

image-20221019113557993

1. 准备工作

1)创建项目

2)创建目录

image-20221019115002396

3)引入依赖

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>MyBlog</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.13.3</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.30</version>
        </dependency>
    </dependencies>

</project>

web.xml

<!DOCTYPE web-app PUBLIC
        "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
        "http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
    <display-name>Archetype Created Web Application</display-name>
</web-app>

2. 数据库设计

在创建数据库时要明确要建立几个表,表中有哪些属性.完成这些操作就必须要知道我们操作的实体是什么? 对于博客系统就是博客和用户,对应着blog表和user表

1)在数据库中创建对应库和表信息

image-20221019125207611

image-20221019125219618

2)把sql代码写入db.sql文件中

原因是如果后续需要把数据库往别的机器上部署,就可以直接复用这里的代码,完成建库创表的操作

create database if not exists blog_system;

use blog_system_java104;

drop table if exists blog;
create table blog (
    blogId int primary key auto_increment,
    title varchar(1024), -- 博客标题
    content mediumtext,  -- 博客正文
    userId int,    -- 作者的 id
    postTime datetime  -- 发布时间
);

drop table if exists user;
create table user (
    userId int primary key auto_increment,
    username varchar(128) unique,
    password varchar(128)
);

3. 封装数据库操作

在web开发中典型的代码结构:MVC

M: model模型,管理和组织数据

C: controller控制器,表达业务逻辑

V: View界面/视图.对应给用户显示的页面

对应的数据库操作就需要创建model目录,在该目录下实现对数据库操作的文件编写

1)创建DBUtil类

采用懒汉模式实现和数据库建立/断开连接

package model;

import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class DBUtil {
    private static final String URL = "jdbc:mysql://127.0.0.1:3306/blog_system?characterEncoding=utf8&useSSL=false";
    private static final String USERNAME = "root";
    private static final String PASSWORD = "123456";

    // volatile保证得到一个被完全初始化的示例对象
    private static volatile DataSource dataSource = null;

    private static DataSource getDataSource() {
        // 双重锁机制使大部分请求都不会进入阻塞代码块
        if (dataSource == null) {
            synchronized (DBUtil.class) {
                if (dataSource == null) {
                    dataSource = new MysqlDataSource();
                    ((MysqlDataSource)dataSource).setUrl(URL);
                    ((MysqlDataSource)dataSource).setUser(USERNAME);
                    ((MysqlDataSource)dataSource).setPassword(PASSWORD);
                }
            }
        }
        return dataSource;
    }

    public static Connection getConnection() throws SQLException {
        return getDataSource().getConnection();
    }

    public static void close(Connection connection, PreparedStatement statement, ResultSet resultSet) {
        if (resultSet != null) {
            try {
                resultSet.close();
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }
        }
        if (statement != null) {
            try {
                statement.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (connection != null) {
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

}

2)创建User类和Blog类

把博客和用户抽象成对象,后面就可以针对这个对象实体实现相关操作

Blog类中的每一个实体对象代表blog表中的一个记录

package model;

import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.logging.SimpleFormatter;

public class Blog {
    private int blogId;
    private String title;
    private String content;
    private int useId;
    private Timestamp postTime;

    public int getBlogId() {
        return blogId;
    }

    public void setBlogId(int blogId) {
        this.blogId = blogId;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public int getUseId() {
        return useId;
    }

    public void setUseId(int useId) {
        this.useId = useId;
    }

//    public Timestamp getPostTime() {
//        return postTime;
//    }
    // 希望返回的是一个格式化好的时间(String类型),而不是时间戳
    public String getPostTime() {
        // 使用 SimpleDateFormat 来完成时间戳到格式化日期时间的转换.
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return simpleDateFormat.format(this.postTime);
    }

    public void setPostTime(Timestamp postTime) {
        this.postTime = postTime;
    }
}

User类的每一个实体对象代表User表中的一条记录

package model;

public class User {
    private int userId = 0;
    private String username = "";
    private String password = "";

    public int getUserId() {
        return userId;
    }

    public void setUserId(int userId) {
        this.userId = userId;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

3)创建BlogDao类和UserDao类

这两个类主要是针对博客表和用户表的CURD(增改删查)

Dao: Data Access Object (数据访问对象),这个只是习惯命名方式,不是强制要求

package model;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

// UserDao类对User表的操作
public class UserDao {
    // 1. 根据用户名查询用户信息 - 用户登录时
    public User selectByName(String username) {
        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;
        try{
            connection = DBUtil.getConnection();
            String sql = "select * from user where username = ?";
            statement = connection.prepareStatement(sql);
            statement.setString(1, username);
            resultSet = statement.executeQuery();
            // 因为username是unique约束,保证其唯一性
            if (resultSet.next()) {
                User user = new User();
                user.setUserId(resultSet.getInt("userId"));
                user.setUsername(resultSet.getString("username"));
                user.setPassword(resultSet.getString("password"));
                return user;
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            // 关闭连接
            DBUtil.close(connection, statement, resultSet);
        }
        return null;
    }
    // 2. 根据用户ID查询用户信息 - 在博客详情页时,根据用户id查询用户信息
    public User selectById(int userId) {
        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;
        try{
            connection = DBUtil.getConnection();
            String sql = "select * from user where userId = ?";
            statement = connection.prepareStatement(sql);
            statement.setInt(1, userId);
            resultSet = statement.executeQuery();
            // 因为username是unique约束,保证其唯一性
            if (resultSet.next()) {
                User user = new User();
                user.setUserId(resultSet.getInt("userId"));
                user.setUsername(resultSet.getString("username"));
                user.setPassword(resultSet.getString("password"));
                return user;
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            // 关闭连接
            DBUtil.close(connection, statement, resultSet);
        }
        return null;
    }

}
package model;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

// BlogDao类把对博客表的基本操作进行封装
public class BlogDao {
    // 1. 新增博客
    public void insert(Blog blog) {
        Connection connection = null;
        PreparedStatement statement = null;
        try {
            // 1) 和数据库建立连接
            connection = DBUtil.getConnection();
            // 2) 构造sql语句
            String sql = "insert into blog values(null, ?, ?, ? ,now())";
            statement = connection.prepareStatement(sql);
            statement.setString(1, blog.getTitle());
            statement.setString(2, blog.getContent());
            statement.setInt(3, blog.getUserId());
            // 3) 执行sql
            int ret = statement.executeUpdate();
            if (ret == 1) {
                System.out.println("插入成功!");
            } else {
                System.out.println("插入失败!");
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            // 4) 关闭连接
            DBUtil.close(connection, statement, null);
        }
    }
    // 2.获取所有博客显示到博客列表页
    public List<Blog> selectAll() {
        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;
        List<Blog> blogs = new ArrayList<>();
        try {
            // 1) 和数据库建立连接
            connection = DBUtil.getConnection();
            // 2) 构造sql语句
            String sql = "select * from blog";
            statement = connection.prepareStatement(sql);
            // 3) 执行sql
            resultSet = statement.executeQuery();
            // 4) 遍历获取结果集合
            while (resultSet.next()) {
                Blog blog = new Blog();
                blog.setBlogId(resultSet.getInt("blogId"));
                blog.setTitle(resultSet.getString("title"));
                // 这里只需要截取博客正文的部分信息即可
                String content = resultSet.getString("content");
                // 如果正文的长度小于100会抛异常,这里if判断处理
                if (content.length() < 100) {
                    blog.setContent(content);
                } else {
                    blog.setContent(content.substring(0, 100));
                }
                blog.setPostTime(resultSet.getTimestamp("postTime"));
                blog.setUserId(resultSet.getInt("userId"));
                blogs.add(blog);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            // 5) 关闭连接
            DBUtil.close(connection, statement, null);
        }
        return blogs;
    }

    // 3.查询博客信息
    public Blog selectOne(int blogId) {
        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;
        try {
            // 1) 和数据库建立连接
            connection = DBUtil.getConnection();
            // 2) 构造sql语句
            String sql = "select * from blog where blogId = ?";
            statement = connection.prepareStatement(sql);
            statement.setInt(1, blogId);
            // 3) 执行sql
            resultSet = statement.executeQuery();
            // 4) 遍历结果集
            if (resultSet.next()) {
                Blog blog = new Blog();
                blog.setBlogId(resultSet.getInt("blogId"));
                blog.setTitle(resultSet.getString("title"));
                blog.setContent(resultSet.getString("content"));
                blog.setPostTime(resultSet.getTimestamp("postTime"));
                blog.setUserId(resultSet.getInt("userId"));
                return blog;
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            // 5) 关闭连接
            DBUtil.close(connection, statement, null);
        }
        return null;
    }

    // 4.删除博客
    public void delete(int blogId) {
        Connection connection = null;
        PreparedStatement statement = null;
        try {
            connection = DBUtil.getConnection();
            String sql = "delete from blog where blogId = ?";
            statement = connection.prepareStatement(sql);
            statement.setInt(1, blogId);
            int ret = statement.executeUpdate();
            if (ret == 1) {
                System.out.println("删除成功!");
            } else {
                System.out.println("删除失败!");
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            DBUtil.close(connection, statement, null);
        }
    }

}

我们发现这些数据库操作代码同质化严重,在后面会使用MyBatis(当前常用的数据库操作框架),它可以根据需要执行的sql语句自动构建具体的JDBC代码.

4. 准备前端页面

1)拷贝页面:

image-20221020145942642

5. 实现博客列表

和之前写表白墙一样,实现动态页面需要下面三个步骤实现其功能

在博客列表页加载时,前端通过ajax构建Http请求向服务器获取数据,当获取成功后,ajax的success函数就会拿到这个数据解析并渲染到页面上,所有在这个过程中需要知道:

  1. 前后端交合的接口是什么?
  2. 后端如何实现从数据库中查询数据并在前端发送请求时准确的返回该信息?
  3. 前端构建怎样的请求向服务器获取对应的数据,如何把这些数据渲染到页面上?

1)约定前后端交互接口

[请求]
GET /blog

[响应]
[
    {
        blogId: 1,
        title: "第一篇博客",
        content: "博客正文",
        userId: 1,
        postTime: "2021-07-07 12:00:00"
    },
    {
        blogId: 2,
        title: "第二篇博客",
        content: "博客正文",
        userId: 1,
        postTime: "2021-07-07 12:10:00"
    },
    ...
]

2)实现后端交互接口

@WebServlet("/blog")
public class BlogServlet extends HttpServlet {
    private ObjectMapper objectMapper = new ObjectMapper();

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("application/json;charset=utf8");
        BlogDao blogDao = new BlogDao();
        List<Blog> blogs = blogDao.selectAll();
        String jsonString = objectMapper.writeValueAsString(blogs);
        resp.getWriter().write(jsonString);
    }
}

3)实现前端交互接口

    <script src="https://code.jquery.com/jquery-3.6.1.min.js"></script>
    <script>
        function getBlogs() {
            $.ajax({
                type:'get',
                url:'blog',
                success:function(body) {
                    // ajax会自动根据响应的Content-type解析body中的数据
                    let container = document.querySelector('.container-right');
                    for (let blog of body) {
                        let blogDiv = document.createElement('div');
                        blogDiv.className = 'blog';
                        // 创建博客标题
                        let titleDiv = document.createElement('div');
                        titleDiv.className = 'title';
                        titleDiv.innerHTML = blog.title;
                        blogDiv.appendChild(titleDiv);
                        // 创建日期
                        let dateDiv = document.createElement('div');
                        dateDiv.className = 'date';
                        dateDiv.innerHTML = blog.postTime;
                        blogDiv.appendChild(dateDiv);
                        // 创建摘要
                        let descDiv = document.createElement('div');
                        descDiv.className = 'desc';
                        descDiv.innerHTML = blog.content;
                        blogDiv.appendChild(descDiv);
                        // 创建查看全文按钮
                        let a = document.createElement('a');
                        a.className = 'detail';
                        a.innerHTML = '查看全文 >>';
                        // 希望根据blogId跳转对应详情页
                        a.href = 'blog_detail.html?blogId=' + blog.blogId;
                        blogDiv.appendChild(a);
                        // 把blogDiv加入外层元素
                        container.appendChild(blogDiv);
                    }
                }
            })
        }
        getBlogs();
    </script>

4)配置好Tomcat启动服务器,在浏览器中输入"http://localhost:8080/MyBlog/blog_list.html"

image-20221020142558362

但是在浏览器中和我们预期的不一样,我们需要的不是静态的页面,而是要由后端数据库中获取的数据渲染的页面

image-20221020142745157

所以先在数据库的blog表中插入数据,然后把原来的静态内容屏蔽;

image-20221020142923225

重启服务器后,进入页面后,利用Fiddler查看后端响应数据

image-20221020143314731

ajax拿到数据后就会去渲染页面:

image-20221020143419738

6. 实现博客详情页

预期效果: 点击博客列表页中的查看全文可以定位到指定文件并跳转到博客详情页,展示出博客正文.

实现步骤:

  1. 由于博客表blogId是auto_increment所以根据在url的query string中携带当前博客的blogId跳转到博客详情页.
    • image-20221020144600949
  2. 博客详情页就会根据当前的blogId向服务器发送请求获取当前的博客内容.
    • image-20221020144625140
  3. 服务器接受请求后,向数据库中获取并在响应返回给ajax

1)约定前后端交互接口

请求:
GET /blog?blogId=1

响应:
HTTP/1.1 200
Content-Type: application/json

{
	blogId:1,
	title:'131',
	content:'23131';
	userId:12;
	postTime:'2020-2-1 20:00:00'
}

2)实现服务器代码

可以利用路径/blog中有无参数,实现不同的效果

@WebServlet("/blog")
public class BlogServlet extends HttpServlet {
    private ObjectMapper objectMapper = new ObjectMapper();

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("application/json;charset=utf8");
        // 获取当前url中的blogId
        String blogId = req.getParameter("blogId");
        BlogDao blogDao = new BlogDao();
        // 1. 如果存在这个id,说明是获取博客详情页
        if (blogId != null) {
            Blog blog = blogDao.selectOne(Integer.parseInt(blogId));
            String jsonString = objectMapper.writeValueAsString(blog);
            resp.getWriter().write(jsonString);
        } else {
            // 2. 否则就是获取博客列表
            List<Blog> blogs = blogDao.selectAll();
            String jsonString = objectMapper.writeValueAsString(blogs);
            resp.getWriter().write(jsonString);
        }
    }
}

3)实现前端代码

利用ajax修改原始页面内容

<script src="https://code.jquery.com/jquery-3.6.1.min.js"></script>
<script>
    function getBlog() {
        $.ajax({
            type:'get',
            //location.search返回当前query string
            url:'blog'+location.search,
            success: function(body) {
                let h3 = document.querySelector('.blog-content h3');
                h3.innerHTML = body.title;
                let divDate = document.querySelector('.blog-content .date');
                divDate.innerHTML = body.postTime;
                // 存放在数据库中的正文是原始的markdown文本格式的数据
                // 所以在渲染到正文部分时应该使用 editor.md 渲染
                // 使用 editor.md 提供的方法把数据格式化
                // 第一个参数为div的id, 第二个参数是一个js对象
                editormd.markdownToHTML('content', {
                    markdown: body.content
                });
            }
        })
    }
    getBlog();  
</script>

记得引入editor.md 的依赖

<link rel="stylesheet" href="editor.md/css/editormd.min.css" />
<script src="js/jquery.min.js"></script>
<script src="editor.md/lib/marked.min.js"></script>
<script src="editor.md/lib/prettify.min.js"></script>
<script src="editor.md/editormd.js"></script>

4)重启服务器后访问,利用Fiddler查看响应

image-20221020152902593

点击查看全文后前端发送的请求

image-20221020153131359

服务器返回的响应

image-20221020153203571

也能够格式化数据了

image-20221020153722135

image-20221020153730798

7. 实现登录页面

实现登录功能可以使用ajax和form表单,但是通常使用的时form表单

1)约定前后端交互接口

请求 
POST /login
Content-Type; application/x-www-form-urlencoded

username=张三&password=123

响应
HTTP/1.1 302 OK
Location: blog_list.html
// 如果登录成功跳转到博客详情页
// 登录失败则则返回失败信息

2)编写后端代码

@WebServlet("/login")
public class LoginServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 1.从请求中获取用户名和密码
        String username = req.getParameter("username");
        String password = req.getParameter("password");
        // 2. 验证数据是否合法
        if (username == null || password == null || username.equals("") || password.equals("")) {
            resp.setContentType("text/html;charset=utf8");
            resp.getWriter().write("当前输入的用户名或者密码为空!");
            return;
        }
        // 3.在数据库中查询数据是否存在
        UserDao userDao = new UserDao();
        User user = userDao.selectByName(username);
        if (user == null) {
            // 用户不存在
            resp.setContentType("text/html;charset=utf8");
            resp.getWriter().write("当前输入的用户名或者密码错误!");
            return;
        }
        if (!user.getPassword().equals(password)){
            // 密码错误
            resp.setContentType("text/html;charset=utf8");
            resp.getWriter().write("您的用户名或者密码错误!");
            return;
        }
        // 4. 登录成功,创建会话
        HttpSession session = req.getSession(true);
        // 把user对象存储在session中
        session.setAttribute("user", user);
        // 5. 返回一个重定向报文,跳转到博客列表页
        resp.sendRedirect("blog_list.html");
    }
}

3)实现后端代码

<!-- 版心 -->
<div class="login-container">
    <!-- 登录框 -->
    <div class="login-dialog">
        <form action="login" method="post">
            <h3>登录</h3>
            <div class="row">
                <span>用户名</span>
                <input type="text" id ="username" name="username">
            </div>
            <div class="row">
                <span>密码</span>
                <input type="password" id="password" name="password">
            </div>
            <div class="row">
                <input type="submit" id="submit" value="提交">
            </div>
        </form>
    </div>
</div>

4)在数据库中创建一个用户,启动服务器,打开浏览器输入http://localhost:8080/MyBlog/blog_login.html

image-20221020163158139

image-20221020163217690

点击提交后前端的请求

image-20221020163748719

body中携带的信息

image-20221020163815804

成功登录后创建会话,和重定向: 302

image-20221020163944381

如果用户名是中文呢?

image-20221020164023838

响应中告诉我们密码或者用户名错误

image-20221020164203655

查看请求的body内容

image-20221020164253881

这里的出现乱码是因为url在传输时会把中文转码(urlencode),可以使用https://tool.chinaz.com/tools/urlencode.aspx该网站验证.

那么问题是出现在哪里呢?我们在服务器中打印解析得到的用户名和密码

image-20221020164813079

原因是对于中文字符,一个字符以%分割其16进制得到的

image-20221020170049413

服务器解码时不是按照utf8格式解码就会出现乱码

可以在http://mytju.com/classcode/tools/messyCodeRecover.asp中实验乱码恢复和查看字符的编码

image-20221020170353908

所以解决方案是明确请求数据的编码格式

// 告诉服务器如何解析请求
req.setCharacterEncoding("utf8");

设置好后就不会出现乱码啦

image-20221020170621793

8. 设置用户强制登录

在用户浏览博客列表页和博客详情页时,如果用户为登录,强制跳转到登录页面,使用户登录

1)约定前后端交互接口.

在登录页面中设置了用户登录成功就会创建会话:

image-20221020182639328

所以就可以使用在用户进入博客详情页/博客列表页时由浏览器发送请求,访问服务器当前是否存在会话来判断用户的登录状态,从而浏览器获取响应就可以判断是否需要跳转或者不跳转

请求
GET /login
Cookie:JSESSIONID=XXXXXXXX

响应
HTTP/1.1 200 OK       [已登录]
HTTP/1.1 403 Forbidden [未登录]

2)实现服务器代码

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    // 1.查看当前是否存在会话
    HttpSession session = req.getSession(false);
    // 2.不存在会话
    if (session == null) {
        // 未登录设置状态码403
        resp.setStatus(403);
        return;
    }
    // 3. 存在会话
    User user = (User) session.getAttribute("user");
    // 4. 但是用户未登录
    if (user == null) {
        resp.setStatus(403);
        return;
    }
    // 5. 已登录
    resp.setStatus(200);
}

3)实现前端代码

因为博客登录页面/博客详情页都需要判断,使用封装成一个单独的js文件,在函数中调用即可

function checkLogin() {
    $.ajax({
        type:'get',
        url:'login',
        success: function(body) {
            // 成功不做处理
        },
        error: function() {
            // 失败就会强行跳转到登录页面
            location.assign('login.html');
        }
    })
}

记得在博客详情页/博客列表页中引入该文件和调用

4)部署项目

登录成功后,进入博客详情页:

image-20221020190616245

第一次请求是检查当前用户是否登录

第二次请求时获取博客详情信息

第三次请求是获取第一次的响应中的状态码,决定当前是否需要跳转

9. 动态显示用户信息

我们希望在博客列表页显示当前登录用户的信息,在博客详情页中显示该博客的作者的信息

1)约定前后端交互接口

博客列表页:

发送一个ajax请求获取当前用户登录信息,然后把该信息显示到页面

请求
GET /userInfo

响应
HTTP/1.1 200 OK
{
	userId:1
	username:'admin'
}

博客列表页:

发送一个带有blogId参数的ajax请求获取当前blogId对应的作者信息

请求:
GET /userInfo?blogId=1

响应:
HTTP/1.1 200 OK
{
	userId:1,
	username:"张三"
}

2)实现后端代码

对应博客详情页和博客列表页前面已经强制用户登录,所以可以根据的会话中的用户信息和当前blogId的作者对比,动态的显示

@WebServlet("/userInfo")
public class UserInfoServlet extends HttpServlet {
    private ObjectMapper objectMapper = new ObjectMapper();

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 1. 根据会话信息获取当前用户信息
        String blogId = req.getParameter("blogId");
        HttpSession session = req.getSession(false);
        if (session == null) {
            // 会话中未存储用户信息
            resp.setStatus(403);
            resp.setContentType("text/html;charset=utf8");
            resp.getWriter().write("当前用户未登录");
            return;
        }
        User user = (User) session.getAttribute("user");
        if (user == null) {
            // 用户信息不存在
            resp.setStatus(403);
            resp.setContentType("text/html;charset=utf8");
            resp.getWriter().write("当前用户未登录!");
            return;
        }
        if (blogId == null) {
            // 来自博客列表页, 返回当前登录用户信息
            // 不希望返回的body中含有用户密码,所以这里要设置为空字符串
            user.setPassword("");
            resp.setContentType("application/json;charset=utf8");
            String jsonString = objectMapper.writeValueAsString(user);
            resp.getWriter().write(jsonString);
        } else {
            // 来自博客详情页, 返回文章作者信息
            BlogDao blogDao = new BlogDao();
            Blog blog = blogDao.selectOne(Integer.parseInt(blogId));
            if (blog == null) {
                resp.setStatus(403);
                resp.setContentType("text/html;charset=utf8");
                resp.getWriter().write("当前 blogId 有误!");
                return;
            }
            UserDao userDao = new UserDao();
            User author = userDao.selectById(blog.getUserId());
            if (author == null) {
                resp.setStatus(403);
                resp.setContentType("text/html;charset=utf8");
                resp.getWriter().write("当前博客对应的作者没有找到!");
                return;
            }
            author.setPassword("");
            resp.setContentType("application/json; charset=utf8");
            String jsonString = objectMapper.writeValueAsString(author);
            resp.getWriter().write(jsonString);
        }
    }
}

3)实现前端代码

blog_list.html

function getUserInfo() {
    $.ajax({
        type:'get',
        url:'userInfo',
        success: function(body) {
            // 修改左侧用户信息
            let h3 = document.querySelector('.card h3');
            h3.innerHTML = body.username;
        }
    })
}
// 获取当前用户信息
getUserInfo();

blog_datail.html

function getUserInfo() {
    $.ajax({
        type:'get',
        url:'userInfo' + location.search,
        success: function(body) {
            // 修改左侧用户信息
            let h3 = document.querySelector('.card h3');
            h3.innerHTML = body.username;
        }
    })
}
// 获取当前用户信息
getUserInfo();

4)部署项目

第一次登录到博客详情页触发4个请求,分别是获取当前页面,获取博客列表,强制用户登录程序,获取用户信息

  • 获取用户登录信息:

image-20221022224752523

  • 点击属于当前用户的博客

image-20221022224925716

image-20221022224938513

  • 点击不属于当前用户的文章

image-20221022225636943

image-20221022225647753

10. 实现退出登录

在用户退出登录后需要

  • 清除当前的会话信息
  • 跳转到用户登录界面

1)约定前后端交互接口

在用户点击注销后实现跳转到博客登录页面,可以使用a标签实现

请求 
GET /logout

响应
HTTP/1.1 302
Location:blog_login.html

2)实现后端代码

@WebServlet("/logout")
public class LogoutServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        HttpSession session = req.getSession(false);
        if (session == null) {
            resp.sendRedirect("blog_login.html");
            return;
        }
        session.removeAttribute("user");
        resp.sendRedirect("blog_login.html");
    }
}

req 对象没有直接提供一个 删除会话 的操作,删除会话有个方法可以把过期时间设置为0,比较繁琐,更简便的方法是删除user这个键

3)实现前端代码

实现很简单,加个a标签即可

 <a href="logout">注销</a>

4)部署项目

image-20221022231354903

11.实现发布博客

1)约定前后端交互接口

请求:
POST /blog
Content-Type: application/x-www-form-urlencoded

body内容
title=?&content=?

响应:
HTTP/1.1 302
Location:blog_list.html

2)实现前端代码

使用form表单提交请求

<div class="blog-edit-container">
    <form action="blog" method="post">
        <!--标题编辑区-->
        <div class="title">
            <input type="text" id="title" placeholder="在这里写下标题" name="title">
            <input type="submit" id="submit" value="发布文章">
        </div>
        <!-- 博客编辑器标签 -->
        <div id="editor">
            <!-- 需要在这里加上一个隐藏的 textarea -->
            <!-- 属于 editor.md 这个库要求的. -->
            <textarea name="content" style="display: none;" ></textarea>
        </div>
    </form>
    <script>
        // 初始化编辑器
        var editor = editormd("editor", {
            // 这里的尺寸必须在这里设置. 设置样式会被 editormd 自动覆盖掉. 
            width: "100%",
            // 设定编辑器高度
            height: "calc(100% - 50px)",
            // 编辑器中的初始内容
            markdown: "# 在这里写下一篇博客",
            // 指定 editor.md 依赖的插件路径
            path: "editor.md/lib/",
            // 加上这个属性, 效果就是把编辑器里的内容给自动保存到 textarea 里. 
            saveHTMLToTextArea: true,
        });
    </script>
</div>

3)实现后端代码

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    req.setCharacterEncoding("utf8");
    // 1. 获取用户登录状态
    HttpSession session = req.getSession(false);
    if (session == null){
        resp.setStatus(403);
        return;
    }
    User user = (User) session.getAttribute("user");
    if (user == null) {
        resp.setStatus(403);
        return;
    }
    // 2. 读取请求的内容
    String title = req.getParameter("title");
    String content = req.getParameter("content");
    if (title == null || title.equals("") || content == null || content.equals("")) {
        resp.setStatus(400);
        resp.setContentType("text/html; charset=utf8");
        resp.getWriter().write("请求中的标题或正文不完整");
        return;
    }
    // 3. 构造Blog对象,插入到数据库中
    Blog blog = new Blog();
    blog.setTitle(title);
    blog.setContent(content);
    blog.setUserId(user.getUserId());
    BlogDao blogDao = new BlogDao();
    blogDao.insert(blog);
    // 4. 插入成功之后, 跳转到博客列表页.
    resp.sendRedirect("blog_list.html");
}

4)部署项目

image-20221022234329774

我们发现这里的编辑框变短了.

原因是编辑框的高度默认是父元素,没加form标签时是blog-edit-container,但是加入form表单后父元素就变成了form标签,所以解决方案是把form标签的高度设置为100%

<form action="blog" method="post" style="height: 100%;">

image-20221023000523391

成功发送请求:

image-20221023000228990

image-20221023000308088

但是生成的博客在最后出现,按照正常思维应该是最新文章出现在上方,所以这里只需要修改为按时间逆序排序

String sql = "select * from blog order by postTime desc";

现在就按照正常时间逆序

image-20221023000735668

12.删除博客

删除博客只能是当前登录用户是博客的作者时,则在导航栏中显示"删除"按钮

1)约定前后端交互接口

在之前获取用户信息的接口中增加一个字段,用于判断当前登录用户是否为当前博客作者

  1. 判定是否要显示删除按钮
请求
GET /user?blogId= 1

响应
{
	user:1,
	username:admin,
	isYourBlog:1,
}
  1. 删除博客
请求
DELETE /blog?blogId=1
响应
HTTP/1.1 200

2)实现服务器代码

  1. 给 User 类新增一个字段
private int isYourBlog;
  1. 修改UserInfoServlet类
UserDao userDao = new UserDao();
User author = userDao.selectById(blog.getUserId());
if (author == null) {
    resp.setStatus(403);
    resp.setContentType("text/html;charset=utf8");
    resp.getWriter().write("当前博客对应的作者没有找到!");
    return;
}
author.setPassword("");
if (user.getUserId() == author.getUserId()) {
    author.setIsYourBlog(1);
} else {
    author.setIsYourBlog(0);
}
resp.setContentType("application/json; charset=utf8");
String jsonString = objectMapper.writeValueAsString(author);
resp.getWriter().write(jsonString);

3)实现前端代码

修改blog_detail.html

function getUserInfo() {
    $.ajax({
        type:'get',
        url:'userInfo'  + location.search,
        success: function(body) {
            // 修改左侧用户信息
            let h3 = document.querySelector('.card h3');
            h3.innerHTML = body.username;

            if (body.isYourBlog) {
                // 在导航栏中加个按钮, 用来删除文章. 
                let deleteA = document.createElement('a');
                // location.search 就是当前页面 url 的 query string, 也就是
                // ?blogId=1 这样的结果. 
                deleteA.href = 'blogDelete' + location.search;
                deleteA.innerHTML = '删除';

                let navDiv = document.querySelector('.nav');
                navDiv.appendChild(deleteA);
            }
        }
    })
}

4)部署项目

登录admin的账号

image-20221023002358584

image-20221023002421082
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Zzt.opkk

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

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

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

打赏作者

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

抵扣说明:

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

余额充值