style="background-color: transparent;"
<div class="container-right">
<div class="blog-content">
<h3>h3>
<div class="date">div>
<div id="content" style="background-color: transparent;">
div>
div>
div>
- 新增 js 代码, 从服务器获取博客详情数据.
ajax({
url: 'blog' + location.search,
method: 'GET',
callback: function (data, status) {
if (status == 200) {
var blog = JSON.parse(data);
buildBlog(blog);
} else {
console.log("status error! " + status);
}
}
});
function buildBlog(blog) {
// 1. 更新标题
var titleDiv = document.querySelector(".blog-content h3");
titleDiv.innerHTML = blog.title;
// 2. 更新时间
var dateDiv = document.querySelector(".blog-content .date");
dateDiv.innerHTML = formatDate(blog.postTime);
// 3. 更新博客正文
editormd.markdownToHTML('content', { markdown: blog.content });
}
部署程序, 验证效果.
实现登陆
这部分逻辑和之前的版本基本一致.
- 登陆页面提供一个 form 表单, 通过 form 的方式把用户名密码提交给服务器.
- 服务器端验证用户名密码是否正确.
- 如果密码正确, 则在服务器端创建 Session , 并把 sessionId 通过 Cookie 返回给浏览器.
前后端分离的项目中, 虽然主要使用 ajax 进行前后端交互, 但是也不是完全不能用 form.
约定前后端交互接口
[请求]
POST /login
Content-Type: application/x-www-form-urlencoded
username=test&password=123
[响应]
HTTP/1.1 302
Location: blog_list.html
实现服务器代码
创建 LoginServlet
代码和 博客系统(基于模板技术) 中的 LoginServlet 相同.
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
req.setCharacterEncoding("utf-8");
resp.setContentType("text/html; charset=utf-8");
// 1. 读取用户名和密码
String username = req.getParameter("username");
String password = req.getParameter("password");
if (username == null || password == null || "".equals(username) ||
"".equals(password)) {
String html = "登陆失败! 缺少 username 或 password 字段
";
resp.getWriter().write(html);
return;
}
// 2. 在数据库中验证用户名密码
UserDao userDao = new UserDao();
User user = userDao.selectByName(username);
if (!password.equals(user.getPassword())) {
String html = "登陆失败! 用户名或者密码错误!
";
resp.getWriter().write(html);
return;
}
// 3. 登陆成功, 设置 Session
HttpSession session = req.getSession(true);
session.setAttribute("user", user);
// 4. 重定向到博客列表页.
resp.sendRedirect("blog_list.html");
}
}
实现客户端代码
修改 login.html
- 给输入框套上一层 form 标签. action 为 login, method 为 POST
- 给 input 加上 name 属性.
- 把提交按钮改成
<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>
部署程序, 验证效果.
实现强制要求登陆
当用户访问 博客列表页 和 博客详情页 时, 如果用户当前尚未登陆, 就自动跳转到登陆页面.
之前的 “跳转到登陆页面” 是直接服务器返回 302 实现的. 现在需要通过页面的 JS 代码来实现.
实现服务器代码
- 创建 Util 类, 实现
checkLoginStatus
方法, 检测当前用户的登陆状态.
public class Util {
public static User checkLoginStatus(HttpServletRequest req) {
HttpSession session = req.getSession(false);
if (session == null) {
return null;
}
User user = (User) session.getAttribute("user");
return user;
}
}
- 修改 BlogServlet, 在 doGet 的开头调用
checkLoginStatus
检测该用户是否登陆, 如果未登录则返 回一个 403 响应.
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws
ServletException, IOException {
// ...... 其他代码不变
// 检测用户登陆状态
User user = Util.checkLoginStatus(req);
if (user == null) {
resp.setStatus(403);
resp.getWriter().write("{ reason: \"当前用户尚未登陆!\" }");
return;
}
// ...... 其他代码不变
}
实现客户端代码
- 修改 blog_list.html
- 在 ajax 的回调函数中, 判定响应状态码是否为 403.
- 使用 location.assign 进行页面跳转.
ajax({
url: 'blog',
method: 'GET',
callback: function (data, status) {
if (status == 200) {
var blogs = JSON.parse(data);
buildBlogs(blogs)
} else if (status == 403) {
// 当前用户未登录, 重定向到 login.html
location.assign("login.html");
} else {
console.log("status error! " + status);
}
}
});
- 修改 blog_detail.html
修改方式同上
ajax({
url: 'blog' + location.search,
method: 'GET',
callback: function (data, status) {
if (status == 200) {
var blog = JSON.parse(data);
buildBlog(blog);
} else if (status == 403) {
// 如果未登录, 直接重定向到 login.html
location.assign("login.html");
} else {
console.log("status error! " + status);
}
}
});
部署程序, 验证效果.
实现显示用户信息
目前页面的用户信息部分是写死的. 形如:
我们期望这个信息可以随着用户登陆而发生改变.
- 如果当前页面是博客列表页, 则显示当前登陆用户的信息.
- 如果当前页面是博客详情页, 则显示该博客的作者用户信息.
注意: 当前我们只是实现了显示用户名, 没有实现显示用户的头像以及文章数量等信息.
约定前后端交互接口
在博客列表页, 获取当前登陆的用户的用户信息.
[请求]
GET /user
[响应]
{
userId: 1,
username: test
}
在博客详情页, 获取当前文章作者的用户信息
[请求]
GET /user?blogId=1
[响应]
{
userId: 1,
username: test
}
实现服务器代码
创建 UserServlet
public class UserServlet extends HttpServlet {
private ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setContentType("application/json; charset=utf-8");
// 1. 先判定当前用户是否已经登陆
User user = Util.checkLoginStatus(req);
if (user == null) {
resp.setStatus(403);
resp.getWriter().write("{ \"reason\": \"当前尚未登陆\" }");
return;
}
// 2. 读取请求中的 blogId 参数
String blogId = req.getParameter("blogId");
String jsonString = null;
if (blogId == null) {
// 获取当前登陆用户的信息
// 这个信息已经在 session 中获取到了.
jsonString = objectMapper.writeValueAsString(user);
} else {
// 获取指定文章作者的用户信息
BlogDao blogDao = new BlogDao();
Blog blog = blogDao.selectOne(Integer.parseInt(blogId));
UserDao userDao = new UserDao();
User author = userDao.selectById(blog.getUserId());
jsonString = objectMapper.writeValueAsString(author);
}
resp.getWriter().write(jsonString);
}
}
实现客户端代码
- 修改 blog_list.html
- 新增一个 ajax 函数的调用, 以 GET 请求 /user 路径.
- 在响应回调函数中, 根据响应中的用户名, 更新界面的显示.
ajax({
url: 'user',
method: 'GET',
callback: function (data, status) {
if (status == 200) {
var user = JSON.parse(data);
changeUser(user);
} else {
console.log("status error! " + status);
}
}
});
function changeUser(user) {
var h3 = document.querySelector(".card h3");
h3.innerHTML = user.username;
}
- 修改 blog_content.html
修改方式同上
ajax({
url: 'user' + location.search,
method: 'GET',
callback: function (data, status) {
if (status == 200) {
var user = JSON.parse(data);
changeUser(user);
} else {
console.log("status error! " + status);
}
}
});
function changeUser(user) {
var h3 = document.querySelector(".card h3");
h3.innerHTML = user.username;
}
部署程序, 验证效果.
实现注销登陆
约定前后端交互接口
[请求]
GET /logout
[响应]
HTTP/1.1 302
Location: login.html
实现服务器代码
创建 LogoutServlet
- 从 session 中删除掉保存的 User 对象.
- 响应重定向到 login.html 页面.
@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.setStatus(403);
return;
}
session.removeAttribute("user");
resp.setStatus(200);
}
}
客户端代码不需要调整.
注销按钮本来就是一个 , 点击的时候就会发送
GET /logou
这样的请求. 部署程序, 验证效果.
实现发布博客
逻辑和 博客系统(基于模板技术) 基本一致.
约定前后端交互接口
[请求]
POST /blog
Content-Type: application/x-www-form-urlencoded
title=标题&content=正文...
[响应]
HTTP/1.1 302
Location: blog_list.html
实现服务器代码
修改 BlogServlet, 新增 doPost 方法.
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws
ServletException, IOException {
req.setCharacterEncoding("utf-8");
resp.setContentType("application/json; charset=utf-8");
// 1. 检查用户是否已经登陆
User user = Util.checkLoginStatus(req);
if (user == null) {
resp.setStatus(403);
return;
}
// 2. 读取请求中的数据
String title = req.getParameter("title");
String content = req.getParameter("content");
if (title == null || content == null || "".equals(title) ||
"".equals(content)) {
String html = "title 或者 content 字段缺失! 新增博客失败!
";
resp.getWriter().write(html);
return;
}
// 3. 构造博客对象
Blog blog = new Blog();
blog.setTitle(title);
blog.setContent(content);
blog.setUserId(user.getUserId());
blog.setPostTime(new Timestamp(System.currentTimeMillis()));
// 4. 把博客对象插入到数据库
BlogDao blogDao = new BlogDao();
blogDao.insert(blog);
// 5. 重定向到博客列表页
resp.sendRedirect("blog_list.html");
}
实现客户端代码
修改 blog_edit.html 页面结构,
- 增加 form 标签, action 为
blog_edit
, method 为POST
- 给 form 指定
height: 100%;
防止编辑器高度不能正确展开. - 给标题的 input 标签加上 name 属性
- 把提交按钮改成
- 在 里面加上一个隐藏的 textarea
<div class="blog-edit-container"> <form action="blog_edit" method="POST" style="height: 100%;"> <div class="title"> <input type="text" placeholder="在这里写下文章标题" id="title" name="title"> <input type="submit" id="submit" value="发布文章">input> div> <div id="editor"> <textarea name="content" style="display: none;">textarea> div> form> div>
在 editor.md 的初始化代码中, 新增一个选项
saveHTMLToTextarea: true
// 初始化编辑器 var editor = editormd("editor", { // 这里的尺寸必须在这里设置. 设置样式会被 editormd 自动覆盖掉. width: "100%", // 高度 100% 意思是和父元素一样高. 要在父元素的基础上去掉标题编辑区的高度 height: "calc(100% - 50px)", // 编辑器中的初始内容 markdown: "# 在这里写下一篇博客", // 指定 editor.md 依赖的插件路径 path: "editor.md/lib/", // 加上这个属性使 编辑器 的内容能保存到用户自己添加的 textarea 中. saveHTMLToTextarea: true, });
部署程序, 验证效果.
实现删除博客
进入用户详情页时, 如果当前登陆用户正是文章作者, 则在导航栏中显示 “删除” 按钮, 用户点击时则删除 该文章.
需要实现两件事:
- 判定当前博客详情页中是否要显示 “删除” 按钮
- 实现删除逻辑.
约定前后端交互接口
- 判定是否要显示删除按钮
修改之前的 获取用户 信息的接口, 在响应中加上一个字段.
- isYourBlog 为 true 表示当前博客就是登陆用户自己写的.
[请求] GET /user?blogId=1 [响应] { userId: 1, username: test, isYourBlog: 1, // 1 表示当前博客就是登陆者的博客. 0 表示当前博客不是登陆者的博客. }
- 删除博客
- 使用 DELETE 请求表示删除一个博客.
[请求] DELETE /blog?blogId=1 [响应] HTTP/1.1 200
实现服务器代码
- 给 User 类新增一个字段
public class User { private int userId; private String username; private String password; // 这个字段只是在判定博客详情页是否显示删除按钮时使用. private int isYourBlog; }
- 修改 UserServlet
其他代码不变. 只处理 “博客详情页” 中的逻辑.
// 获取指定文章作者的用户信息 BlogDao blogDao = new BlogDao(); Blog blog = blogDao.selectOne(Integer.parseInt(blogId)); UserDao userDao = new UserDao(); User author = userDao.selectById(blog.getUserId()); author.setYourBlog(author.getUserId() == user.getUserId() ? 1 : 0); jsonString = objectMapper.writeValueAsString(author);
- 修改 BlogServlet
- 增加 doDelete 方法, 处理删除逻辑.
逻辑和之前版本基本相同. 但是此处删除完毕不必返回 302 了, 由客户端自己决定重定向逻辑.
protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 1. 验证用户是否登陆 User user = Util.checkLoginStatus(req); if (user == null) { resp.setStatus(403); return; } // 2. 读取要删除的 blogId String blogId = req.getParameter("blogId"); if (blogId == null) { String html = "
blogId 参数错误!
"; resp.getWriter().write(html); return; } // 3. 从数据库中删除博客 BlogDao blogDao = new BlogDao(); blogDao.delete(Integer.parseInt(blogId)); // 4. 返回响应数据 resp.setStatus(200); }实现客户端代码
修改 blog_content.html
- 修改 changeUser 函数, 当获取到的响应中的 isYourBlog 为 true 的时候, 则在导航上添加一个a标签作为删除按钮.
- 当点击删除按钮的时候, 给服务器发送一个 ajax 请求.
function changeUser(user) { var h3 = document.querySelector(".card h3"); h3.innerHTML = user.username; if (user.isYourBlog) { // 显示删除按钮 var navDiv = document.querySelector(".nav"); var delBtn = document.createElement("a"); delBtn.innerHTML = "删除"; delBtn.href = "#"; delBtn.onclick = deleteBlog; navDiv.appendChild(delBtn); } } function deleteBlog() { // 使用 ajax 给服务器发送一个 DELETE 请求 ajax({ url: "blog" + location.search, method: "DELETE", callback: function (data, status) { if (status == 200) { // 重定向到博客列表页 location.assign("blog_list.html"); } else { console.log("status error! " + status); } } }) }
部署程序, 验证效果.
总结
服务器渲染和客户端渲染(前后端分离) 都是常见的 web 开发的方式. 目前 前后端分离 的方式更主流一 些.
主要原因:
- 前后端分离更便于分工协作: 开发开始时, 前端工程师和后端工程师共同约定好交互接口, 然后就可 以分别开发, 各自测试.直到最终双方开发完毕再在一起联调.
- 网络带宽越来越大: 因此渲染一个页面多使用几个 HTTP 请求-响应 也问题不大.
- 用户主机的计算能力越来越强: 无论是手机还是PC, 算力都在突飞猛进的增长. 因此这样的渲染工作 对于客户端来说不是什么负担,
但是能降低服务器的负荷. - 更便于多端开发: 比如同一份服务器代码, 就既可以给网页端提供服务, 也可以给手机app 提供服务.
在前后端分离的模式下, 约定前后端交互接口是一件至关重要的事情. 约定的方式也有很多种. 其中一种 比较流行的方式称为 “Restful 风格”
- 使用不同的 HTTP 方法, 表示要执行的动作. 例如 GET 用于获取数据, POST 用于新增数据, PUT 用 于修改数据,DELETE 用于删除数据.
- 使用 URL 中的 PATH 表示要操作的资源.
- 使用响应的状态码表示不同的响应结果.
- 使用 JSON 格式作为 body 中的数据组织方式.
我们上面的代码模仿了 Restful 风格, 但是还不算特别严格. 比如我们在提交博客的时候不是使用JSON 格式的数据.
实际开发的时候也不必完全拘泥于这样的格式. 都可以灵活对待.