Java EE 博客系统(Servlet版)

1. 基本情况

这里的博客系统主要是四个界面

  1. 博客列表页
    显示出当前网站上都有哪些博客
  2. 博客详情页
    点击列表上的某个博客,就能进入对应详情页(显示出博客的具体内容)
  3. 博客编辑页
    让用户输入博客内容,并且发送到服务器
  4. 登录页

这里主要来写后端的代码,前端代码已经准备就绪,直接导入即可


任务:
基于上述的页面,编写服务器/前后端交互代码

通过这些代码,完成博客系统,完整的功能

  1. 实现博客列表页
    让页面从服务器拿到博客数据(数据库)
  2. 实现博客详情页
    点击博客详情的时候,可以从服务器拿到博客的完整数据
  3. 实现登录功能
  4. 实现强制要求登录
    (当前处于未登录的情况下,其他的界面,博客列表、博客详情、博客编辑…就会强制跳转到登录页)
    要求用户登录后才能使用
  5. 实现显示用户信息
    从服务器获取到
    博客列表页,拿到的是当前登录的用户的信息
    博客详情页,拿到的是文章作者的信息
  6. 实现退出登录
  7. 发布博客
    博客编辑页,输入文章标题和内容之后,点击发布,就能把这个数据上传到 服务器上并保存

在这些功能搞定之后,一个功能相对完整的博客网站,就初具规模了

2. 准备工作

  1. 创建项目,引入依赖,把当前的前端界面引入到项目中

  1. 数据库设计

设计好对应的表结构,并且把数据库相关代码,也进行封装

  • 找到实体
    博客(blog 表)
    用户(user 表)
  • 确认实体之间的关系
    一对多
    一个博客,只属于一个用户
    一个用户,可以发布多个博客
    这样就应该在博客列表中,引入一个 userId 这样的属性

blog (blogId, title, content, postTime, userId)
user (userId, username, password)
在这里插入图片描述
在这里插入图片描述

接下来,把数据库的代码进行一些封装
在这里插入图片描述

在进行网站开发的工程中,一种常见的代码组织结构,MVC
M model:操作数据的代码
V view:操作/构造界面的代码
C controler:业务逻辑,处理前端请求

由于这套组织结构比较古老,在现在写的过程中,也不会完全遵守

在这里插入图片描述
DBUtil 这个类,封装数据建立连接的操作

在这里插入图片描述
当前这个懒汉模式是不安全的
当前 servlet 本身就是在多线程环境下执行的
tomcat 收到多个请求的时候,就会使用 多线程 的方式,执行不同的 servlet 代码
这里就可能有现成不安全的问题

因此要加锁
在这里插入图片描述

package model;

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

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

// 通过这个类, 封装数据库建立连接的操作.
// 由于接下来代码中, 有多个 Servlet 都需要使用数据库. 就需要有一个单独的地方来把 DataSource 这里的操作进行封装.
// 而不能只是放到某个 Servlet 的 init 中了.
// 此处可以使用 单例模式 来表示 dataSource
public class DBUtil {
    private volatile static DataSource dataSource = null;

    private static DataSource getDataSource() {
        if (dataSource == null) {
            synchronized (DBUtil.class) {
                if (dataSource == null) {
                    dataSource = new MysqlDataSource();
                    ((MysqlDataSource) dataSource).setURL("jdbc:mysql://127.0.0.1:3306/servlet_blog_system?characterEncoding=utf8&useSSL=false");
                    ((MysqlDataSource) dataSource).setUser("root");
                    ((MysqlDataSource) dataSource).setPassword("123456");
                }
            }
        }
        return dataSource;
    }

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

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

DBUtil 完成对于数据库建立连接和关闭连接的实现


  1. 创建实体类

大部分表,都需要搞一个专门的类来表示
表里的一条数据,就会对应到这个类的一个对象

这样就可以把数据库中的数据和代码联系起来了


