一.准备工作
1.创建项目,把前端写好的博客静态页面拷贝到webapp目录中
2.引入依赖,这里主要用到servlet,mysql5.1.47,jacson2.15.0
3.找到右上角的edit configurations->smartTomcat->进行配置
4.数据库设计:设计对应的表结构,并把数据库相关代码进行封装。如何设计表结构?首先确定实体,其次确认实体之间的关系。这里的实体就是博客和用户,所以就是一个blog表,一个user表。两者是一对多的关系->一个博客只能属于一个用户,然而一个用户可以拥有多篇博客。所以应在博客表中引入userid。
1.对数据库的操作
先把数据库创建好,并创建好表,为了方便创建,先在webapp下面新建一个文件db.sql,然后再这里面把sql语句编辑好,然后复制粘贴到MySQL中。这样方便修改语法错误(其实直接在MySQL中创建也可以)
然后我们把对数据库进行操作的代码进行封装。我们将对数据库的操作都放到model包中。所以在java目录下新建一个model包
建立连接等通用操作
然后再在model包中新建一个DButil类,用来封装数据库建立连接的代码。这是由于接下来的代码中有很多个servlet都需要使用库,所以就需要有单独的地方把DataSourse的操作进行封装,而不是只放到某个servlet的init中
public class DButil {private static DataSource ds=null;private static DataSource getDataSourse(){if(ds==null){ds=new MysqlDataSource();((MysqlDataSource)ds).setUrl("jdbc:mysql//127.0.0.1:3306/blog_system?charset=utf8&useSSL=false");((MysqlDataSource)ds).setUser("root");((MysqlDataSource)ds).setPassword("20050430zyh");}return ds;}public Connection getConnection() throws SQLException {return getDataSourse().getConnection();}public static void close(ResultSet resultSet, PreparedStatement statement,Connection connection){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();}}}
}
如上,对外只开放建立连接和关闭的操作,建立数据源的操作直接封装起来
两个类表示两张表
每个表都需要专门搞一个类来表示
public class Blog {private int blogid;private String content;private Timestamp postTime;private int userid;public int getBlogid() {return blogid;}public void setBlogid(int blogid) {this.blogid = blogid;}public String getContent() {return content;}public void setContent(String content) {this.content = content;}public Timestamp getPostTime() {return postTime;}public void setPostTime(Timestamp postTime) {this.postTime = postTime;}public int getUserid() {return userid;}public void setUserid(int userid) {this.userid = userid;}@Overridepublic String toString() {return "Blog{" +"blogid=" + blogid +", content='" + content + '\'' +", postTime=" + postTime +", userid=" + userid +'}';}
}
public class User {private int userid;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;}@Overridepublic String toString() {return "User{" +"userid=" + userid +", username='" + username + '\'' +'}';}
}
两个类完成对两张表的操作
就用BlogDao和UserDao来命名。Dao,就是Data Access Object(数据访问对象)通过这两个类的对象完成对数据的操作
public class BlogDao {//新增,提交博客public void insert(Blog blog){Connection connection=null;PreparedStatement statement=null;try {//建立连接connection=DButil.getConnection();//构造sql语句String sql="insert into blog values (null,?,?,now(),?)";statement=connection.prepareStatement(sql);statement.setString(1,blog.getTitle());statement.setString(2,blog.getContent());statement.setString(3,""+blog.getUserid());//执行sqlstatement.executeUpdate();} catch (SQLException e) {e.printStackTrace();}finally {DButil.close(null,statement,connection);}}//查询博客列表(博客列表页),把库中所有博客都拿到public List<Blog> getBlogs(){Connection connection=null;PreparedStatement statement=null;ResultSet resultSet=null;List<Blog> blogList=new ArrayList<>();try {connection=DButil.getConnection();String sql="select * from blog";statement=connection.prepareStatement(sql);resultSet=statement.executeQuery();while(resultSet.next()){Blog blog=new Blog();blog.setBlogid(resultSet.getInt("blogid"));blog.setTitle(resultSet.getString("title"));String content=resultSet.getString("content");if(content.length()>100){content=content.substring(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(resultSet,statement,connection);}return blogList;}//根据博客id查询指定博客public Blog getBlog(int blogid){Connection connection=null;PreparedStatement statement=null;ResultSet resultSet=null;Blog blog=new Blog();try {connection=DButil.getConnection();String sql="select * from blog where blogid=?";statement=connection.prepareStatement(sql);statement.setInt(1,blogid);resultSet=statement.executeQuery();if(resultSet.next()){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"));}} catch (SQLException e) {e.printStackTrace();}finally {DButil.close(resultSet,statement,connection);}return blog;}//根据博客id删除指定博客public void deleteBlog(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(null,statement,connection);}}
}
注意,在getBlogs这个方法中,获取正文时,不要把全部正文内容都拿出来显示到列表页上,只需要拿出一部分即可,所以会有一个截断操作
在getBlog操作中,由于blogid时主键,所以只能查询到一篇博客,所以不需要while循环,只需要一个if判断,同时,正文不用截断!!!
public class UserDao {//根据userid查询对应用户信息(获取用户信息)public User getUserByid(int userid){Connection connection=null;PreparedStatement statement=null;ResultSet resultSet=null;User user=new User();try {connection=DButil.getConnection();String sql="select * from user where userid=?";statement=connection.prepareStatement(sql);statement.setInt(1,userid);resultSet=statement.executeQuery();if(resultSet.next()){user.setUserid(resultSet.getInt("userid"));user.setUsername(resultSet.getString("usernsme"));user.setPassword(resultSet.getString("password"));}} catch (SQLException e) {e.printStackTrace();}finally {DButil.close(resultSet,statement,connection);}return user;}//根据usernsme查询对应用户信息public User getUserByname(String username){Connection connection=null;PreparedStatement statement=null;ResultSet resultSet=null;User user=new User();try {connection=DButil.getConnection();String sql="select * from user where userid=?";statement=connection.prepareStatement(sql);statement.setString(1,username);resultSet=statement.executeQuery();if(resultSet.next()){user.setUserid(resultSet.getInt("userid"));user.setUsername(resultSet.getString("usernsme"));user.setPassword(resultSet.getString("password"));}} catch (SQLException e) {e.printStackTrace();}finally {DButil.close(resultSet,statement,connection);}return user;}
}
上面这两方法就都很相似。
二.前后端交互,实现博客功能
1.获取博客列表页
在博客列表页加载时,通过ajax方式给服务器发送请求,从服务器数据库中拿到数据显示到页面上
约定前后端交互接口
请求
GET /blog
响应
HTTP/1.1 200 OK
Content-Type:application/json
[
{
blogid:1
title:' '
content:
postTime:
userid:
}
……
]
让浏览器给服务器发送请求
我们要对前端的blog_list.html使用ajax进行修改,所以首先要引入jquery库:也就是在head标签内加一个script标签:<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
编写前把写死的那部分标签给注释掉,然后在</body>上方进行代码编写,加一个<script>标签,在开始和结束标签之间进行编写:
我们直接将代码封装到函数中,但一定要记住调用!!
服务器处理请求
我们将所有服务器的代码都放到一个包里面,也就是servlet包,然后在改包中线新建一个BlogServlet类。由于对于博客列表页发送的请求的处理方式是要将表中数据按照json格式字符串返回,所以肯定要用到jackson,所以要定义一个成员变量ObjectMapper,最终代码如下:
让前端代码处理响应
这里的关键是根据服务器返回的博客信息去构造博客列表页!!我们使用的是比较朴素的方式来构造博客列表页,也就是基于dom api。dom api是浏览器提供的标准的api,不属于任何第三方库/框架。
如上,我们要构建出类似于这样结构的标签,这些标签有着包含关系以及并列关系,最大的标签是containner-right,其他所有标签都是在它里面。它里面就是一个一个博客,其中每个博客标签里面又并列包含了标题,发布时间,摘要,查看全文按钮
如上。由于在像一个中设置了正文格式为json,所以当把响应返回给客户端后,body部分就自动被解析成了一个json对象,这里由于响应是一个对象列表转化成的json字符串数组,所以是被解析成了json对象数组,所以要通过for循环遍历数组,拿出每一个blog对象进行遍历。
其中,在创建查看全文按钮时,我们的目的是创建一个类似于”查看全文>>"这样的标签,但是大于号要想在html中写出,就必须使用转义字符>;小于号就是<。因为html标签就是<>构成的。
其次,一点击查看全文,就要进入博客详情页,所以要用到超链接标签,也就是a标签。其中的href属性就是在描述点击后会跳转到哪个页面。同时,点击a标签就会自动发送一个get请求!!!之前我们提到过。
解决出现的问题
到这里,博客列表页就构造完了。我们来测试一下,发现几个问题:1.时间显示不对,现在显示的是时间戳,但我们希望是某年某月某日几点。2.返回的数据的顺序问题,正常来说,最上面的博客应该是最新的博客,但是我们现在再插入一篇博客后,它会被显示到博客列表页的最下面,而不是最上面。
解决时间问题
抓包观察是哪里出现了问题,仔细观察发现在获取博客数据的响应中的postTime就是时间戳。所以是resp.getWriter().write(respJson)出问题了。所以关键就是respJson,它是怎么获取到的呢?
1.首先jackson发现,blogList是一个list,于是就循环遍历了
2.它针对每一个元素(Blog对象)通过反射的方式获取到属性名字,然后再用get方法拿到属性的值。
所以关键就在于修改postTime的get方法,如下:
java标准库中提供了SimpleDateFormet类,来完成时间戳到格式化时间的转化。此类的使用很复杂,不需要背,每次使用前都查一下就行。创建SDF对象,传入指定字符串,用来描述当前时间日期的具体格式,然后使用format方法,里面的参数可以是时间戳,也可以是date对象
解决顺序问题
我们是到,数据库查询到结果的顺序其实是随机的,所以要想固定顺序,就要使用orderby关键字,并按照发布时间降序排列,如下选中部分。
2.获取博客详情页
这个就是在点击查看全文后,就会发送get请求并跳转到了博客详情页(这是我们刚刚编写的a标签实现的逻辑),那我们现在要实现的就是根据请求中的blogid去查询到对应的博客并返回给前端
约定前后端交互接口
请求
GET /blog?blogid=1
响应
HTTP/1.1 200 OK
Content-Type:application/json
{
blogid:1
title:
content:
postTime:
userid:
}
前端使用ajax发起请求
打开blog_list.html,在</div>下面添加script标签,进行代码编写:
url中的blogid是如何得到的呢?这里就是用location.search来拿到当前页面的url中的queryString(因为在设置a标签时,其中的href属性中就带有?blogid=……)。(在浏览器中,ctrl+shift+i就可以打开控制台,输入location.search就可以看到当前页面的queryString语句)注意,location.search拿到的是整个query String语句,包括了问号!!
服务器处理上述请求
上面请求的路径还是blog,所以还是在BlogServlet中处理get方法。但是在获取博客列表页的时候已经写了一个doGet方法了呀,这该怎么办?找两者的不同,发现上面的请求时没有blogid的,而当前这个请求是有blogid的。这就是区别。所以进行如下编辑:
前端将响应数据构造成html片段
首先把containner-right里面的内容给注释掉
然后按这个格式去构造片段:
测试一下,发现了两个问题:
1.写完代码之后,会发现点击某个博客,有点博客详情页里还是那些注释掉的内容。这个问题是浏览器缓存引起的。浏览器在加载页面时,是通过网络获取资源的,但是网络速度很慢,所以浏览器会把已经加载是页面在本地硬盘中保存一份,后续再次访问同一个页面时,就不通过网络加载,而是直接加载本地硬盘中的这一份。那这如何克服呢?前端有专业的解决方案,不过咱们不用关心,只需要ctrl+F5刷新一下即可。
当前的详情页,虽然能够显示正文了,但是显示的正文是markdown的原始数据。正常应该显示md渲染后的效果。这就需要通过引入第三方库来完成:
引入依赖:在blog_editor.html的</head>前面粘贴
然后在刚才的构造页面正文的代码进行修改:
这个editormd是editor.md官方文档上提供的一个全局变量。此方法的第一个参数必须是一个标签的id,但是content标签没有设置id,所以我们要在上面的content标签内部加上一个id属性。第二个参数就是一个js对象:
最终这个函数的效果就是把blog.content这里的原始md数据渲染成html放到content div的内容中
3.实现登录
在login.html点击登陆后,应该给服务器发起一个http请求。服务器处理上述请求,读取用户名和密码,在库中查询匹配,若正确,则成功登录,创建会话,跳转到博客列表页(所以登陆成功就直接进行重定向跳转
约定前后端交互接口
请求
POST /login
Content-Type:application/x-www-form-urlencoded
username= &password=
响应
HTTP/1.1 302
Location:blog_list.html
注意,若使用ajax发送请求,就要再写代码完成跳转;但是使用form表单的话,只要提交成功,就可以直接使用302完成页面跳转
前端发送请求
打开login.html,对下面的代码进行逻辑修改:
修改完就是:
服务器处理请求返回响应
先将请求中的用户名和密码获取到,判断是否为空。然后根据用户名去获取对应的用户,要是没获取到,就说明用户名错误了,若获取到,就说明用户名是对的。然后拿着刚刚获取到的用户的密码和请求中的密码对照一下,要是不一致,就说明密码填错了(不过,不管是密码写错买时用户名写错,我们都统一提示用户名或密码不正确)。最后创建会话,还是参数是true,并且设置会话中的属性user就是user对象。
4.强制要求登录
这个功能就是在博客的列表页,详情页,编辑页去判定当前用户是否已经登陆。若未登录,就强制要求跳转到登录页。所以要在这几个页面中,在页面加载时,给服务器发送ajax请求,从服务器获取到一个登录状态即可
约定前后端交互接口
请求
GET /login
响应
HTTP/1.1 200 OK ->表示成功登录
HTTP/1.1 403 ->表示登录失败
响应的格式可以有很多种,比如返回的都是200,但是正文不一样
前端发起ajax请求
打开blog_list.html,在function getBlogs()下面再写一个方法:
这里success是在返回2开头的状态码时会执行,error则是在返回除2以外的数字开头的状态码就hi执行。location.assign就是强制要求跳转到login.html,这是前端页面跳转的方式。那为啥不待会儿在服务器直接返回个302,直接跳转到登录页呢?因为302这样的响应回到ajax中的error中后,无法被ajax直接处理。除非通过提交form表单或者点击a标签这种触发的http请求,浏览器才可以直接跳转。
处理响应的过程比较简单,所以一起写了
服务器处理请求
注意,未登录状态的判定不单单是看会话是否存在,还有看该会话中是否存放着user(为什么?待会实现退出登录的时候就知道了)
测试一下:直接打开列表页,在列表页刷新一下,就发现跳转到了登录页。
但有个问题,虽然已经登陆了,但是一旦重启服务器,就会被判定为未登录状态。因为登陆状态是根据服务器的内存中的session确定的,重启服务器意味着之前的内存要释放。其实这一设定并不科学,如何解决?以后再说。
现在,博客编辑页和详情页也要执行上述逻辑,所以在vscode中创建一个新目录js,js中创建新文件app.js,将getLoginStqatus函数复制过去。
然后在每个页面的<script>上再来个<script src="js/app.js"></script>,然后在下面的script中调用getLoginStatus即可:
这就是把一些公共的js代码单独提取出来放到某个.js文件中,然后通过html中的script标签来引用这些文件内容,此时就可以在html中调用对应的公共代码了。
5.显示用户信息
在列表页显示当前登录的用户的信息,在详情页显示作者信息。在页面加载时,给服务器发送ajax请求,服务器返回对应的用户信息
约定前后端交互接口
请求
GET /userinfo(详情页则是authorid)
响应
HTTP/1.1 200 OK
Type:application/json
{
userid:
username:
}
前端发起ajax请求
这是博客列表页
这是博客详情页,通过location.search查到对应的博客id,从而查找对应的作者
服务器处理上述请求
首先是针对博客列表页的。最终要将user对象转成json格式的字符串,所以要有一个ObjectMapper。然后,先取出会话,看看会话是否为空,或者会话中是否存储了user,要是没有,就说明未登录,反之就是已经登录了。最后,在显示用户信息时,为保证安全,就应该将密码置为空!!
最后就是处理博客详情页的请求,首先看看请求中是否带有blogid,要不然没法查询。然后就是根据blogid查到对应的博客。再根据博客找到作者,最后返回作者信息
前端将响应构造成html片段
首先是列表页:
应该将用户名显示到<h3>标签处,所以代码如下:
注意,h3标签没有class属性,所以前面不用加点。
然后是详情页,也是要设置到h3标签:
6.注销/退出登录
在博客列表页/编辑页/详情页都有注销按钮,这就是一个a标签,其中有一个href属性,点击就能够触发一个http请求,就能引起浏览器跳转到另一个页面。
我们现在要做的就是修改a标签,让用户一点击它,就能触发一个http请求,服务器收到了就会把会话中的这个user删除掉并跳转到登录页。(那为啥不直接把会话给删除呢?因为servlet没有提供删除会话的操作!!!不过也可以给会话设置一个过期时间,但是不够优雅。session提供了removeAttribute的方法)
约定前后端交互接口
请求
GET /logout
响应
HTTP/1.1 302
location:login.html
前端发起请求
不用写ajax,直接给a标签添加一个href属性即可
服务器处理请求
7.发布博客
输入标题,正文,期望一点击提交就能够发到服务器这边进行保存
约定前后端交互接口
请求
POST /blog
content-Type:application/x-www-form-urlencoded
title= &content=
响应
HTTP/1.1 302
location:blog_list.html
前端发起请求
就是修改这一块,变成:
也就是给title标签加上name属性。那么如何给正文加上name属性?就是在 <div id="editor">这个标签下加上<textarea name="content" style="display:none"></textarea>,textarea就是一个多行编辑器容器,就把name属性加到它上面即可,然后在初始化editormd对象时加上一个对应的属性即可,也就是在下面的图片中的var editor 中加一个属性saveHTMLToTextarea并赋值为true:
上面这个图片修改为:
表示会把用户在编辑器里面的内容自动保存到textarea中,这样,一点提交,就会在form表单中有拷贝