前后端分离技术渲染动态页面 - 表白墙(Servlet)
目录
1. 准备工作
2. 第一个版本 - 将数据保存在服务器上
2.1 约定好前后端交互接口
2.2 编写前后端代码
2.2.1 客户端向服务器提交数据
2.2.2 客户端向服务器获取数据
2.2.3 数据保存在服务器版本总结
3. 第二个版本 - 将数据保存在数据库
3.1 引入 MySQL 的依赖
3.2 创建数据库数据表
3.3 调整后端代码
3.3.1 和数据建立连接
3.3.2 封装数据库操作
1. 准备工作
1.创建项目(Maven).
2.创建目录(webapp/WEB-INF/web.xml).
3.引入依赖(HttpServlet, MySQL5系列 的 jar 包, jackson-databind).
4.把写好的表白墙页面的 html 拷贝到项目中.
原始表白墙静态页面的 html 代码:
<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>
<style>
/* 起手操作 */
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
.container {
width: 800px;
margin: 10px auto;
}
.container h2 {
text-align: center;
margin: 30px 0;
}
.row {
height: 50px;
display: flex;
justify-content: center;
margin-top: 5px;
line-height: 50px;
}
.row span {
height: 50px;
width: 100px;
line-height: 50px;
}
.row input {
height: 50px;
width: 300px;
line-height: 50px;
}
#from, #to, #message {
font-size: 24px;
border: none;
outline: none;
padding-left: 10px;
border-radius: 10px;
}
.row button {
width: 200px;
height: 50px;
color: #fff;
background-color: grey;
border: none;
border-radius: 10px;
}
.row button:active{
background-color: grey;
}
</style>
</head>
<body bgcolor="pink">
<div class="container">
<h2>表白墙</h2>
<div class="row">
<span>谁</span>
<input type="text" id="from">
</div>
<div class="row">
<span>对谁</span>
<input type="text" id="to">
</div>
<div class="row">
<span>说什么</span>
<input type="text" id="message">
</div>
<div class="row">
<button>提交</button>
</div>
</div>
<!-- 使用 ajax 所需的依赖 -->
<script src="https://code.jquery.com/jquery-3.6.1.min.js"></script>
<script>
let container = document.querySelector(".container");
let fromInput = document.querySelector("#from");
let toInput = document.querySelector("#to");
let messageInput = document.querySelector("#message");
let button = document.querySelector("button");
button.onclick = function() {
// 1.把用户输入的内容获取到
let from = fromInput.value;
let to = toInput.value;
let message = messageInput.value;
// 空内容不输出
if(from == '' || to == '' || message == '') {
return;
}
// 2.构造一个新的 div,把这个 div 插入到 .container的末尾
let newDiv = document.createElement('div');
newDiv.className = "row";
newDiv.innerHTML = from + " 对 " + to + " 说: " + message;
// 3.把 div 挂在 container 里面
container.appendChild(newDiv);
// 4.把之前输入的内容清空
fromInput.value = '';
toInput.value = '';
messageInput.value = '';
}
</script>
</body>
前端页面有了, 现在我们的问题就是如何使数据能够持久化的保存下来:
两个办法:
1. 将数据保存在服务器上.(服务器重启,数据才会丢失)
2.将数据保存在数据库中.(保存在本地数据库, 永久保存)
2. 第一个版本 - 将数据保存在服务器上
2.1 约定好前后端交互接口
前面我们已经把前端代码写好了, 接下来就是搞后端服务器代码了, 并且还要考虑前后端交互..
1. 前端给后端发送的请求是怎样的, 后端给前端返回的响应是怎样的, 这些都是要在开发之前约定好的.
2. 例如我们使用 getParameter 函数去获取到 query string 中的参数, 务必要先知道 query string 中的 key, 才能获取到 value. 所以开发之前, 前后端的人会先坐在一起约定好, 但是现在我们自己前后端都包了, 就自己跟自己约定就好了.
约定的接口主要是两个:
1. 让页面在加载的时候, 从后端获取消息数据
2. 在用户点击提交的时候, 把当前的消息交给后端.
约定 GET 从服务器获取数据, POST 来提交数据给服务器 是习惯用法, 反过来用也是可以的.
2.2 编写前后端代码
2.2.1 客户端向服务器提交数据
前端代码
// 5.[新增步骤] 需要把刚才从输入里取到的数据, 构造成 POST 请求, 交给后端后端服务器
let messageJson = {
// 构造 js 对象类似于 json 结构, 也是 {} 和键值对的形式
from: from,
to: to,
message: message
};
$.ajax({
type: 'post',
// 相对路径的写法
url: 'message',
contentType: 'application/json;charset=utf8',
// 绝对路径的写法
// url: '/MessageWall/message',
// 将 js 对象封装成 Json 格式的字符串
data: JSON.stringify(messageJson),
success: function(body) {
alert("提交成功!");
},
// 处理提交失败
error: function() {
// 当服务器返回的状态码不是 2xx 时, 就会触发这个 error
alert("提交失败!");
}
});
上输入代码添加在提交按钮的点击事件里面
后端代码
class Message {
public String from;
public String to;
public String message;
@Override
public String toString() {
return "Message{" +
"from='" + from + '\'' +
", to='" + to + '\'' +
", message='" + message + '\'' +
'}';
}
}
@WebServlet("/message")
public class MessageServlet extends HttpServlet {
private ObjectMapper objectMapper = new ObjectMapper();
private List<Message> messageList = new ArrayList<>();
// 负责实现让客户端提交数据给服务器
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 1.把 body 中 json 数据解析出来
Message message = objectMapper.readValue(req.getInputStream(), Message.class);
// 2.把这个对象保存在内存中.
messageList.add(message);
// 方便看到打印结果
System.out.println("message: " + message);
// 3.返回保存成功的响应
resp.setContentType("application/json;charset=utf8");
resp.getWriter().write("{ \"ok\": 1 }");
}
}
启动 Tomcat 服务器, 在浏览器输入 URL http://127.0.0.1:8080/MessageWall/MessageWall.html
得到如下界面, 在表白墙中输入相关信息, 例如 "张三对李四说: 今晚出来喝酒", 然后点击提交, 再通过 fiddler 进行抓包查看请求报文:
fiddler 抓包结果:
理解客户端向服务器提交数据的前后端交互过程:
2.2.2 客户端向服务器获取数据
前端代码
<script>
// 这个函数在页面加载的时候调用, 通过这个函数从服务器获取到当前的消息列表, 并且显示到页面上.
function load() {
$.ajax({
type: 'get',
url: 'message',
success: function(body) {
// 此处得到的 body 已经是一个 js 对象的数组了, 由 ajax 自动转换
// 将服务器返回的 json 格式的字符串解析成 js 对象
// 遍历数组, 把内容构造到页面上
let container = document.querySelector('.container');
// 此处的 body 已经是一个 js 数组了
// ajax 自动将服务器返回的 json 格式的字符串解析成 js 对象
for(let message of body) {
let newDiv = document.createElement('div');
newDiv.className = 'row';
newDiv.innerHTML = message.from + " 对 " + message.to + " 说: " + message.message;
container.appendChild(newDiv);
}
}
});
}
// 页面加载的时候, 就调用该函数
load();
</script>
后端代码
// 负责实现让客户端从服务器获取数据
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 由于约定的请求没有参数, 所以不需要进行解析
resp.setContentType("application/json;charset=utf8");
// 把对象转成 json 格式的字符串, 此处的对象是一个 List
String respString = objectMapper.writer().writeValueAsString(messageList);
resp.getWriter().write(respString);
}
客户端从服务器获取数据前后端交互过程
通过 fiddler 抓包进行查看
🍁当客户端没有提交数据到服务器时, 加载页面观察响应报文:
🍁 当客户端提交一次数据后, 重新加载页面时观察页面和响应报文:
通过上述抓包我们就可以清楚客户端与服务器的交互流程:
1. 当客户端没有提交过数据到服务器的时候, 加载页面, 此时服务器的 messageList 是空的,所返回的结果就是一个空的 json 数组, 所以响应报文中看到的就是一个 [].
2. 当客户端向服务器提交过一次数据, 再次刷新页面时, 页面就会显示以往提交过的数据, 并且响应报文中有一个 json 数组, 里面有一个元素.
2.2.3 数据保存在服务器版本总结
上述客户端向服务器提交数据以及从服务器获取数据的过程无非就是操作一个 List, 然后就是通过 objectMapper 这个类将 json 数据转换成 Message 对象, 或者将 Message 对象转换成 json 字符串.
再一个就是理解前后端的交互流程: 客户端发送的请求 (ajax) 会触发服务器这里的 doXXX 方法. 服务器这里的响应 (getWriter().write()) 就会触发客户端代码 ajax 中的回调函数.
数据保存在服务器上, 已经使得数据的生命周期长了很多, 只要不重启服务器, 数据就不会丢失. 如果想要重启服务器, 数据也存在, 就需要引入数据库了.
3. 第二个版本 - 将数据保存在数据库
由于第二个版本是在第一个版本上进行改进的, 前面的大部分步骤都是一样的, 下面就只会列出不同的步骤.
3.1 引入 MySQL 的依赖
在 maven 中央仓库中找到 mysql 5系列的驱动包, 复制粘贴到 pom.xml 的 <dependencies> 标签里
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.41</version>
</dependency>
3.2 创建数据库数据表
在 main 包下创建一个单独的文件 db.sql, 方便后续在别的机器上部署.
create database MessageWall;
use MessageWall;
drop table if exists MessageWall;
create table MessageWall (
`from` varchar(100),
`to` varchar(100),
message varchar(1024)
);
3.3 调整后端代码
3.3.1 和数据建立连接
单独创建一个类, 来实现数据库的建立连接. 后续在进行前后端交互时, 需要将数据保存在数据库中以及从数据库中获取数据都要建立连接和关闭连接, 就只需要调用相关方法即可.
public class DBUtil {
private static DataSource dataSource = null;
private static DataSource getDataSource() {
if(dataSource == null) {
dataSource = new MysqlDataSource();
((MysqlDataSource)dataSource).setURL("jdbc:mysql://127.0.0.1:3306/MessageWall?characterEncoding=utf8&useSSL=false");
((MysqlDataSource)dataSource).setUser("root");
((MysqlDataSource)dataSource).setPassword("316772");
}
return dataSource;
}
建立连接
public static Connection getConnection() throws SQLException {
return getDataSource().getConnection();
}
// 关闭连接
public static void close(Connection connection, PreparedStatement statement, ResultSet resultSet) {
// 此处最好写成分开的 try catch
// 保证即使一个地方 close 出现异常了, 也不会影响其他的 close 的正常执行
if(resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(statement != null) {
try {
statement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
3.3.2 封装数据库操作
@WebServlet("/message")
public class MessageServlet extends HttpServlet {
private ObjectMapper objectMapper = new ObjectMapper();
// 负责实现让客户端提交数据给服务器
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 1.把 body 中 json 数据解析出来
Message message = objectMapper.readValue(req.getInputStream(), Message.class);
// 2.把这个对象保存在内存中.
save(message);
System.out.println("message: " + message);
// 3.返回保存成功的响应
resp.setContentType("application/json;charset=utf8");
resp.getWriter().write("{ \"ok\": 1 }");
}
// 负责实现让客户端从服务器获取数据
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 由于约定的请求没有参数, 所以不需要进行解析
resp.setContentType("application/json;charset=utf8");
// 把对象转成 json 格式的字符串, 此处的对象是一个 List
List<Message> messageList = load(); // 从数据库查询出来
String respString = objectMapper.writer().writeValueAsString(messageList);
resp.getWriter().write(respString);
}
// 把当前的消息存放到数据库中
private void save(Message message) {
Connection connection = null;
PreparedStatement statement = null;
try {
// 1.和数据库建立连接
connection = DBUtil.getConnection();
// 2.构造 SQL语句
String sql = "insert into MessageWall values(?, ?, ?)";
statement = connection.prepareStatement(sql);
statement.setString(1, message.from);
statement.setString(2, message.to);
statement.setString(3, message.message);
// 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);
}
}
// 从数据库查询到记录
private List<Message> load() {
Connection connection = null;
PreparedStatement statement = null;
ResultSet resultSet = null;
List<Message> messageList = new ArrayList<>();
try {
// 1.和数据库建立连接
connection = DBUtil.getConnection();
// 2.构造 SQL 语句
String sql = "select * from MessageWall";
statement = connection.prepareStatement(sql);
// 3.执行 SQL 语句
resultSet = statement.executeQuery();
// 4.遍历结果集
while(resultSet.next()) {
Message message = new Message();
message.from = resultSet.getString("from");
message.to = resultSet.getString("to");
message.message = resultSet.getString("message");
messageList.add(message);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
// 5.释放资源
DBUtil.close(connection, statement, resultSet);
}
return messageList;
}
}
上述代码相比前面的保存在服务器代码主要就是多了 save 和 load 两个方法, 存数据就不再是存放在服务器的 List 里面了, 而是通过 save 函数存放在数据库中, 取数据就不再是从服务器的 List 中取了, 而是通过 load 函数从数据中查询出来显示到页面. 此时我们提交的数据是可以从数据库中查询到的,如下图:
问题分析
我们在写完代码, 如果发现程序不符合预期, 怎么办? 例如, 写完代码, 运行后发现页面无法将之前提交的数据显示出来, 出现这种问题, 首先我们就要使用 fiddler 抓个包进行分析, 然后判断是前端代码的问题, 还是后端代码的为问题.
1. 可以抓一个页面加载的包, 看看 GET /messge 方法的请求的响应是啥样的:
- 如果请求没问题, 响应有问题, 那么就是后端问题;
- 如果请求有问题, 响应也有问题, 那么就是前端问题(检查发送 ajax 的代码);
- 如果请求和响应都没问题, 但是页面不能正确显示, 就还是前端的问题(检查 ajax 的回调函数 success())
2.也可以抓包 POST 进行查看.
3.还可以检查数据库里有没有正确的数据:
- 如果数据库有数据, 说明大概率是 GET 的时候出错了;
- 如果数据库没数据, 说明大概率是 POST 的 时候出错了.
谢谢观看!!