  1. 针对博客表和用户表操作

这里再创建两个类,来完成准对博客表和用户表的增删改查操作

这两个类,叫做BlogDao 和 UserDao

DAO(Data Access Object):数据访问对象
通过这两个类的对象,来完成针对数据库表的操作

写一个复杂一些的代码,往往需要先理清楚思路
相比于细节来说,理清思路是更复杂的
(为了实现这个代码,要写哪些类,有哪些方法)
在这里插入图片描述
在这里插入图片描述

package model;

import com.mysql.jdbc.Connection;
import com.mysql.jdbc.JDBC4PreparedStatement;

import javax.xml.stream.events.DTD;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

// 通过 BlogDao 来完成针对 blog 表的操作
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
            statement.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            DBUtil.close(connection, statement, null);
        }
    }

    // 2. 查询博客列表 (博客列表页)
    //    把数据库里所有的博客都拿到.
    public List<Blog> getBlogs() {
        List<Blog> blogList = new ArrayList<>();
        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;
        try {
            connection = DBUtil.getConnection();
            String sql = "select * from blog order by postTime desc";
            statement = connection.prepareStatement(sql);
            resultSet = statement.executeQuery();
            while (resultSet.next()) {
                Blog blog = new Blog();
                blog.setBlogId(resultSet.getInt("blogId"));
                blog.setTitle(resultSet.getString("title"));
                // 此处读到的正文是整个文章内容. 太多了. 博客列表页, 只希望显示一小部分. (摘要)
                // 此处需要对 content 做一个简单截断. 这个截断长度 100 这是拍脑门出来的. 具体截取多少个字好看, 大家都可以灵活调整.
                String content = resultSet.getString("content");
                if (content.length() > 100) {
                    content = content.substring(0, 100) + "...";
                }
                blog.setContent(content);
                blog.setPostTime(resultSet.getTimestamp("postTime"));
                blog.setUserId(resultSet.getInt("userId"));
                blogList.add(blog);
            }

        } catch (SQLException e) {
            e.printStackTrace();
        }finally {
            DBUtil.close(connection, statement, resultSet);
        }
        return blogList;
    }

    // 3. 根据博客 id 查询指定的博客
    public Blog getBlog(int blogId) {
        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;
        try {
            connection = DBUtil.getConnection();
            String sql = "select * from blog where blogId = ?";
            statement = connection.prepareStatement(sql);
            statement.setInt(1, blogId);
            resultSet = statement.executeQuery();
            // 由于此处是拿着 blogId 进行查询. blogId 作为主键, 是唯一的.
            // 查询结果非 0 即 1 , 不需要使用 while 来进行遍历
            if (resultSet.next()) {
                Blog blog = new Blog();
                blog.setBlogId(resultSet.getInt("bligId"));
                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 {
            DBUtil.close(connection, statement, resultSet);
        }
        return null;
    }

    // 4. 根据博客 id, 删除博客
    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);
            statement.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        }finally {
            DBUtil.close(connection, statement, null);
        }
    }
}

这里的 JDBC 代码,大同小异
因此,后面就会有一些数据库的框架
(MyBatis,MyBatisPlus,JPA…)
这里封装了 JDBC 的代码
这些框架 本质上 就是帮我们自动生成 JDBC 的代码

3. 博客列表页

在博客列表页加载的时候,通过 ajax 给服务器发起请求
从服务器(数据库)拿到博客列表数据,并求显示到页面上


  1. 约定前后端交互接口
    请求
    GET /blog
    响应
    HTTP/1.1 200 OK
    Content-Type: applicantion/json
    在这里插入图片描述

  1. 让浏览器给服务器发起请求
    在这里插入图片描述

  1. 服务器处理上述请求,返回响应数据(查询数据库)
    在这里插入图片描述由于是 list,Jackson 就会把结果转成 数组
    每个元素又是一个 Blog 对象

  1. 让前端代码,处理上述数据
    构造成 html 片段,显示在页面上
    形如:
    在这里插入图片描述

