当前位置: 首页 > news >正文

【JavaEE】博客系统前后端交互

目录

一、准备工作

二、数据库的表设计

三、封装JDBC数据库操作

1、创建数据表对应的实体类

2、封装增删改查操作

 四、前后端交互逻辑的实现

1、博客列表页

1.1、展示博客列表

1.2、博客详情页 

1.3、登录页面

1.4、强制要求用户登录,检查用户的登录状态

1.5、实现显示用户信息的功能

1.5.1、针对列表页进行处理

1.5.2、针对详情页进行处理

1.6、用户退出登录功能

1.7、实现发布博客功能


一、准备工作

 

1️⃣创建新的maven项目

2️⃣引入依赖

我们需要使用的依赖有servlet、Jackson、MySQL。在中央仓库中搜索Java Servlet API,选择3.1.0版本,将maven中的代码复制到pom.xml中。

 搜索点击Jackson Databind,引入Jackson没有特定的版本,随便选一个版本的maven代码复制到pom.xml的<dependencies></dependencies>标签中。 

引入数据库,搜索MySQL选择5.1版本的maven中的代码,复制到pom.xml的<dependencies></dependencies>标签中。

引入依赖之后可能代码中会出现爆红,这个时候点击刷新,出发一下下载即可。 

3️⃣创建必要的目录

目录创建好之后,需要 给web.xml中写入指定配置。

<!DOCTYPE web-app PUBLIC"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN""http://java.sun.com/dtd/web-app_2_3.dtd" ><web-app><display-name>Archetype Created Web Application</display-name>
</web-app>

二、数据库的表设计

我们之前的设计的博客前端页面中有博客详情页,博客编辑页,博客列表页,博客登录页。这些页面中,需要使用数据存储的数据主要有两部分,一个是编写的博客数据,一个是用户数据。这里的建表操作和我们之前的直接在数据库中间表的方式有一些区别。首先我们在blog_system项目的main目录下创建一个.sql文件,用来保存建表的过程。如果我们写的服务器需要部署到不同的机器上,就需要在对应的主机上也将数据库建号。这个时候我们只需要将这里的代码拷贝到数据中就可以了。

-- 一般对于建表的sql都会单独用一个 .sql文件来保存
-- 后续程序可能需要在不同的主机上部署,部署的时候就需要在对应的主机上把数据库也给建好。
-- 把建表sql保存好,方便在不同的机器上进行建库建表。--表示当前电脑的数据库中不存在blog_sysyem这个库就创建,存在就不创建了。
create database if not exists blog_system;use blog_system;--表示如果这个库中有blog表,就先删除这个表
drop table if exists blog;
create table blog(blogId int primary key auto_increment,title varchar(128),content varchar(4096), --正文userId int,posTime datetime
);drop table if exists user;
create table user(userId int primary key auto_increment,username varchar(50) unique,password varchar(50)
);

上述我们在建表的时候,先删除库中存在的我们要创建的表,是为了清空之前残留的数据。

由于我们的博客系统并没有实现注册功能,所以小编事先在数据库中存入两个用户信息。


三、封装JDBC数据库操作

JDBC中提供了简单的API,但是我们写的类的太多,如果每个类都需要初始化数据源、建立连接、关闭资源,那就太麻烦了。所以这里我们将这些操作封装到一个类中,需要使用的时候,直接调用封装好的方法。

package model;import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;public class DBUtil {//这个类中提供DataSource,DataSource对于一个项目来说,有一个就行了(单例)//DataSource是用来描述数据源的,也就是用来描述数据库服务器在哪里。//实例并初始化数据源private static volatile 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/blog_system?characterEncoding=utf8&&useSSL=false");((MysqlDataSource)dataSource).setUser("root");((MysqlDataSource)dataSource).setPassword("991218zf");}}}return dataSource;}//建立连接public static Connection getConnection() throws SQLException {//这里调用getDataSource方法的作用就是没有实例化数据源,就会被实例先一下return getDataSource().getConnection();}//关闭资源public static void close(Connection connection, PreparedStatement statement, ResultSet resultSet) {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();}}}
}

需要了解的是,我们实例并初始化数据源的时候,使用了单例模型中的懒汉模式。懒汉模式在不加锁的情况下,是多线程不安全的。

单例模型是指我们在一个项目中某个类只能有一个实例,也就是我们这里DataSource,它在我们的项目中只需要出现实例一次,只需要描述一次数据库服务器所在位置。

这里使用懒汉模式有两个线程问题,一个是保证创建MysqlDataSource实例时的原子性,一个是保证内存可见性

  1. 保证原子性,就是让我们在实例化MysqlDataSource时,不会出现创建多个MysqlDataSource对象,针对DBUtil类对象进行加锁,一个线程在创建对象的时候,其他线程阻塞等待。当一个对象创建好之后,下次需要使用的时候,就不需要再创建这个对象了,也就不需要进程加锁了,所以这里在外层添加了一个if判断,如果MysqlDataSource对象存在,直接返回创建好的对象。因为加锁会导致程序执行速度变慢,所以必要的时候加锁,不必要的时候就不需要加锁。
  2. 保证内存可见性,使用volatile关键字,防止在读取数据表中的数据时,从内存中读数据的操作被编译器优化掉,而另一个线程修改数据的时候,读取数据的线程感知不到。

1、创建数据表对应的实体类

这里我们在数据库中已经创建好了两个表,一个用来表示博客信息,一个用来表示用户信息。但是我们还需要在Java代码中,创建对应的类,表示这两个实体,比如创建Blog类,Blog类的每个对象,就代表数据库的一个记录。

1️⃣用户信息类(User)

package model;
/*
* 这个类表示数据库中user表的内容
* 每个user对象,就对应user表中的一条记录
* */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;}
}

