1.针对完成任务
在前端共涉及的四个页面,都需要分别完成“约定前后端交互接口”、“编写服务器代码”、“编写客户端代码”等任务流程。
1.1博客列表页
这个页面需要展示出数据库中的博客列表,按以下开发流程来进行操作
1)约定接口
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 { //从数据库中查询到博客列表,转成JSON格式,然后直接返回即可 BlogDao blogDao=new BlogDao(); List<Blog> blogs=blogDao.selectAll(); //把blogs对象转成JSON格式 String respJson=objectMapper.writeValueAsString(blogs); resp.setContentType("application/json;charset=utf8"); resp.getWriter().write(respJson); } }
postman测试成功:
3)编写客户端代码
这一步需要我们在之前写好的前端代码中进行调整,加入ajax请求,使得前端页面能够与服务器交互,并从数据库中获取到博客列表。
<!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> <link rel="stylesheet" href="css/common.css"> <link rel="stylesheet" href="css/blog_list.css"> </head> <body> <div class="nav"> <img src="image/dd.jpg" alt=""> <span>我的博客系统</span> <!-- 空白元素,用来占位置 --> <div class="spacer"></div> <a href="blog_list.html">主页</a> <a href="blog_edit.html">写博客</a> <a href="#">注销</a> </div> <!-- container作为页面的版心 --> <div class="container"> <!-- 左侧个人信息 --> <div class="left"> <!-- 表示整个用户信息区域 --> <div class="card"> <img src="image/2.jpg" alt=""> <h3>如风暖阳</h3> <a href="#">gitHub地址</a> <div class="counter"> <span>文章</span> <span>分类</span> </div> <div class="counter"> <span>2</span> <span>1</span> </div> </div> </div> <!-- 右侧个人信息 --> <div class="right"> <!-- <div class="blog"> <div class="title"> 我的第一篇博客 </div> <div class="date"> 2022-10-21 21:24:00 </div> <div class="desc"> 刷爆LeetCode!Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolore, hic ipsa veritatis adipisci rem, provident accusantium deserunt soluta magnam distinctio consequatur fugit, neque omnis explicabo deleniti reiciendis magni architecto eaque! </div> <a href="#">查看全文>></a> </div> --> </div> </div> <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script> <script> // 在页面加载的时候, 通过 ajax 给服务器发送数据, 获取到博客列表信息, 并且显示在界面上. function getBlogList() { $.ajax({ type: 'get', url: 'blog', success: function(body) { // 获取到的 body 就是一个 js 对象数组, 每个元素就是一个 js 对象, 根据这个对象构造 div // 1. 先把 .right 里原有的内容给清空 let rightDiv = document.querySelector('.right'); rightDiv.innerHTML = ''; // 2. 遍历 body, 构造出一个个的 blogDiv 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.innerHTML = '查看全文 >>'; // 此处希望点击之后能够跳转到 博客详情页 !! // 这个跳转过程需要告知服务器要访问的是哪个博客的详情页. a.href = 'blog_detail.html?blogId=' + blog.blogId; blogDiv.appendChild(a); // 把 blogDiv 挂到 dom 树上 rightDiv.appendChild(blogDiv); } }, error: function() { alert("获取博客列表失败!"); } }); } getBlogList(); </script> </body> </html>
1.2博客详情页
1)约定前后端交互接口
可以发现在博客详情页的请求中,与博客列表页不同的是多了?blogId=1这样的查询字符串;
该查询字符串在blog_list.html的a链接标签中就加入了
2)编写客户端代码
加入ajax请求,使得博客详情页的内容,可以通过请求,从服务器中根据查询字符串中的信息,找到对应的博客,展示在前端页面上。
该操作还需要注意,博客正文要用markdown格式渲染,所以要引入editor.md依赖
核心代码块:
完整前端代码:
<!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> <link rel="stylesheet" href="css/common.css"> <link rel="stylesheet" href="css/blog_detail.css"> <!-- 引入 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> </head> <body> <div class="nav"> <img src="image/dd.jpg" alt=""> <span>我的博客系统</span> <!-- 空白元素,用来占位置 --> <div class="spacer"></div> <a href="blog_list.html">主页</a> <a href="blog_edit.html">写博客</a> <a href="#">注销</a> </div> <div class="container"> <!-- 左侧个人信息 --> <div class="left"> <!-- 表示整个用户信息区域 --> <div class="card"> <img src="image/2.jpg" alt=""> <h3>如风暖阳</h3> <a href="#">gitHub地址</a> <div class="counter"> <span>文章</span> <span>分类</span> </div> <div class="counter"> <span>2</span> <span>1</span> </div> </div> </div> <!-- 右侧个人信息 --> <div class="right"> <!-- 使用该div来包裹整个博客的内容详情 --> <div class="blog-content"> <!-- 与博客详情页逻辑不同的是,该页面不再需要创建标签, 直接在已经挂在DOM树上的标签中填写内容即可,因为该页面是固定区域的, 而列表页是多个列表 --> <!-- 博客标题 --> <h3></h3> <!-- 博客的时间 --> <div class="date"></div> <!-- 正文 --> <div id="content" style="opacity: 80%"> </div> </div> <script> function getBlogDetail() { $.ajax({ type:'get', //location.search 拿到查询字符串 url:'blog'+location.search, success:function(body) { //body就是一个blog,根据body中的内容来构造页面 //1.构造博客标题 let h3=document.querySelector(".blog-content>h3"); h3.innerHTML=body.title; //2.构造博客发布时间 let dateDiv=document.querySelector('.date'); dateDiv.innerHTML=body.postTime; //3.构造博客正文,用markdown渲染 editormd.markdownToHTML('content',{ markdown:body.content }); } }); } getBlogDetail(); </script> </div> </div> </body> </html>
3)编写服务器代码,在BlogServlet类中修改doGet方法,使得浏览器发来的请求中,如果有查询字符串,就返回博客详情页json数据;如果没有查询字符串,就返回博客列表页json数据。
//通过这个类来处理 /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"); BlogDao blogDao=new BlogDao(); //先尝试获取到req中的blogId参数,如果该参数存在,说明是要求请求博客详情 //如果该参数不存在,说明是要请求博客的列表 String parm=req.getParameter("blogId"); if(parm==null) { //不存在参数,获取博客列表 List<Blog> blogs=blogDao.selectAll(); //把blogs对象转成JSON格式 String respJson=objectMapper.writeValueAsString(blogs); resp.getWriter().write(respJson); }else { //存在参数,获取博客详情 int blogId=Integer.parseInt(parm); Blog blog=blogDao.selectOne(blogId); String respJson=objectMapper.writeValueAsString(blog); resp.getWriter().write(respJson); } } }
效果预览:
1.3登录功能
1)约定接口
登录成功后,跳转到博客列表页。
在前端登录实现部分中,我们使用的是input标签,所以通过form表单的方式来构造登录请求更方便。
2)编写前端代码
主要是加入form表单,并把提交按钮类型改为submit,还需要改提交按钮对应的css样式
blog_login.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>Document</title> <link rel="stylesheet" href="css/common.css"> <link rel="stylesheet" href="css/blog_login.css"> </head> <body> <div class="nav"> <img src="image/dd.jpg" alt=""> <span>我的博客系统</span> <!-- 空白元素,用来占位置 --> <div class="spacer"></div> <a href="blog_list.html">主页</a> <a href="blog_edit.html">写博客</a> <!-- <a href="#">注销</a> --> </div> <div class="login-container"> <form action="login" method="post"> <div class="login-dialog"> <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> </div> </form> </div> </body> </html>
blog_login.css:
.login-container { width: 100%; /* 注意减号两边有空格 */ height: calc(100% - 50px); /* 需要让里面的子元素, 垂直水平居中, 需要用到 flex 布局 */ display: flex; align-items: center; justify-content: center; } .login-dialog { width: 400px; height: 350px; background-color: rgba(255,255,255,0.8); border-radius: 10px; } .login-dialog h3 { text-align: center; padding: 50px 0; } .login-dialog .row { height: 50px; width: 100%; display: flex; align-items: center; justify-content: center; } .login-dialog .row span { /* 把span设置为块级元素方便后续设置尺寸 */ display: block; width: 100px; font-weight: 700; } #username,#password { width: 200px; height: 40px; font-size: 22px; line-height: 40px; padding-left: 10px; border-radius: 10px; } .row #submit { width: 300px; height: 50px; border-radius: 10px; color: white; background-color: rgb(0,128,0); border: none; outline: none; margin-top:50px ; } .row #submit:active { background-color: #666; }
3)编写服务器代码
约定的路径是/login,需要新建一个Servlet类来处理这里的登录请求
@WebServlet("/login") public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("utf8"); resp.setCharacterEncoding("utf8"); //1.获取到请求中的参数 String username=req.getParameter("username"); String password=req.getParameter("password"); System.out.println("username="+username+",password="+password); if(username==null||"".equals(username)||password==null||"".equals(password)) { // 请求的内容缺失, 肯定是登录失败!! resp.setContentType("text/html; charset=utf8"); resp.getWriter().write("当前的用户名或密码为空!"); return; } //2.和数据库中的内容进行比较 UserDao userDao=new UserDao(); User user=userDao.selectByName(username); if(user==null||!user.getPassword().equals(password)) { //用户没有查到或者密码不匹配,登录失败! 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"); } }
效果预览:
1.4检验登录状态
在我们完成了登录功能后,需要对前边两个页面(博客列表页和博客详情页)进行调整,使得这两个页面必须登录后才能访问。
要想实现上述的功能,就需要在博客列表页/详情页加载的时候,通过ajax访问一下服务器,获取当前的登录状态,看看能不能获取到,如果获取到了,就说明当前确实是已经登录了,此时就可以留在这个页面了;如果没有获取到,说明未登录,就需要跳转到登录页面。
1)约定接口
如果登录了就返回当前登录的用户信息,未登录,就直接返回一个userId=0的对象。
2)编写服务器代码
在LoginServlet类中加上doGET方法
//这个方法用来让前端检测当前的登录状态 @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("application/json;charset=utf8"); HttpSession session=req.getSession(false); if(session==null) { //检测下会话是否存在,不存在说明未登录 User user=new User(); resp.getWriter().write(objectMapper.writeValueAsString(user)); return; } User user=(User) session.getAttribute("user"); if(user==null) { //虽然会话存在,但是会话里没有user对象,也视为未登录 user=new User(); resp.getWriter().write(objectMapper.writeValueAsString(user)); return; } //代码执行到这里说明已经登录 //不把密码返回给前端 user.setPassword(""); resp.getWriter().write(objectMapper.writeValueAsString(user)); }
3)编写前端代码
在博客详情页和博客列表页中加入ajax请求,在页面一加载出来的时候就向服务器发送请求来判定登录状态。
因为两个页面都需要进行判断,所以把判断逻辑单独出一个js文件中,让这两个前端代码引入即可。
//这个文件放一些页面公共的代码 function getUserInfo(pageName) { $.ajax({ type:'get', url:'login', success:function(body) { //判定此处的body是不是一个有效的user对象(userId是否为0) if(body.userId&&body.userId>0) { //登录成功,不做处理 console.log("当前用户登录成功! 用户名: " + body.username); }else { //登录失败! //让前端页面跳转到login.html alert("当前您尚未登录!请登录后再访问博客列表!"); location.assign('blog_login.html'); } }, error: function() { alert("当前您尚未登录! 请登录后再访问博客列表!"); location.assign('blog_login.html'); } }); }
1.5正确显示用户信息
该步骤需要注意两点:
在博客列表页的用户名,要根据登录的用户来进行确定
在博客详情页的用户名,要根据博客的作者来确定
1.5.1 博客列表页
因为在数据库中并没有存储个人的头像和文章数量等信息,所以只能对用户名做出变动,因为之前在1.4中完成了登录状态检测的步骤后,也就完成了后端代码,这一步只需要在前端进行微调,在页面是博客列表页时,能够将用户名做出修改即可。
//这个文件放一些页面公共的代码 function getUserInfo(pageName) { $.ajax({ type:'get', url:'login', success:function(body) { //判定此处的body是不是一个有效的user对象(userId是否为0) if(body.userId&&body.userId>0) { //登录成功,不做处理 console.log("当前用户登录成功! 用户名: " + body.username); if(pageName=='blog_list.html') { changeUserName(body.username); } }else { //登录失败! //让前端页面跳转到login.html alert("当前您尚未登录!请登录后再访问博客列表!"); location.assign('blog_login.html'); } }, error: function() { alert("当前您尚未登录! 请登录后再访问博客列表!"); location.assign('blog_login.html'); } }); } function changeUserName(username) { let h3=document.querySelector('.card>h3'); h3.innerHTML=username; }
效果预览:
1.5.2 博客详情页
要想完成博客详情页中用户名的更改,还需要重新约定接口、编写前后端代码。
1)约定接口
2)编写前端代码
关键代码:
前端代码中的两个ajax请求,是异步并发执行的,并不能确定先后顺序,所以在一个函数的回调函数中,调用另一个函数,让其变为串行执行,才能控制顺序。
3)编写服务器代码
@WebServlet("/authorInfo") public class AuthorServlet extends HttpServlet { private ObjectMapper objectMapper = new ObjectMapper(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("application/json; charset=utf8"); // 通过这个方法, 来获取到指定的博客的作者信息. String param = req.getParameter("blogId"); if (param == null || "".equals(param)) { // 参数缺少了. resp.getWriter().write("{ \"ok\": false, \"reason\": \"参数缺失!\" }"); return; } // 根据当前 blogId 在数据库中进行查找, 找到对应的 Blog 对象, 再进一步的根据 blog 对象, 找到作者信息. BlogDao blogDao = new BlogDao(); Blog blog = blogDao.selectOne(Integer.parseInt(param)); if (blog == null) { resp.getWriter().write("{ \"ok\": false, \"reason\": \"要查询的博客不存在!\" }"); return; } // 根据 blog 对象, 查询到用户对象 UserDao userDao = new UserDao(); User author = userDao.selectById(blog.getUserId()); if (author == null) { resp.getWriter().write("{ \"ok\": false, \"reason\": \"要查询的用户不存在!\" }"); return; } // 把 author 返回到浏览器这边 // 注意要把密码给干掉! author.setPassword(""); resp.getWriter().write(objectMapper.writeValueAsString(author)); } }
1.6注销功能
在导航栏中有一个“注销”按钮,当用户点击注销以后,就会在服务器上取消登录状态,并且能够跳转到登录页面。
1)约定前后端交互接口
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.getWriter().write("当前用户尚未登录!无法注销!"); return; } //把这个用户的会话中的信息给删掉即可 session.removeAttribute("user"); resp.sendRedirect("blog_login.html"); } }
用户有一个session,同时session中有一个user属性,两者同时兼备时,就是登陆状态,注销只要把其中一个条件破坏掉即可。
3)客户端代码修改
把博客列表页、博客详情页、博客编辑页中的导航栏中的注销按钮中的herf属性,都做出修改,改成“logout”这个路径。
1.7发布博客
在博客编辑页中,当用户输入了博客标题和正文之后,点击发布,此时就会把博客数据提交到服务器,由服务器存储到数据库中。
1)约定前后端交互接口
2)实现服务器代码
在BlogServlet里面添加一个doPost方法,来处理上述的post请求;
核心操作,就是读取请求中的标题和正文,构造blog,构造Blog对象,并插入数据库
@Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession session=req.getSession(false); if(session==null) { //当前用户未登录,不能提交博客 resp.setContentType("text/html;charset=utf8"); resp.getWriter().write("当前用户未登录,不能提交博客!"); return; } User user=(User) session.getAttribute("user"); if(user==null) { resp.setContentType("text/html;charset=utf8"); resp.getWriter().write("当前用户未登录,不能提交博客!"); return; } //一定要先指定好请求,按照哪种编码来解析 req.setCharacterEncoding("utf8"); //先从请求中取出参数(博客的标题和正文) String title=req.getParameter("title"); String content=req.getParameter("content"); if(title==null||"".equals(title)||content==null||"".equals(content)) { //直接告诉客户端,请求参数不对 resp.setContentType("text/html;charset=utf8"); resp.getWriter().write("提交博客失败!缺少必要的参数!"); return; } //构造Blog对象,把当前的信息填进去,并插入数据库中 //此处要给Blog设置的属性,主要是title,content,userId(作者信息) //postTime和blogId都不需要手动指定,都是插入数据库的时候自动生成的 Blog blog=new Blog(); blog.setTitle(title); blog.setContent(content); //作者id就是当前提交这个博客的用户的身份信息 blog.setUserId(user.getUserId()); BlogDao blogDao=new BlogDao(); blogDao.insert(blog); //重定向到博客列表页 resp.sendRedirect("blog_list.html"); }
3)调整客户端代码
将整个文章内容用form表单套住,提交form表单
<!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>Document</title> <link rel="stylesheet" href="css/common.css"> <link rel="stylesheet" href="css/blog_edit.css"> <!-- 引入 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> </head> <body> <div class="nav"> <img src="image/dd.jpg" alt=""> <span>我的博客系统</span> <!-- 空白元素,用来占位置 --> <div class="spacer"></div> <a href="blog_list.html">主页</a> <a href="blog_edit.html">写博客</a> <a href="logout">注销</a> </div> <!-- 包裹整个博客编辑页内容的顶级容器 --> <div class="blog-edit-container"> <form action="blog" method="post" style="height: 100%;"> <div class="title"> <input type="text" placeholder="在此处输入标题" name="title" id="title"> <!-- <button>发布文章</button> --> <input type="submit" value="发布文章" id="submit"> </div> <!-- 放置md编译器 --> <div id="editor"> <textarea name="content" style="display:none"></textarea> </div> </form> </div> <script> // 初始化编辑器 let editor = editormd("editor", { // 这里的尺寸必须在这里设置. 设置样式会被 editormd 自动覆盖掉. width: "100%", // 设定编辑器高度 height: "calc(100% - 50px)", // 编辑器中的初始内容 markdown: "# 在这里写下一篇博客", // 指定 editor.md 依赖的插件路径 path: "editor.md/lib/", // 此处要加上一个重要的选项, 然后 editor.md 就会自动把用户在编辑器输入的内容同步保存到 隐藏的 textarea 中了! saveHTMLToTextarea: true, }); </script> </body> </html>
注意对css样式的选择器进行调整,保证提交按钮的样式不丢。
1.8删除博客
只有自己能够删除自己的博客,不能够删除别人的博客。
1)约定接口
2)调整前端代码
我们需要在博客详情页中进行判定,当前博客的作者,是否就是登录的用户;
如果是,就在导航栏里显示一个删除按钮,如果不是,就不显示删除按钮。
3)编写服务器代码
@WebServlet("/blogDelete") public class BlogDeleteServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //1.检查当前用户是否登录 HttpSession session=req.getSession(false); if(session==null) { resp.setContentType("text/html;charset=utf8"); resp.getWriter().write("当前尚未登录,不能删除!"); return; } User user=(User) session.getAttribute("user"); if(user==null) { resp.setContentType("text/html;charset=utf8"); resp.getWriter().write("当前尚未登录,不能删除!"); return; } //2.获取到参数中的blogId String blogId=req.getParameter("blogId"); if(blogId==null||"".equals(blogId)) { resp.setContentType("text/html;charset=utf8"); resp.getWriter().write("当前blogId参数不对!"); return; } //3.获取要删除的博客信息 BlogDao blogDao=new BlogDao(); Blog blog=blogDao.selectOne(Integer.parseInt(blogId)); if(blog==null) { resp.setContentType("text/html;charset=utf8"); resp.getWriter().write("当前要删除的博客不存在!"); return; } //4.再次校验,当前的用户是否是博客的作者 if(user.getUserId()!=blog.getUserId()) { resp.setContentType("text/html;charset=utf8"); resp.getWriter().write("当前登录的用户不是作者,没有删除权限"); return; } //5.确认无误,开始删除 blogDao.delete(Integer.parseInt(blogId)); //6.重定向到博客列表 resp.sendRedirect("blog_list.html"); } }