这里构造页面的过程,还是之前的 api
(1)querySelector:获取页面已有的元素
(2)createElement:创建新的元素
(3).innerHtml:设置元素里的内容
(4).className:设置元素的 class 属性
(5)appendChild:把这个元素添加到另一个元素的末尾

html 中
显示 >:
需要使用转义字符 &gt;
显示 <:
也需要使用转义字符 &lt;
在这里插入图片描述

a 标签在 html 中称为“超链接”
点击之后能够跳转到新的页面
在这里插入图片描述

这个代码,和前面的 jdbc 的感觉类似
在这里插入图片描述

对于前端页面来说
生成页面的方式其实有很多种
此处使用的比较朴素的方式(基于 dom api 的方式)
dom api 就是属于是浏览器提供的标准的 api(不属于任何的第三方框架和库)

定位就类似于 jdbc api

前端也有一些框架和库,是把 dom api 又进行了封装,用起来更简单一些


这里有一个问题,就是返回的时间,是时间戳
在这里插入图片描述

在这里,Jackson 在进行主要的工作
(1)Jackson 发现 blogs 是一个 list,于是就会循环遍历里面的每个元素
(2)针对每个元素(Blog 对象),通过反射的方式,获取到都是哪些属性,属性的名字,属性的值
在获取属性值的时候,就是通过调取 get 方法

在这里插入图片描述
这个时候,我们就要对代码进行改变
修改 getPostTime 方法,让其直接返回一个“格式化时间”
在这里插入图片描述
制定了一个格式化字符串
描述了当前时间日期具体的格式
各种语言,表示格式化时间都有这样的字符串
但是不同语言,表示的含义是不同的

在这里插入图片描述


此时还有一个问题,就是希望新加入的博客在上面,以前写的博客在下面

这个时候,该如何做呢?
返回 list,把 list 先逆序一下?

此处的结果顺序,是从数据库里查询出来的
一个 sql 如果不加 order by,结果的顺序是不可预期的

此处科学的做法,应该是加上 order by,时间逆序
在这里插入图片描述

4. 博客详情页

点击查看全文,就可以跳转到 带有不同 blogId 的 query string
后续在博客详情页中,就可以给服务器发起 ajzx 请求,根据这里的 blogId ,查询数据库中,博客的具体内容再返回
前端还是把得到的数据给构造到页面上
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


  1. 约定前后端交互接口
    请求
    GET /blog?blogId=1
    请求
    HTTP/1.1 200 OK
    Content-Type: applivation/json
    在这里插入图片描述

这个请求,是希望在博客详情页的时候,通过 ajax 发给服务器


  1. 让前端代码,通过 ajax 发起请求

此处有个问题,发起 ajax 请求的时候要带有 blogId
blogId 当前处于 博客详情页 url 中
在这里插入图片描述
这里我们可以通过 location.search 方式拿到 页面 url 中的 query string
在这里插入图片描述


  1. 让服务器处理这个请求

这里依然使用 servlet 处理,一个路径对应到一个 servle

当前是使用一个 servlet 处理两种请求
博客列表页,不带 query string
博客详情页,带有 query string
就可以根据 query string 是否存在的情况,来区分是哪种请求
分别返回不同的数据即可

使用两个 servlet 处理这里的两个请求,也可以
就约定成不同的路径即可
使用一个 servlet 也可以,这里没有一个明确的标准

在这里插入图片描述


  1. 前端拿到响应之后,把响应数据,构成页面的 html 片段
    在这里插入图片描述
    在这里插入图片描述
    由于显影中,带有 Content-Type:application/json
    jquery 自动帮我们把字符串转成 js 对象了
    直接通过 . 的方式就能访问属性了
    在这里插入图片描述
    在博客列表页,需要循环遍历,构造的页面内容页更复杂
    此处就更简单一些,只需要设置三个内容即可