2️⃣博客信息类(Blog)

package model;
/*
* 这里类表示数据库中Blog表的内容
* 没给Blog对象,就对应blog表中的一条记录
* */import java.sql.Timestamp;
import java.text.SimpleDateFormat;public class Blog {private int bolgId;private String title;private String  content;private int userId;private Timestamp postTime;public int getBolgId() {return bolgId;}public void setBolgId(int bolgId) {this.bolgId = bolgId;}public String getTitle() {return title;}public void setTitle(String title) {this.title = title;}public String getContent() {return content;}public void setContent(String content) {this.content = content;}public int getUserId() {return userId;}public void setUserId(int userId) {this.userId = userId;}public String getPostTime() {SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//format方法是用来转换时间戳为上述规定的格式。return format.format(postTime);}public void setPostTime(Timestamp postTime) {this.postTime = postTime;}
}

2、封装增删改查操作

这里我们创建两个类分别为BlogDao和UserDao,这里的Dao是 Data Access Object的缩写,表示的意思是数据访问对象,通过这个类的对象来访问数据。所以这里我们通过在这两个类的方法中封装JDBC来操作数据库。

1️⃣BlogDao

package model;import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;public class BlogDao {//把一个Blog对象插入到数据库中public void insert(Blog blog){Connection connection = null;PreparedStatement statement = null;try{//1.建立连接connection = DBUtil.getConnection();//2.构造sqlString sql = "insert into blog values(null,?,?,?,?)";statement = connection.prepareStatement(sql);statement.setString(1, blog.getTitle());statement.setString(2, blog.getContent());statement.setInt(3, blog.getUserId());//如果数据库表里面是datetime类型,插入数据的时候,按照TimeStamp来插入或者按照格式化时间来插入都是可以的statement.setString(4, blog.getPostTime());//3.执行sqlstatement.executeUpdate();}catch(SQLException e){e.printStackTrace();}finally{DBUtil.close(connection,statement,null);}}//查询blog表中所有的博客数据public List<Blog> selectAll() {List<Blog> blogs = new ArrayList<>();Connection connection = null;PreparedStatement statement = null;ResultSet resultSet = null;try {//1.建立连接connection = DBUtil.getConnection();//2.构造并执行sqlString sql = "select * from blog order by postTime desc";statement = connection.prepareStatement(sql);resultSet = statement.executeQuery();//遍历结果while(resultSet.next()){//这里的resultSet是一个结果集Blog blog = new Blog();blog.setBolgId(resultSet.getInt("blogId"));blog.setTitle(resultSet.getString("title"));//先把博客的正文取出来String content = resultSet.getString("content");//判断如果正文超过了100,从0到100截取出来。if(content.length() > 100){content = content.substring(0,100)+"...";}//最后将这个content放入到blog对象中。最后通过响应显示在博客简介中blog.setContent(content);blog.setUserId(resultSet.getInt("userId"));blog.setPostTime(resultSet.getTimestamp("postTime"));//将每次遍历到的结果都放在list中保存。blogs.add(blog);}} catch (SQLException e) {e.printStackTrace();}finally{DBUtil.close(connection,statement,resultSet);}return blogs;}//指定一个博客id来查询对应的博客public Blog selectOne(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();if(resultSet.next()){Blog blog = new Blog();blog.setBolgId(resultSet.getInt("blogId"));blog.setTitle(resultSet.getString("title"));blog.setContent(resultSet.getString("content"));blog.setUserId(resultSet.getInt("userId"));blog.setPostTime(resultSet.getTimestamp("postTime"));return blog;}} catch (SQLException e) {e.printStackTrace();} finally{DBUtil.close(connection,statement,resultSet);}return null;}//指定一个博客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);}}
}

2️⃣UserDao

package model;import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;public class UserDao {//根据用户id进行查询public User selectUserById(int userId){Connection connection = null;PreparedStatement statement = null;ResultSet resultSet = null;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 user = new User();user.setUserId(resultSet.getInt("userId"));user.setUsername(resultSet.getString("username"));user.setPassword(resultSet.getString("password"));return user;}} catch (SQLException e) {e.printStackTrace();} finally{DBUtil.close(connection,statement,resultSet);}return null;}//根据用户名进行查询public User selectUserByName(String username){Connection connection = null;PreparedStatement statement = null;ResultSet resultSet = null;try {connection = DBUtil.getConnection();String sql = "select * from user where username = ? ";statement = connection.prepareStatement(sql);statement.setString(1,username);resultSet = statement.executeQuery();if(resultSet.next()){User user = new User();user.setUserId(resultSet.getInt("userId"));user.setUsername(resultSet.getString("username"));user.setPassword(resultSet.getString("password"));return user;}} catch (SQLException e) {e.printStackTrace();} finally{DBUtil.close(connection,statement,resultSet);}return null;}
}

✨这里需要注意的是执行sql语句时,使用了两个方法executeQuery和executeUpdate方法,这两个方法都表示执行sql语句,但是在使用的时机上存在差异。

  1. executeQuery方法用于执行从数据库中检索某些数据的SQL语句。例如select
  2. executeUpdate方法用于执行更新或修改数据库的sql语句。例如:insert into ,update

 四、前后端交互逻辑的实现

1、博客列表页

1.1、展示博客列表

1️⃣约定前后端交互接口

请求:GET/blog :这里的GET表示的是HTTP请求方法,blog表示路径

响应:由于博客列表页中存在多个博客,所以使用数组,来存放这个博客对象。

[{blogId:1,title:"这是标题",content:"这是正文",userId:1,postTime:"2023-07-27 12:00:00"},{blogId:1,title:"这是标题",content:"这是正文",userId:1,postTime:"2023-07-27 12:00:00"},{blogId:1,title:"这是标题",content:"这是正文",userId:1,postTime:"2023-07-27 12:00:00"},
]

2️⃣编写后端代码(BlogServlet类)

/*
*通过这个类,来实现一些后端提供的接口
* */
@WebServlet("/blog")
public class BlogServlet extends HttpServlet {//实例化一个ObjectMapper对象,用来将数据转换为json格式private ObjectMapper objectMapper = new ObjectMapper();@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {BlogDao blogDao = new BlogDao();List<Blog> blogs = blogDao.selectAll();//将从数据库中查询到的数据转换为json格式String respString = objectMapper.writeValueAsString(blogs);//使客户端浏览器区分不同种类的数据,并根据不同的MIME调用浏览器内不同的程序嵌入模块来处理相应的数据。resp.setContentType("application/json;charset=utf8");resp.getWriter().write(respString);}
}

3️⃣修改前端代码

之前写的前端代码的博客列表页,都是写死的,现在我们的博客列表页使用数据库中获取数据,所以就需要我们使用JavaScript根据之前写死的样式来编写标签以及其中的内容。

这是之前写死的博客列表页中的组成一个博客的标签。

根据上述的样式,在回调函数中利用JavaScript编写博客列表页的样式,现在只是对博客列表页的右半部分进行了修改。

        <!-- 右侧信息 --><div class="container-right"></div></div><script>//通过ajax给服务器发请求,获取到所有的博客数据,并且构造到页面上。function getBlogs(){$.ajax({type:'get',  //这是请求的方法url:'blog',  //这是请求的路径success:function(body){//根据返回的响应数据,构造出页面中对应的元素//由于返回的响应的数据是一个application/json,所以jQuery自动的将字符串转化为了数组对象let containerRight = document.querySelector('.container-right')for(let blog of body){//拿到的body数据可以当作数组来使用的let blogDiv = document.createElement("div");blogDiv.className = 'blog';let titleDiv = document.createElement("div");titleDiv.className = 'title';titleDiv.innerHTML = blog.title;let dateDiv = document.createElement('div');dateDiv.className = 'date';dateDiv.innerHTML = blog.postTime;let descDiv = document.createElement("div");descDiv.className = 'desc';descDiv.innerHTML = blog.content;let a = document.createElement("a");a.href = 'blog_detail.html?blogId=' + blog.blogId;a.innerHTML = '查看全文 &gt;&gt;';//把上述标签构造好了之后,还需要组合起来blogDiv.appendChild(titleDiv);blogDiv.appendChild(dateDiv);blogDiv.appendChild(descDiv);blogDiv.appendChild(a);containerRight.appendChild(blogDiv);}}});}getBlogs();</script>

 🍂我们博客显示的发布时间,在页面上显示的是时间戳,这里需要我们在Blog类中对getPostTime方法进行修改,使用SimpleDateFormat类的format方法对时间戳转换成我们设置的格式。

 public String getPostTime() {SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//format方法是用来转换时间戳为上述规定的格式。return format.format(postTime);}

这里的我们设置的格式中小时的HH,一定要大写,使用大写表示的是24小时制,使用的hh表示的是12小时制。

🍂我们写博客的时候,看见的都是最近写的博客都在最上面,所以我们这里设置在数据库中根据博客发布的时间倒序排序,这样在页面中显示的时候就会将最近写的博客放在最上面。

select * from blog order by postTime desc

🍂我们写的博客中,摘要只出现正文的一部分,所以给我们自己博客系统也实现一个,在BlogDao类中selectAll进行一下改造,让其先获取正文部分,然后再对正文内容的长度进行判断,长度超过100,去正文的前100个字最为摘要。

//先把博客的正文取出来String content = resultSet.getString("content");//判断如果正文超过了100,从0到100截取出来。if(content.length() > 100){content = content.substring(0,100)+"...";}//最后将这个content放入到blog对象中。最后通过响应显示在博客简介中blog.setContent(content);

将这三个问题修改完成之后,出现的页面就是这样的。


1.2、博客详情页 

1️⃣约定前后端交互接口

请求:GET /blog?blogId=1;这里后面添加blogId= 1表示的意思就是指定获取某个博客内容

响应:只获取一个博客内容。

{blogId:1,title:"这是一篇博客",content:"这是正文",userId:1,postTime:"2023-07-27 12:00:00"}

2️⃣编写后端代码

这里是否创建新的类,是根据我们约定前后端接口的时候,请求指定的路径来区分的,由于这里我们约定的路径与博客列表页的请求路径是一样,所以博客详情页的代码继续在BlogServlet类中来编写。

这里就是根据请求的query string中是否有blogId来区分是获取一个博客还是获取所有的博客。

mport java.util.List;/*
*通过这个类,来实现一些后端提供的接口
* */
@WebServlet("/blog")
public class BlogServlet extends HttpServlet {//实例化一个ObjectMapper对象,用来将数据转换为json格式private ObjectMapper objectMapper = new ObjectMapper();@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {String blogId = req.getParameter("blogId");//从query string中查询一下看是否有blogId,如果有就认为是查询指定博客;如果没有就是查询所有博客。BlogDao blogDao = new BlogDao();if(blogId == null){List<Blog> blogs = blogDao.selectAll();//将从数据库中查询到的数据转换为json格式String respString = objectMapper.writeValueAsString(blogs);//使客户端浏览器区分不同种类的数据,并根据不同的MIME调用浏览器内不同的程序嵌入模块来处理相应的数据。resp.setContentType("application/json;charset=utf8");resp.getWriter().write(respString);}else{//上述使用getParameter从query string中得到的blogId是一个字符串,这里需要将其转为数字Blog blog = blogDao.selectOne(Integer.parseInt(blogId));String respString = objectMapper.writeValueAsString(blog);resp.setContentType("application/json;charset=utf8");resp.getWriter().write(respString);}}
}

3️⃣修改前端代码

这里需要在博客列表页点击查看全文跳转,跳转后的博客详情页,是由markdown格式构成的数据,所以在前端页面显示的时候需要引入editor.md的依赖,然后使用markdown官方提供的editormd.markdownToHTML方法来对正文进行渲染,然后显示在页面上。

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><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"><!-- 引入jquery --><script src="jquery.min.js"></script><!-- 引入editor.md依赖 --><link rel="stylesheet" href="editor.md/css/editormd.min.css"><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><!-- 导航栏 nav 是导航整个次的缩写 --><div class="nav"><!-- logo --><img src="image/logo.png" alt=""><div class="title">我的博客系统</div><!-- 只是一个空白,用来把后面的链接挤过去 --><!-- 这是一个简单粗暴的写法 --><div class="spancer"></div><a href="blog_list.html">主页</a><a href="blog_edit.html">写博客</a><!-- 这里的地址回头在说 --><a href="#">注销</a></div><!-- 页面的主题部分 --><div class="container"><!-- 左侧信息 --><div class="container-left"><!-- 这个div表示整个用户信息的区域 --><div class="card"><!-- 用户的头像 --><img src="image/head_portrait.jpg" alt=""><!-- 用户名 --><h3>小张学编程</h3><!-- GitHub地址 --><a href="https://github.com">github 地址</a><!-- 统计信息 --><div class="counter"><span>文章</span><span>分类</span></div><div class="counter"><span>2</span><span>1</span></div></div></div><!-- 右侧信息 --><div class="container-right"><h3></h3><div class="date"></div><div id="content"></div></div></div><script>function getBlog(){$.ajax({type:'get',//location.searchurl:'blog'+location.search,success:function(body){//设置博客标题let h3 = document.querySelector('.container-right h3');h3.innerHTML = body.title;//设置博客发布时间let dateDiv = document.querySelector('.container-right .date');dateDiv.innerHTML = body.postTime;//设置正文,正文内容应该是markdown格式的数据//此处要显示的应该是渲染过的markdown的内容,而不是markdown的原始字符串。//第一个参数,是一个html元素的id,接下来渲染的结果机会放到对应的元素中editormd.markdownToHTML('content',{markdown:body.content})}});}getBlog();</script>
</body>
</html>

上述代码中需要注意的是location.search,这个代码可以获取当前的页面的URL中的查询字符串内容。location与document一样是一个全局变量。


1.3、登录页面

1️⃣约定前后端交互接口

这里提交用户名和密码,可以使用form也可以使用ajax,但是form更简单一点,所以我们这里使用form构造请求

请求:

POST/login

Content-Type:application/x-www-form-urlencoded  这种数据的组织类型就是form专属的类型

响应: 

登录成功,直接跳转到主页,302表示重定向

HTTP/1.1 302

Location:blog_list.html 

✨注意:

如果通过302来跳转页面。前端必须使用form,不能使用ajax.如果使用ajax,当收到302响应,不会触发页面跳转。

2️⃣编写后端代码

这里的前端代码由于我们只是使用费form发送请求,所以改动就非常小。

    <!-- 登录页的版心 --><div class="login-container"><!-- 登录对话框 --><div class="login-dialog"><h3>登录</h3><!-- 这里使用form包裹一下 下列内容,便于后续给服务器提交数据 --><form action="login" method="post"><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>

这里我们在每个input标签中添加一个name属性,这个属性的值和id属性值相同。但是他们的作用大不相同。id属性,只是针对html生效,只是用来方便获取到该元素。name属性是针对form表单构造http请求的 

3️⃣编写前端代码

由于我们约定的路径发生了变化,所以我们在编写后端代码的时候,重新创建一个类,用来实现登录页面的后端程序。

package controller;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;@WebServlet("/login")
public class LoginServlet extends HttpServlet {@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {//设置请求传过来的字符服务器以utf8字符集进行解析req.setCharacterEncoding("utf8");//1.从请求中,获取到用户名和密码String username = req.getParameter("username");String password = req.getParameter("password");if(username == null || username.equals("") || password ==null ||password.equals("")){//用户名或者密码不全,登录必然失败String html = "<h3>登录失败!缺少用户名或者密码</h3>";resp.setContentType("text/html;charset=utf8");resp.getWriter().write(html);return;}//2.读取数据库,看这里的用户名和密码,是否和数据库中的匹配UserDao userDao = new UserDao();User user = userDao.selectUserByName(username);if(user == null){//用户名不存在String html = "<h3>用户名或密码错误!</h3>";resp.setContentType("text/html;charset=utf8");resp.getWriter().write(html);return;}if(password.equals(user.getPassword())){//密码错误String html = "<h3>用户名或密码错误!</h3>";resp.setContentType("text/html;charset=utf8");resp.getWriter().write(html);return;}//3.用户名和密码都正确,登录成功,需要设置会话//先要创建一个会话HttpSession session = req.getSession(true);//此处就把用户对象存储到session中了,下次用户访问其他页面,就可以直接拿到会话,进一步就能拿到之前的user对象了session.setAttribute("user",user);//4.返回一个重定向响应,能够跳转到博客列表页resp.sendRedirect("blog_list.html");}
}

1.4、强制要求用户登录,检查用户的登录状态

当用户想要访问博客列表页/详情页/编辑页的时候,必须是登录状态,如果是未登录状态,则直接跳转到登录页,要求用户登录。

在博客列表页/详情页/登录页,页面加载的时候,发起一个ajax请求,通过这个请求,访问服务器,获取到当前的登录状态,如果当前为登录,则跳转到登录页面,如果已登录,则不做任何操作。

1️⃣约定前后端交互接口

  • 请求:

GET/login

  • 响应:

登录成功,就返回一个200这样的响应。body可以不要,登录失败(未登录),就返回403这样的响应。

2️⃣编写后端代码

我们可以直接在之前的LoginServlet类中写一个doGet判定当前的登录状态。

    //通过这个方法,判定用户的登录状态。已登录,返回200.未登录返回403@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {//看当前请求是否已经存在会话,并且当前的会话是否包含user对象。HttpSession session = req.getSession(false);if(session == null){//会话不存在,未登录resp.setStatus(403);return;}//因为当前的getAttribute返回的是一个Object类型的数值,这里我们使用User将其强转User user = (User)session.getAttribute("user");if(user == null){//虽然会话对象存在,但是用户对象没有,也将其判定为未登录状态resp.setStatus(403);return;}//如果会话存在并且用户也存在,这个时候就是已登陆状态//200是默认的状态,这里的这句代码也可以不用写resp.setStatus(200);}
}

3️⃣编写前端代码

由于判定是否登录,每个页面都需要,所以这里我们将这个判定的方法单独取出来,放到一个文件中,其他的页面代码中引用这个外部代码即可。

function getLoginStatus(){$.ajax({type:'get',url:'login',success:function(body){//响应返回200的时候,执行success回调函数//用户已经登录,不用进行任何操作console.log("用户已经登录");},error:function(body){//只要返回的不是2开头的状态码,都会触发error回调函数//assign方法用来跳转到login.html主页location.assign("login.html");}})
}

其他的页面使用这种样式引用即可,但是需要注意的是引入的路径是否正确

<script src="js/app.js"></script>//引入完成之后调用这个方法
getLoginStatus();

1.5、实现显示用户信息的功能

我们编写的页面中博客列表页和博客详情页的左边页面中都有显示用户信息,但是这两个地方显示的不是一个用户信息,在博客列表页显示的是登录用户的信息,但是当用户点击到别人写的博客中,也就是博客详情页,这个时候就会显示本篇博客作者的信息。

所以实现显示用户信息的功能,需要获取两部分的用户信息

  • 一个是博客列表页,此处显示的是登录的用户的信息
  • 一个是博客详情页,显示的是文章作者的信息

所以针对这两部分的数据,就需要分别进行获取

1.5.1、针对列表页进行处理

 1️⃣约定前后端交互接口

在博客列表页,要获取到登录用户的信息

前端部分可以直接复用getLoginStatus方法,之前登录成功之后什么也没有做,这里将其修改一下,登录成功之后,将响应返回的用户名设置到左侧的用户信息中。

后端代码,之前的LoginServlet类中,使用doGet方法判定用户的登录状态,判定登录成功之后只返回了一个200的状态码,这里我们登录成功之后的用户名,返回给前端。

请求:

GET/login

响应:

HTTP/1.1 200Content-Type:application/json;{userId:1username:zhangsan,
}

2️⃣修改后端代码

3️⃣修改后端代码

只有博客列表页显示登录用户的信息,所以这里我们将之间的getLoginStatus方法,复制出来放在blog_list的代码中,对齐进行修改,之前的这个方法中当服务器返回200的时候,这个代码只是打印了日志,什么都没有做,现在我们通过querySelector方法找到h3标签,然后修改h3标签中的内容。

      function getLoginStatus(){$.ajax({type:'get',url:'login',success:function(body){//响应返回200的时候,执行success回调函数//用户已经登录,不用进行任何操作console.log("用户已经登录");//把返回的用户名,设置到页面中。let h3 = document.querySelector('.card h3');//这里的body是一个js对象,就是前面服务器返回的json格式的user对象h3.innerHTML = body.username;},error:function(body){//只要返回的不是2开头的状态码,都会触发error回调函数//assign方法用来跳转到login.html主页location.assign("login.html");}})}

1.5.2、针对详情页进行处理

这里的详情页,用户信息这里显示的是当前文章的作者,首先我们需要根据blogId查询到文章对象,然后拿到文章作者的id,在根据作者id查询对应的作者名字,显示到页面上。

1️⃣约定前后端交互接口

请求

GET/user?blogId=1

响应:

HTTP/1.1 200

Content-Type:application/json

{userId:1,username:"zhangsan"
}

2️⃣编写后端代码

由于我们约定的时候,指定了新的路径,所以我们需要创建新的Servlet类来处理请求。


package controller;import com.fasterxml.jackson.databind.ObjectMapper;
import model.Blog;
import model.BlogDao;
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 java.io.IOException;@WebServlet("/user")
public class UserServlet extends HttpServlet {private ObjectMapper objectMapper = new ObjectMapper();@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {//1、先读取出blogId,由于我们的blogId是通过query string 传递的,所以我们使用getParameter获取String blogId = req.getParameter("blogId");if(blogId == null || blogId.equals("")){//直接返回一个userId为0的对象,因为最终返回的是一个Json数据//此时也是返回json格式比较好,如果返回一个html,前端处理的时候还需要判断//这里new的user对象,是一个空的对象,也就满足了返回一个userId为0的对象String respJson = objectMapper.writeValueAsString(new User());resp.setContentType("application/json;charset=utf8");resp.getWriter().write(respJson);System.out.println("参数给定的blog为空");return;}//2.查询数据库,查询对应的Blog对象BlogDao blogDao = new BlogDao();Blog blog = blogDao.selectOne(Integer.parseInt(blogId));if(blog == null){String respJson = objectMapper.writeValueAsString(new User());resp.setContentType("application/json;charset=utf8");resp.getWriter().write(respJson);System.out.println("参数给定的blog不存在");return;}//3.根据blog中的userId,查询作者信息UserDao userDao = new UserDao();User user = userDao.selectUserById(blog.getUserId());if(user == null){String respJson = objectMapper.writeValueAsString(new User());resp.setContentType("application/json;charset=utf8");resp.getWriter().write(respJson);System.out.println("该博客对应的作者不存在");}//4.把user对象返回给页面String respJson = objectMapper.writeValueAsString(user);resp.getWriter().write(respJson);return;}
}

3️⃣修改前端代码

在前端的博客详情页(blog_detail)中添加getAuthor方法。将之前左侧的用户信息框中的h3标签中写死的内容删除掉。

        function getAuthor(){$.ajax({type:'get',url:'user'+location.search,success:function(body){//把响应中得到的user对象的数据,构造到页面上if(body.userId == 0){//表示服务器没有找到匹配的用户alert("当前未找到作者信息!");return;}//找到了   没有进if表示这是一个合法的user对象let h3 = document.querySelector('.card h3');h3.innerHTML = body.username;}});}getAuthor();

1.6、用户退出登录功能

之前我们在导航栏中写了注销,这是一个a标签,点击这个标签的时候就会触发一个GET请求,服务器收到这个get请求,就可以把当前用户的会话中的user对象给删除掉。这里为什么不直接删除会话session,但是由于servlet中没有提供直接删除会话的操作,只是提供了设置过期时间的方法,在我们的现在的业务需求上不适用,所以这里直接删除session中的user对象,也就实现了退出登录。

1️⃣约定前后端交互接口

  • 请求:

GET/logout

  • 响应:

HTTP/1.1 302

Lcation:login.html

2️⃣编写后端代码

这里我们约定的请求使用新的路径,所以我们需要创建新的Servlet,来实现业务。由于之前的代码中用来判定用户是否登录的时候,判断user对象是否存在,所以这里我们直接将user对象删除,并且重定向到登录页。

package controller;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;@WebServlet("/logout")
public class LogoutServlet extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {HttpSession session = req.getSession(false);if(session == null){//session不存在,说明用户本来就没有登录,这里直接跳转到登录页//重定向resp.sendRedirect("login.html");return;}//拿到session之后 删除session中的user对象即可//这里也不要判断session中的user对象存在不存在,存在删除,不存在也不影响session.removeAttribute("user");//重定向resp.sendRedirect("login.html");}
}

3️⃣编写后端代码

由于我们的a标签本身就可以发送http请求,所以我们只需要在注销所在a标签的href属性设置为我们约定的路径即可


1.7、实现发布博客功能

1️⃣约定前后端交互接口

前端使用form提交博客

  • 请求:

POST/blog

Content-Type:application/x-www-form-urlencoded

body: title=标题&content=......

  • 响应:

HTTP/1.1 302

Location:blog_list.html

发布成功直接跳转到列表页

2️⃣编写后端代码

从请求中拿到标题和正文,从会话中拿到用户的登录状态(作者id)、获取到系统时间。将这些进行拼接,就构成了一个blog对象,然后将这个对象插入到数据库中。这里由于我们约定的时候,路径为blog,所以我们在BlogServlet类中,添加一个doPost方法来处理博客发布的请求。

    @Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {req.setCharacterEncoding("utf8");//1.先从请求中拿到标题和正文String title = req.getParameter("title");String content = req.getParameter("content");if(title == null || title.equals("") || content == null || content.equals("")){//只要其中有一个符合条件进入判断,就认为内容是不全的。没有标题或者内容,博客发布失败String html = "<h3>title 或者 content 为空!新增博客是失败!</h3>";resp.setContentType("text/html;charset=uft8");resp.getWriter().write(html);return;}//2.从会话中拿到作者的idHttpSession session = req.getSession(false);if(session == null){String html = "<h3>当前用户未登录!新增博客是失败!</h3>";resp.setContentType("text/html;charset=uft8");resp.getWriter().write(html);return;}User user = (User)session.getAttribute("user");if(user == null){String html = "<h3>当前用户未登录!新增博客是失败!</h3>";resp.setContentType("text/html;charset=uft8");resp.getWriter().write(html);return;}//3.构造Blog对象Blog blog = new Blog();blog.setUserId(user.getUserId());blog.setTitle(title);blog.setContent(content);blog.setPostTime(new Timestamp(System.currentTimeMillis()));//4.插入blog对象到数据库中BlogDao blogDao = new BlogDao();blogDao.insert(blog);//5.跳转到博客列表页(重定向)resp.sendRedirect("blog_list.html");}
}

3️⃣编写前端代码

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><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-edit.css"><!-- 引入依赖 --><script src="jquery.min.js"></script><link rel="stylesheet" href="editor.md/css/editormd.min.css"><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><!-- 导航栏 nav 是导航整个次的缩写 --><div class="nav"><!-- logo --><img src="image/logo.png" alt=""><div class="title">我的博客系统</div><!-- 只是一个空白,用来把后面的链接挤过去 --><!-- 这是一个简单粗暴的写法 --><div class="spancer"></div><a href="blog_list.html">主页</a><a href="blog_edit.html">写博客</a><!-- 这里的地址回头在说 --><a href="#">注销</a></div><!-- 博客编辑页的版心 --><div class="blog-edit-container"><form action="blog" method="post"><!-- 标题编辑区 --><div class="title"><input type="text" id="title-input" name="title"><input type="submit" id="submit"></div><!-- 博客编辑器 --><!-- 把 mackdown编辑器放到这个div中--><div id="editor"><textarea name="content" system="display:none;"></textarea></div></form></div><script src="js/app.js"></script><!-- 针对editor.md初始化,创建一个编辑器对象,并关联到页面的某个元素中 --><script>var editor =editormd("editor",{// 设置编辑器的宽度和高度width:"100%",height:"calc(100% - 50px)",// 这是编辑器中的初始内容markdowm:"# 在这里写下一篇博客",// 指定editor.md依赖的插件路径path:"editor.md/lib/",saveHTMLToTextarea:true});getLoginStatus();</script>
</body>
</html>

相关文章:

【JavaEE】博客系统前后端交互

目录 一、准备工作 二、数据库的表设计 三、封装JDBC数据库操作 1、创建数据表对应的实体类 2、封装增删改查操作 四、前后端交互逻辑的实现 1、博客列表页 1.1、展示博客列表 1.2、博客详情页 1.3、登录页面 1.4、强制要求用户登录&#xff0c;检查用户的登录状态 …...

Redis 简介

文章目录 Redis 简介 Redis 简介 Redis&#xff08;Remote Dictionary Server&#xff09;&#xff0c;远程词典服务器&#xff0c;基于 C/S 架构&#xff0c;是一个基于内存的键值型 NoSQL 数据库&#xff0c;开源&#xff0c;遵守 BSD 协议&#xff0c;Redis 由 C语言 实现。…...

CS162 13-17 虚拟内存

起源 为啥我们需要虚拟内存-----------需求是啥&#xff1f; 可以给程序提供一个统一的视图&#xff0c;比如多个程序运行同一个代码段的话&#xff0c;同一个kernel&#xff0c;就可以直接共享 cpu眼里的虚拟内存 无限内存的假象 设计迭代过程 为啥这样设计&#xff1f; 一…...

接口自动化测试-Jmeter+ant+jenkins实战持续集成(详细)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 1、下载安装配置J…...

最长连续序列——力扣128

文章目录 题目描述法一 哈希表 题目描述 法一 哈希表 用一个哈希表存储数组中的数&#xff0c;这样查看一个数是否存在即能优化至 O(1) 的时间复杂度 每次在哈希表中检查是否存在 x−1 即能判断是否需要跳过 int longestConsecutive(vector<int>& nums){unordered_s…...

uniapp app端 echarts 设置tooltip的formatter不生效问题以及解决办法

需求一&#xff1a; y轴数据处理不同数据增加不同单位 需求二&#xff1a; 自定义图表悬浮显示的内容 需求一&#xff1a;实现方式 在yAxis里面添加formatter yAxis: [{//y轴显示value的设置axisLabel: {show: true,formatter (value, index) > {var valueif (value > 1…...

Spring入门-技术简介、IOC技术、Bean、DI

前言 Spring是一个开源的项目&#xff0c;并不是单单的一个技术&#xff0c;发展至今已形成一种开发生态圈。也就是说我们可以完全使用Spring技术完成整个项目的构建、设计与开发。Spring是一个基于IOC和AOP的架构多层j2ee系统的架构。 SpringFramework&#xff1a;Spring框架…...

深度学习之反向传播

0 特别说明 0.1 学习视频源于&#xff1a;b站&#xff1a;刘二大人《PyTorch深度学习实践》 0.2 本章内容为自主学习总结内容&#xff0c;若有错误欢迎指正&#xff01; 1 forward&#xff08;前馈运算&#xff09;过程 通过输入相应的x和权重w&#xff08;可能涉及bais偏置…...

网络安全 Day23-mariadb数据库数据管理和备份

mariadb数据库数据管理和备份 1. 管理数据库中的库2. 管理库中的表3. 管理表中的字段(列)4. 管理表中的数据(行)5. 数据库数据备份与恢复 1. 管理数据库中的库 进入指定数据库: use 数据库名字库的增删改查 创建数据库: create database 数据库名字指定字符及创建数据库: CREA…...

Centos7 上安装 redis-dump 和redis-load 命令

一、安装rvm 1、安装GPG keys gpg2 --keyserver keyserver.ubuntu.com --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDBcurl -sSL http://rvm.io/mpapis.asc | gpg2 --import - curl -sSL http://rvm.io/pkuczynski.asc | g…...

【NLP PyTorch】字符级RNN循环网络模型姓氏对应国家分类(项目详解)

字符级RNN模型姓氏对应国家分类 1 序言1 数据来源与加载1.1 数据来源1.2 数据加载2 数据预处理2.1 单个字符数据处理标准2.2 单词的张量构造3 模型创建4 模型训练5 模型检验6 模型预测7 模型部署1 序言 本文的任务主要来源于PyTorch的官方教程,即给定各国人名的数据集,你需要…...

C++设计模式之责任链设计模式

C责任链设计模式 什么是责任链设计模式 责任链设计模式是一种行为型设计模式&#xff0c;它允许多个处理请求的对象串联起来&#xff0c;形成一个处理请求的链。每个对象都有机会处理请求&#xff0c;如果该对象不能处理请求&#xff0c;则将请求传递给链中的下一个对象。 该…...

《Java-SE-第二十三章》之单例模式

文章目录 单例模式概述饿汉模式懒汉模式单线程版懒汉单例多线程版枚举实现单例 单例模式概述 单例模式是设计模式中的一种,其作用能保证某个类在程序中只存在唯一一份实例,而不会创建多份实例。单例模式具体的实现方式, 分成 “饿汉” 和 “懒汉” 两种.。饿汉模式中的饿不并不…...

如何快速同步第三方平台数据?

全量的数据主要是针对多个系统的历史数据,大概有几千万数据,只需要初始化一次即可。 而增量的数据,是系统后续变更的数据。 这个需求其实不简单,至少有以下难点: 不能直接访问第三方数据库。 不能将历史数据导出到excel中,有泄露数据的风险。 如何快速同步历史数据? 增…...

反射(一)

动态 VS 静态语言 动态语言&#xff1a;运行时&#xff0c;可以改变其结构。 Object-C、C#、JS、PHP、Python JS 就是动态语言。 function f() {var x "var a3; var b5; alert(ab)";eval(x); }静态语言&#xff1a;运行时&#xff0c;结构不可变。 Java、C、C J…...

29.利用fminbnd 求解 最大容积问题(matlab程序)

1.简述 用于求某个给定函数的最小值点。 使用方法是&#xff1a; xfminbnd(func,x1,x2) func是函数句柄&#xff0c;然后x1和x2就是函数的区间&#xff0c;得到的结果就是使func取最小值的x值 当然也可以使用[x,fv]fminbnd(func,x1,x2)的方式&#xff0c;这个时候fv就是函数…...

express学习笔记7 - docker跟mysql篇

安装Docker和Navicat Docker 进官⽹https://docs.docker.com/get-docker/ 选择机型安装即可。 Navicat&#xff08;也可以在网上找个破解版本&#xff09; 进官⽹https://www.navicat.com/en/products/navicat-premium 安装完之后连接新建⼀个数据库连接 然后再⾥⾯新建⼀个数…...

Leetcode(一):数组、链表部分经典题目详解(JavaScript版)

数组、链表部分算法题 一、数组1. 二分查找2. 移除数组元素3. 有序数组的平方4. 长度最小的子数组5. 螺旋矩阵 二、链表1. 删除链表元素2. 设计链表3.反转链表4.两两交换链表中的节点5.删除链表倒数第n个节点6.环形链表 提前声明&#xff1a;本博客内容均为笔者为了方便个人理解…...

内网穿透的底层原理是什么

目录 内网穿透的功能 内网穿透的底层原理 内网穿透的功能 前段时间研究了一下内网穿透&#xff0c;果真是一个神奇的技术&#xff0c;就拿企业级内网穿透-神卓互联来说&#xff0c;在需要在本地安装一个神卓互联客户端&#xff0c;简单设置一下服务应用的端口号&#xff0c;就…...

Bash配置文件

当Bash以登录Shell启动的时候&#xff0c;会首先读取并执行文件“/etc/profile”中的命令。 接着&#xff0c;Bash会依次查找文件“~/.bash_profile”&#xff0c;“~/.bash_login”&#xff0c;“~/.profile”&#xff0c;读取并执行找到的第一个文件中的命令。也就是说&…...

python打卡day49

知识点回顾&#xff1a; 通道注意力模块复习空间注意力模块CBAM的定义 作业&#xff1a;尝试对今天的模型检查参数数目&#xff0c;并用tensorboard查看训练过程 import torch import torch.nn as nn# 定义通道注意力 class ChannelAttention(nn.Module):def __init__(self,…...

PL0语法,分析器实现!

简介 PL/0 是一种简单的编程语言,通常用于教学编译原理。它的语法结构清晰,功能包括常量定义、变量声明、过程(子程序)定义以及基本的控制结构(如条件语句和循环语句)。 PL/0 语法规范 PL/0 是一种教学用的小型编程语言,由 Niklaus Wirth 设计,用于展示编译原理的核…...

ios苹果系统,js 滑动屏幕、锚定无效

现象&#xff1a;window.addEventListener监听touch无效&#xff0c;划不动屏幕&#xff0c;但是代码逻辑都有执行到。 scrollIntoView也无效。 原因&#xff1a;这是因为 iOS 的触摸事件处理机制和 touch-action: none 的设置有关。ios有太多得交互动作&#xff0c;从而会影响…...

USB Over IP专用硬件的5个特点

USB over IP技术通过将USB协议数据封装在标准TCP/IP网络数据包中&#xff0c;从根本上改变了USB连接。这允许客户端通过局域网或广域网远程访问和控制物理连接到服务器的USB设备&#xff08;如专用硬件设备&#xff09;&#xff0c;从而消除了直接物理连接的需要。USB over IP的…...

七、数据库的完整性

七、数据库的完整性 主要内容 7.1 数据库的完整性概述 7.2 实体完整性 7.3 参照完整性 7.4 用户定义的完整性 7.5 触发器 7.6 SQL Server中数据库完整性的实现 7.7 小结 7.1 数据库的完整性概述 数据库完整性的含义 正确性 指数据的合法性 有效性 指数据是否属于所定…...

免费PDF转图片工具

免费PDF转图片工具 一款简单易用的PDF转图片工具&#xff0c;可以将PDF文件快速转换为高质量PNG图片。无需安装复杂的软件&#xff0c;也不需要在线上传文件&#xff0c;保护您的隐私。 工具截图 主要特点 &#x1f680; 快速转换&#xff1a;本地转换&#xff0c;无需等待上…...

在Mathematica中实现Newton-Raphson迭代的收敛时间算法(一般三次多项式)

考察一般的三次多项式&#xff0c;以r为参数&#xff1a; p[z_, r_] : z^3 (r - 1) z - r; roots[r_] : z /. Solve[p[z, r] 0, z]&#xff1b; 此多项式的根为&#xff1a; 尽管看起来这个多项式是特殊的&#xff0c;其实一般的三次多项式都是可以通过线性变换化为这个形式…...

Rust 开发环境搭建

环境搭建 1、开发工具RustRover 或者vs code 2、Cygwin64 安装 https://cygwin.com/install.html 在工具终端执行&#xff1a; rustup toolchain install stable-x86_64-pc-windows-gnu rustup default stable-x86_64-pc-windows-gnu ​ 2、Hello World fn main() { println…...

在 Spring Boot 中使用 JSP

jsp&#xff1f; 好多年没用了。重新整一下 还费了点时间&#xff0c;记录一下。 项目结构&#xff1a; pom: <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://ww…...

CSS3相关知识点

CSS3相关知识点 CSS3私有前缀私有前缀私有前缀存在的意义常见浏览器的私有前缀 CSS3基本语法CSS3 新增长度单位CSS3 新增颜色设置方式CSS3 新增选择器CSS3 新增盒模型相关属性box-sizing 怪异盒模型resize调整盒子大小box-shadow 盒子阴影opacity 不透明度 CSS3 新增背景属性ba…...