在这里插入图片描述
写完代码之后,再点击某个博客,就可以看到,有的博客里面的详情页,还是之前的旧的内容

这个问题是浏览器缓存引起的

浏览器加载页面的时候,是通过网络获取的(网络速度比较慢)
浏览器有时候就会把已经加载过的界面,在本地硬盘保存一份,后续再访问同一个界面
就不需要通过网络加载,直接加载本地硬盘的这一份

默认认为 html 出现修改的概率比较低
但是也不是完全不会修改

如何克服缓存的干扰,前端有专业的解决方案
我们在这里可以直接使用 ctrl + F5,强制刷新界面


当前博客详情页,虽然能用出博客的正文,但是显示的是正文的 md 原始数据
作为博客网站,正确的做法应该是显示出 md 渲染后的效果

此处仍然 通过 第三方库 (editor.md)
在这里插入图片描述

这个是 editormd 这个库给的一个全局变量
把依赖正确引入了,这个变量就能直接使用

这个方法的效果,就是把 blog.content 这里的 md 的原始数据,渲染成 html,放到 id 为 content 的 div 中
在这里插入图片描述
一个 html 标签,可以有很多的属性
class 属性,往往是用来和 css 样式配合的
id 属性,则是一个“身份标识”要求一个界面中,id 必须是唯一的
在这里插入图片描述

5. 实现登录

在这里插入图片描述

在登录界面,在输入框中填写 用户名和密码
点击登录,就会给服务器发起 http 请求(这里使用 form)

服务器处理登录请求,读取用户名密码,在数据库查询、匹配
如果正确,就登录成功,创建会话,跳转到博客列表页

由于这里,登录成功,直接进行重定向跳转,就不要浏览器额外写代码处理,直接浏览器自动跳转


  1. 约定前后段交换接口
    请求
    POST /login
    Content-Type: application/x-www-form-urlencoded
    username=zhangsan&password-123
    在这里插入图片描述

form 表单,提交成功,可以直接使用 302 重定义跳转
如果使用 ajax,ajax 处理响应就需要写代码来完成跳转(不是浏览器自动完成了)


  1. 让前端发起请求
    form

username=zhangsan&password-123
这里的 input 标签,name 属性就是这里 body 中的 key

什么时候一个元素要有 id,什么时候没有呢?
看个人的需要,灵活处理

在这里插入图片描述


  1. 让服务器处理请求,并返回响应
    在这里插入图片描述

在这里插入图片描述

package servlet;

import model.User;
import model.UserDao;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.time.temporal.Temporal;

@WebServlet("/login")
public class LoginServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 1. 读取参数中的用户名和密码
        req.setCharacterEncoding("utf8");
        String username = req.getParameter("username");
        String password = req.getParameter("password");
        //    验证一下参数, 看下是否合理.
        if (username == null || username.length() == 0 || password == null || password.length() == 0) {
            resp.setContentType("text/html; charset=utf8");
            resp.getWriter().write("您输入的用户名或者密码为空!");
            return;
        }
        // 2. 查询数据库, 看看这里的用户名密码是否正确.
        UserDao userDao = new UserDao();
        User user = userDao.getUserByName(username);
        if (user == null) {
            // 用户名不存在!
            resp.setContentType("text/html; charset=utf8");
            resp.getWriter().write("您输入的用户名或密码不正确!");
            return;
        }
        if (!password.equals(user.getPassword())) {
            // 密码不正确!
            resp.setContentType("text/html; charset=utf8");
            resp.getWriter().write("您输入的用户名或密码不正确!");
            return;
        }
        // 3. 创建会话
        HttpSession session = req.getSession(true);
        session.setAttribute("user", user);
        // 4. 跳转到主页了.
        resp.sendRedirect("blog_list.html");
    }
}

6. 强制要求登录

在博客列表页、详情页、编辑页,判断当前用户是否已经登录
如果未登录,则强制跳转到登录页(要求用户必须登录后才能使用)

在上述的页面中,在页面加载中,给服务器发起 ajax
从服务器获取一下当前的登录状态


  1. 约定前后端交互接口

GET /login

登录成功:HTTP/1.1 200
登录失败:HTTP/1.1 403

也可以通过其他的方式,比如都是返回 200,但是在 body 中给不同的结果


  1. 让前端代码发起这个请求,并响应
    在这里插入图片描述

一个页面,触发的 ajax是可以有多个的
一个页面通常都会触发多个 ajax
这些 ajax 之间是“并发执行”这样的效果

js 中是没有“多线程”这样的机制
而 ajax 是一种特殊的情况,能够起到类似于“多线程”的效果

当页面中发起两个或者多个 ajax 的时候,这些 ajax 请求就相当于并发的发送出去
彼此之间不会相互干预
(不是 串行 执行,不是执行完一个 ajax,得到响应之后,再执行下一个)

同时发出去多个请求,谁的响应先回来了,就先执行谁的回调函数


  1. 让服务器处理上述请求

在这里插入图片描述
当前虽然等登录服了,一旦重启服务器,仍然会被判定为未登录状态

登录状态是通过服务器这里的 session 来存储的
session 这是服务器内存中的类似于 hashmap 这样的结构
一旦服务器重启了,hashmap 里面原有的内容就没了

但是这种设定,并不科学,相比支架,我们还有更好的解决方案

  1. 我们把会话进行持久化保存(文件,数据库,redis…)
  2. 使用令牌的方式保存(把用户信息,在服务器里面,还是保存在浏览器这里,相当于服务器没有在内存中存贮内存中的用户的身份)

这里我们需要让多个界面都有这样的机制,这里就可以把一些公共的代码,单独拿出来
放到某个 .js 文件中
通过 html 中的 script 标签,来引用这样的文件内容
此时,就可以在 html 中调用对应的公共代码了
在这里插入图片描述
在这里插入图片描述

7. 显示用户信息

博客列表页:显示的是当前登录的用户的信息

博客详情页:显示的是当前文章的作者信息

在页面加载的时候,给服务器发起 ajax 请求
在服务器返回对应的用户数据
根据发起请求不同的界面,服务器返回不同的信息即可


  1. 约定前后端交换接口

博客列表页,获取当前登录的用户信息
请求:
GET /userInfo
响应:
HTTP/1.1 200 OK
application/json
在这里插入图片描述
博客详情页,获取当前文章的作者信息
请求:
GET /authorInfo?blogId=1
响应:
HTTP/1.1 200 OK
application/json
在这里插入图片描述


  1. 先让前端代码,发起这样的请求

博客列表页:
在这里插入图片描述
博客详情页:
在这里插入图片描述


  1. 编写服务器代码,来处理上述请求

博客列表页:
在这里插入图片描述
博客详情页:
在这里插入图片描述
这里是通过两步 sql 分别查询的
先查 blog 表里面的 blog 对象
再查 user 表

其实也可以一步 sql 搞定
比如:可以使用联合查询,把 blog user 进行笛卡尔积,找出匹配的结果
也可以使用子查询,把两个 sql 合并在一个完成

当然,一步完成是要付出代价的
联合查询来说,笛卡尔积,对于数据库是一个不小的开销
子查询来说,这样的 sql 可读性可能比较差


  1. 在前端代码中,处理响应
    把响应中的数据,给写到刚才页面的对应的位置上

在这里插入图片描述
在这里插入图片描述
以下这个请求从服务器拿到了当前用户的信息
进一步的就把用户的名字显示到页面上了
在这里插入图片描述
在这里插入图片描述

8. 退出登录

博客列表、博客详情、博客编辑 的导航栏中,都有一个“注销”按钮

在这里插入图片描述
注销 这个东西是 a 标签
可以有一个 href 属性
点击就会触发一个 http 请求
并且可能会引起浏览器跳转到另一个页面

让用户点击“注销”的时候,就能够触发一个 HTTP 请求(GET 请求)

服务器收到这个 GET 请求的时候,就会把会话里的 user 这个 Attribute 给删了
由于在判断用户是否是登录状态的逻辑中,需要同时验证,会话存在,且 这里的 user Attribute 也存在
只要破坏一个,就可以是登录状态发生改变了


为什么不直接删除 session 本身?
主要因为,sevlet 没有提供,删除 session 的方法
虽然有间接的方式(session 可以摄者国企时间,设置一个非常短的过期时间),也可以起到删除的效果,但是不太优雅

session 提供了 removeAttribute 这样的方法,可以把 user 这个 Attribute 给删了


  1. 约定前后端交互接口
    请求:
    GET /logout
    响应:
    直接重定向到登录页
    HTTP/1.1 302
    Location: login.html

  1. 编写前端代码,发送请求
    不用写 ajax,直接就给 a 标签设置 href 属性即可

在这里插入图片描述


  1. 编写后端代码,处理这个请求,完成退出登录的操作

在这里插入图片描述

9. 发布博客

当点击提交的时候,就需要构造 http 请求,把此时的页面中的标题和正文都传输到服务器这边
服务器把这个数据存入数据库即可

此处这里的 http 请求,可以使用 ajax,也可以使用 form
这种填写输入框,提交数据的场景,使用 form 会更方便


  1. 约定前后端交互接口

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

title=这是标题&content=这是正文
(上面的中文 都是要 urlencode,form 表单直接就能完成这个操作)

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


  1. 编写前端代码,构造请求

标题本身就是自己写的一个 input,给他加上 name 属性,很容易
但是博客正文,是由 editor md 构成的一个编辑器,这里如何添加 name 属性呢?

editor md 的开发者们,也考虑到了这种情况
在官方文档中也有这样的例子

在这里插入图片描述
这个 div 就是 editor.md 的编辑器的容器
在这个 div 里,搞一个隐藏的 textarea 标签(多行编辑框,把 name 属性加到 textarea 属性上)
并且在初始化 editormd 对象的时候,加上一个 对应的属性即可
在这里插入图片描述
name=“content”:是 form 中键值对的 key
“display: none;” :让这个 textarea 隐藏起来
在这里插入图片描述
这个代码是初始化 editormd 的编辑器的代码
在这里插入图片描述
在这里插入图片描述


  1. 编写服务器代码,处理刚才的请求
    在这里插入图片描述
    在这里插入图片描述

10. 如果程序出现问题怎么办?

这个时候,我们可以使用抓包来实现

抓包的目的,是为了先确定,在点击刷新这个过程中,浏览器和服务器之间有几次 http 交互
每一次交互,请求是什么样的,响应是什么样的

解下来就需要观察,抓包结果中,这几个 http 交互的请求是否都符合预期

  1. 先开请求发没有发
    如果你的浏览器都没发这个请求,说明前端代码有问题
    就需要检查你的前端带啊,ajax 是怎么写的

  2. 再看请求中各个部分是否正确,是否符合约定的接口要求
    如果请求不符合预期,说明还是前端代码有问题,检查 ajax 代码

  3. 如果请求没问题,需要再检查响应数据
    如果请求正常,相应数据不符合预期
    此时就下检查后端代码,是否你的后端代码
    没能正确的完成数据库查询操作等
    (尤其要注意服务器的控制套是否出现异常信息)

  4. 如果请求和响应都没有问题,说明服务器已经返回正确的数据了,但是页面没有把这些数据正确的显示出来
    此时还是要检查前端代码,尤其是检查前端处理响应的这里的逻辑
    (尤其要注意,浏览器控制台是否有报错)

当确定范围之后,进一步排查问题,还需要在代码中,加入更多的日志
System.out.println
console.log

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

柒柒要开心

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

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

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

打赏作者

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

抵扣说明:

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

余额充值