接上次博客:初阶JavaEE(15)(Cookie 和 Session、理解会话机制 (Session)、实现用户登录网页、上传文件网页、常用的代码片段)-CSDN博客
目录
2、 数据库设计:设计好对应的表结构,并且把数据库相关代码进行封装。
在前端代码中处理响应数据,把响应数据写到刚才页面的对应位置上
我们想要写一个博客系统,首先得先梳理一下基本情况:
我们的博客系统主要是四个页面:
1、博客列表页:显示出当前网站上已经存在哪些博客;
2、博客详页:点击列表上的某个博客,就能进入对应的详情页(显示出博客的具体内容);
3、博客编辑页:让用户输入博客内容,并且发送到服务器;
4、登录页。
我们的重点仍然放在前后端交互和后端代码的编写上,具体的前端代码会直接提供给你。
Markdown编辑器
在编辑页我们要实现编写博客的功能不得不用到Markdown编辑器,所以我们先来简单的了解一下Markdown编辑器。
什么是Markdown编辑器?
Markdown编辑器是程序猿们常用的一种用于创建和编辑Markdown格式文本的工具,Markdown是一种轻量级标记语言,通常用于编写纯文本文档,如博客文章、README文件、文档、笔记等。Markdown编辑器可以帮助用户以简单的文本方式编写内容,然后将其转换为HTML或其他格式,以便在Web上发布或共享。
以下是Markdown编辑器的一些主要特点和功能:
-
简洁的语法:Markdown语法非常简单和易于学习。它使用一些符号和约定来表示文本的样式和结构,如标题、列表、链接、粗体、斜体等。例如,使用
#
表示标题,*
或-
表示无序列表。 -
实时预览:许多Markdown编辑器提供实时预览功能,允许用户在编辑时立即查看文本将如何呈现为HTML。这有助于用户查看他们的文本在发布之前的外观。
-
代码块支持:Markdown编辑器通常支持插入代码块,并可以根据编程语言语法高亮显示代码。这对于编写技术文档和教程非常有用。
-
自动保存:编辑器通常会定期自动保存正在编辑的文档,以避免数据丢失。
-
导出格式:Markdown编辑器允许用户将Markdown文档导出为HTML、PDF、纯文本或其他格式,以适应不同的发布和分享需求。
-
嵌入图片和链接:用户可以插入图片和创建超链接,以丰富文档内容和引用其他资源。
-
表格支持:Markdown编辑器通常支持创建和编辑表格,使其适用于数据呈现。
-
多平台兼容性:Markdown文档可以在各种平台上阅读,包括Web浏览器、文本编辑器、博客平台等,而且文档的格式不会因平台而改变。
-
扩展功能:某些Markdown编辑器具有插件或扩展支持,允许用户自定义功能,如数学公式渲染、目录生成等。
常见的Markdown编辑器包括:
- Typora:一款流行的跨平台Markdown编辑器,以其实时预览和简洁的界面而闻名。
- Visual Studio Code:一款开源代码编辑器,可以使用Markdown插件来编辑和预览Markdown文档。
- MarkdownPad:适用于Windows的Markdown编辑器,具有分栏编辑和预览功能。
- Dillinger:基于Web的Markdown编辑器,无需安装,可以直接在浏览器中使用。
Markdown编辑器是一种强大的工具,可用于轻松创建格式化文档,而不需要复杂的排版和格式设置。
Markdown的一些核心语法元素的示例
1、标题:Markdown中表示标题非常简单,只需在文本前面添加井号(#
),井号的数量表示标题级别。
# 这是一级标题
## 这是二级标题
### 这是三级标题
2、列表:Markdown支持有序列表和无序列表。无序列表使用*
、+
或-
表示列表项,而有序列表使用数字和点号表示。例如:
无序列表:
- 项目1
- 项目2
- 项目3
* 项目1
* 项目2
* 项目3
有序列表:
1. 第一步
2. 第二步
3. 第三步
3、链接:创建超链接也很简单,只需使用方括号([]
)括住链接文本,然后使用圆括号(()
)括住链接地址。
[点击这里](http://www.example.com)
4、粗体和斜体:强调文本可以通过使用**
或__
表示粗体,使用*
或_
表示斜体。例如:
粗体:
**这是粗体文本**
斜体:
*这是斜体文本*
5、引用:引用文本可以使用大于号(>)表示。
> 这是引用的文本。
6、代码块:插入代码块使用反引号(````)括住代码,可以指定编程语言以实现代码高亮显示。
def hello_world():
print("Hello, world!")
Markdown编辑器当然不会是我们自己来写,这个工作量是巨大的,我们会直接使用第三方库:editor.md,它是一个开源的项目,来自于GitHub,大家可以自行去下载。
准备工作完成,接下来要完成的任务就是基于上述页面,编写服务器/前后端交互的代码,通过这些代码完成博客系统的完整功能。Let's go!!!
博客系统功能:
- 实现博客列表页:让页面从服务器拿到博客数据(数据库)
- 实现博客详情页:点击博客详情的时候可以从服务器拿到博客的完整数据
- 实现登录功能
- 实现强制要求登录:当下处于未登录的状态下,其他页面:博客列表页、博客详情页、博客编辑页等,会强制跳转到登录页,要求客户登录后才可以使用
- 实现显示用户信息:从服务器获取,博客列表页拿到的是当前登录的用户的信息,博客详情页拿到的是文章作者的信息。
- 实现退出登录
- 发布博客:在博客编辑页输入标题和内容之后点击发布,就能把这个数据上传到服务器上并保存。
博客系统
1、创建项目,引入依赖,把前端页面导入到项目中:
<dependencies>
<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
</dependencies>
我们可以先启动一下看看效果:
好的,看起来没啥问题。
2、 数据库设计:设计好对应的表结构,并且把数据库相关代码进行封装。
先找到实体:
博客表 (blog):
- blog_id (主键):唯一标识每篇博客的ID。
- title:博客标题。
- content:博客正文内容。
- post_time:博客发布时间。
- user_id:外键,与用户表中的用户ID关联,用于指示博客的作者是哪个用户。
用户表 (user):
- user_id (主键):唯一标识每个用户的ID。
- username:用户的用户名。
- password:用户的密码(需要安全加密存储,不建议明文存储密码)。
确认实体之间的关系:一对多:
一个博客只能属于一个用户;
一个用户可以发布多个博客。
所以我们就应该在博客表中引入一个user ID这样的属性。
先创建一个SQL的文件:
create database if not exists java_blog_system charset utf8;
use java_blog_system;
drop table if exists blog;
drop table if exists user;
create table blog (
blogId int primary key auto_increment,
title varchar(1024),
content varchar(4096),
postTime datetime,
userId int
);
create table user (
userId int primary key auto_increment,
username varchar(50) unique, -- 用户名也要求是不能重复的.
password varchar(50)
);
-- 插入一些测试数据, 方便后续进行测试工作
insert into blog values (1, '这是第一篇博客', '# 从今天开始我要认真写代码', now(), 1);
insert into blog values (2, '这是第二篇博客', '# 从昨天开始我要认真写代码', now(), 1);
insert into blog values (3, '这是第三篇博客', '# 从前天开始我要认真写代码', now(), 1);
insert into user values (1, '酒鬼阿婷', '222');
insert into user values (2, '阿茨拉斐尔', '666');
我们还需要打开MySQL,把这些代码复制进去:
3、把数据库操作的代码进行一些封装:
我们先在java目录里面创建一个包,专门用来存储数据库相关的操作:
这里给大家简单介绍一下model模型:
模型-视图-控制器(Model-View-Controller,MVC)是一种常见的代码组织结构,用于网站开发和应用程序设计。它帮助开发人员将应用程序的不同部分分离开来,以提高代码的可维护性和可扩展性。
下面是对MVC结构的简单介绍:
模型 (Model):模型负责管理应用程序的数据和业务逻辑。它包括数据的结构和规则,以及与数据交互的方法。模型通常是与数据库交互的部分,用于执行数据的读取、写入、更新和删除等操作。它独立于用户界面,确保数据的完整性和一致性。在Web应用中,模型通常用于定义数据表和对象以及它们的操作方法。
视图 (View):视图负责用户界面的呈现。它将数据从模型中获取,并以用户友好的方式显示给用户。视图通常包括HTML模板、CSS样式和前端JavaScript代码,用于构建页面布局和渲染内容。视图独立于业务逻辑,允许多个不同的视图呈现相同的数据。
控制器 (Controller):控制器是连接模型和视图的部分,它处理用户的请求和指导应用程序的响应。控制器接收来自用户界面的输入,然后根据应用程序的逻辑和规则调用适当的模型方法来处理数据。它还可以根据操作结果选择适当的视图来显示给用户。控制器充当应用程序的协调员。
MVC结构的好处包括:
- 分离关注点:MVC将应用程序的不同关注点分开,使得每个部分可以单独开发、测试和维护。
- 可扩展性:添加新功能或更改现有功能变得更容易,因为不必修改所有部分。
- 可重用性:模型和视图可以在不同的上下文中重用,以提高效率。
- 清晰的逻辑分层:MVC鼓励将代码逻辑分层,使代码更易于理解和组织。
需要注意的是,虽然MVC是一种有用的设计模式,但实际开发中可能会采用不同的变种或其他架构模式,如MVVM(Model-View-ViewModel)和MVP(Model-View-Presenter)。每个应用程序的需求和技术堆栈都不同,因此应该根据具体情况选择合适的架构。不过,MVC的核心思想仍然对于理解和构建应用程序非常有帮助。
MVC是非常古老的概念,我们现在编写代码不会完全遵守,但是我们可以进行一些参考。
封装数据库建立连接的操作:
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;
// 通过这个类, 封装数据库建立连接的操作.
// 由于接下来代码中, 有多个 Servlet 都需要使用数据库. 就需要有一个单独的地方来把 DataSource 这里的操作进行封装.
// 而不能只是放到某个 Servlet 的 init 中了.
// 此处可以使用 单例模式 来表示 dataSource
public class DBUtil {
private volatile static 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/java_blog_system?characterEncoding=utf8&useSSL=false");
((MysqlDataSource) dataSource).setUser("root");
((MysqlDataSource) dataSource).setPassword("2222");
}
}
}
return dataSource;
}
public static Connection getConnection() throws SQLException {
return getDataSource().getConnection();
}
public static void close(Connection connection, PreparedStatement statement, ResultSet resultSet) {
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
if (statement != null) {
try {
statement.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
}
懒汉模式,是线程不安全的。当前 Servlet 本身就是在多线程环境下执行的。
而Tomcat 收到多人请求的时候, 就会使用多线程的方式,执行不同的 Servlet 代码。
所以很有可能触发多线程安全问题,所以我们进行了加锁操作,还进行了一系列优化操作。
这段代码主要是用于数据库连接的封装和管理,它采用了单例模式,以确保只有一个数据库连接池实例:
- 数据库连接池:我们的这段代码封装了一个数据库连接池,以便在多个Servlet中共享和复用数据库连接,而不需要每次都重新建立连接。这提高了数据库访问效率。
- 单例模式:使用单例模式确保只有一个数据库连接池实例。这是通过使用volatile关键字和双重检查锁定来实现的,以防止多线程环境下的多次创建实例。
- 初始化数据源:在我们第一次调用getDataSource()方法时,会创建一个MysqlDataSource对象,并设置其连接参数,如数据库URL、用户名和密码。这个数据源对象会在后续的连接请求中被重复使用。
- 获取数据库连接:getConnection()方法用于从数据源中获取数据库连接,使应用程序可以执行SQL查询和更新操作。我们后续直接调用这个方法即可,相当于把getDataSource()封装进去了。
- 关闭资源:close()方法按照顺序关闭数据库连接、PreparedStatement和ResultSet等资源。它处理了异常情况,将SQLException包装为RuntimeException抛出,以便上层代码可以捕获和处理。而且我们分开执行,如果前面抛了异常也不会影响到我们后面代码的执行情况。
总体来说,这段代码的思想是将数据库连接的创建和管理抽象化,以便在整个应用程序中重复使用连接,减少了数据库连接的开销。这是一种常见的最佳实践,特别是在我们Web应用中,多个Servlet可能需要与数据库交互,而共享一个数据库连接池可以提高性能和资源利用率。
创建实体类——Blog、User
每个表都需要搞一个专门的类来表示,表里的每一条数据就会对应到这个类的一个对象。这样我们就可以把数据库中的数据和代码联系起来了。
Blog:
package model;
import java.sql.Timestamp;
// Blog 对象就是对应到 blog 表中的一条记录.
// 表里有哪些列, 这个类里就应该有哪些属性
public class Blog {
private int blogId;
private String title;
private String content;
private Timestamp postTime;
private int userId;
public int getBlogId() {
return blogId;
}
public void setBlogId(int blogId) {
this.blogId = blogId;
}
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 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;
}
@Override
public String toString() {
return "Blog{" +
"blogId=" + blogId +
", title='" + title + '\'' +
", content='" + content + '\'' +
", postTime=" + postTime +
", userId=" + userId +
'}';
}
}
User:
package model;
// 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;
}
@Override
public String toString() {
return "User{" +
"userId=" + userId +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
}
针对博客表和用户表的增删改查操作
我们还需要创建两个类,来完成针对博客表和用户表的增删改查操作:
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;
// 通过 BlogDao 来完成针对 blog 表的操作
public class BlogDao {
// 1. 新增操作 (提交博客就会用到)
public void insert(Blog blog) {
Connection connection = null;
PreparedStatement statement = null;
try {
// 1. 建立连接
connection = DBUtil.getConnection();
// 2. 构造 SQL
String sql = "insert into blog values (null, ?, ?, now(), ?)";
statement = connection.prepareStatement(sql);
statement.setString(1, blog.getTitle());
statement.setString(2, blog.getContent());
statement.setInt(3, blog.getUserId());
// 3. 执行 SQL
statement.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException();
} finally {
DBUtil.close(connection, statement, null);
}
}
// 2. 查询博客列表 (博客列表页)
// 把数据库里所有的博客都拿到.
public List<Blog> getBlogs() {
List<Blog> blogList = new ArrayList<>();
Connection connection = null;
PreparedStatement statement = null;
ResultSet resultSet = null;
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"));
// 此处读到的正文是整个文章内容. 太多了. 博客列表页, 只希望显示一小部分. (摘要)
// 此处需要对 content 做一个简单截断. 这个截断长度 100 这是拍脑门出来的. 具体截取多少个字好看, 大家都可以灵活调整.
String content = resultSet.getString("content");
if (content.length() > 100) {
content = content.substring(0, 100) + "...";
}
blog.setContent(content);
blog.setPostTime(resultSet.getTimestamp("postTime"));
blog.setUserId(resultSet.getInt("userId"));
blogList.add(blog);
}
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
DBUtil.close(connection, statement, resultSet);
}
return blogList;
}
// 3. 根据博客 id 查询指定的博客
public Blog getBlog(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();
// 由于此处是拿着 blogId 进行查询. blogId 作为主键, 是唯一的.
// 查询结果非 0 即 1 , 不需要使用 while 来进行遍历
if (resultSet.next()) {
Blog blog = new Blog();
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"));
return blog;
}
} catch (SQLException throwables) {
throwables.printStackTrace();
} finally {
DBUtil.close(connection, statement, resultSet);
}
return null;
}
// 4. 根据博客 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 throwables) {
throwables.printStackTrace();
} finally {
DBUtil.close(connection, statement, null);
}
}
}
UserDao:
package model;
import java.lang.ref.PhantomReference;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
// 通过 UserDao 完成针对 user 表的操作
public class UserDao {
// 新增暂时不需要. 不需要实现 "注册" 功能.
// 删除暂时不需要. 不需要实现 "注销帐户" 功能.
// 1. 根据 userId 来查到对应的用户信息 (获取用户信息)
public User getUserById(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 throwables) {
throwables.printStackTrace();
} finally {
DBUtil.close(connection, statement, resultSet);
}
return null;
}
// 2. 根据 username 来查到对应的用户信息 (登录)
public User getUserByName(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 throwables) {
throwables.printStackTrace();
} finally {
DBUtil.close(connection, statement, resultSet);
}
return null;
}
}
所谓的DAO就是Data Access Object,数据访问对象。
DAO,即数据访问对象(Data Access Object),是一种设计模式,用于分离数据存储和业务逻辑。它的主要目的是将数据库操作封装到一个独立的组件中,从而简化数据访问并提高代码的可维护性和可测试性。
主要思想和功能包括:
-
数据访问抽象:DAO 提供了一个抽象的接口,定义了应用程序可以执行的各种数据库操作,如插入、查询、更新、删除等。
-
数据访问实现:针对特定数据库引擎的实际数据访问逻辑在 DAO 实现中编写。这些实现通常包括将数据库查询转换为SQL语句、执行SQL语句、处理结果集等操作。
-
分离业务逻辑:DAO 将数据访问逻辑从业务逻辑中分离出来,以确保业务逻辑不直接与数据库操作耦合在一起。这有助于提高代码的可维护性和可测试性。
-
复用和性能:DAO 提供了一种重复使用数据库操作的机制,允许多个部分的应用程序共享相同的数据访问代码。此外,DAO 还可以实现性能优化,如查询缓存和批处理操作。
-
数据库无关性:良好设计的 DAO 可以提供一定程度的数据库无关性,因为应用程序的其余部分无需关心底层数据库的具体实现细节。
通过这两个类我们就可以完成针对数据库表的操作。
现在我们来运行一下:
当前我们的博客页面还是写死的,主要是blog_list里面的这段代码决定的:
</div>
<!-- 右侧信息 -->
<div class="container-right">
<!-- 这个 div 表示一个 博客 -->
<div class="blog">
<!-- 博客标题 -->
<div class="title">我的第一篇博客博客博客博客</div>
<!-- 博客的发布时间 -->
<div class="date">2023-05-11 20:00:00</div>
<!-- 博客的摘要-->
<div class="desc">
<!-- 使用 lorem 生成一段随机的字符串 -->
从今天起, 我要认真敲代码. Lorem ipsum dolor sit amet consectetur, adipisicing elit. Debitis repellendus voluptatum, reiciendis rem consectetur incidunt aspernatur eveniet excepturi magni quis sint, provident est at et pariatur dolorem aliquid fugit voluptatem.
</div>
<!-- html 中不能直接写 大于号, 大于号可能会被当成标签的一部分 -->
<a href="blog_detail.html?blogId=1">查看全文 >> </a>
</div>
<div class="blog">
<!-- 博客标题 -->
<div class="title">我的第一篇博客</div>
<!-- 博客的发布时间 -->
<div class="date">2023-05-11 20:00:00</div>
<!-- 博客的摘要-->
<div class="desc">
<!-- 使用 lorem 生成一段随机的字符串 -->
从今天起, 我要认真敲代码. Lorem ipsum dolor sit amet consectetur, adipisicing elit. Debitis repellendus voluptatum, reiciendis rem consectetur incidunt aspernatur eveniet excepturi magni quis sint, provident est at et pariatur dolorem aliquid fugit voluptatem.
</div>
<!-- html 中不能直接写 大于号, 大于号可能会被当成标签的一部分 -->
<a href="blog_detail.html?blogId=1">查看全文 >> </a>
</div>
<div class="blog">
<!-- 博客标题 -->
<div class="title">我的第一篇博客</div>
<!-- 博客的发布时间 -->
<div class="date">2023-05-11 20:00:00</div>
<!-- 博客的摘要-->
<div class="desc">
<!-- 使用 lorem 生成一段随机的字符串 -->
从今天起, 我要认真敲代码. Lorem ipsum dolor sit amet consectetur, adipisicing elit. Debitis repellendus voluptatum, reiciendis rem consectetur incidunt aspernatur eveniet excepturi magni quis sint, provident est at et pariatur dolorem aliquid fugit voluptatem.
</div>
<!-- html 中不能直接写 大于号, 大于号可能会被当成标签的一部分 -->
<a href="blog_detail.html?blogId=1">查看全文 >> </a>
</div>
</div>
</div>
而我们真正想要得到的效果是从我们的数据库里面加载数据,所以我们现在就要把这些写死的内容全部删掉:
<!-- 页面的主体部分 -->
<div class="container">i-Dora
<!-- 左侧信息 -->
<div class="container-left">
<!-- 这个 div 表示整个用户信息的区域 -->
<div class="card">
<!-- 用户的头像 -->
<img src="image/微信图片_20231030142347.jpg" alt="">
<!-- 用户名 -->
<h3>Di-Dora</h3>
<!-- github 地址 -->
<a href="https://www.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">
<!-- 这个 div 表示一个 博客 -->
</div>
</div>
此刻我们再次刷新页面:
获取博客列表页
在博客列表页加载时通过Ajax给服务器发起请求,从服务器(数据库)拿到博客列表数据,并且显示到页面上
约定前后端交互接口:
让浏览器给服务器发起请求
先引入我们的 jqurery cdn,可以先验证一下再引入:
可以的,没问题,引入:
让服务器能够处理这个请求,返回响应数据(查询数据库)
package servlet;
import com.fasterxml.jackson.databind.ObjectMapper;
import model.Blog;
import model.BlogDao;
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;
import java.util.List;
@WebServlet("/blog")
public class BlogServlet extends HttpServlet {
private ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
BlogDao blogDao = new BlogDao();
// 查询数据库, 得到博客列表.
List<Blog> blogs = blogDao.getBlogs();
// 把博客列表数据按照 json 格式返回回去.
String respJson = objectMapper.writeValueAsString(blogs);
System.out.println("respJson: " + respJson);
resp.setContentType("application/json; charset=utf8");
resp.getWriter().write(respJson);
}
}
前端代码处理响应数据,构造成HTML片段,显示到页面上
<script>
// 把这样的代码封装到函数里.
// js 中定义函数, 使用 function 关键字. 不必写返回值类型. ( ) 也是形参列表. 不必写形参的类型
function getBlogs() {
$.ajax({
type: 'get',
url: 'blog',
success: function(body) {
// 服务器成功响应之后, 调用的回调函数.
let containerDiv = document.querySelector('.container-right');
for (let i = 0; i < body.length; i++) {
// blog 就是一个形如 { blogId: 1, title: "这是标题" .... }
let blog = body[i];
// 构建整个博客
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.innerHTML = '查看全文 >>'
// 点击不同的博客, 要跳转到不同的博客详情页.
a.href = 'blog_detail.html?blogId=' + blog.blogId;
// 上面的操作完成了每个零件的创建. 还需要往一起进行组装.
blogDiv.appendChild(titleDiv);
blogDiv.appendChild(dateDiv);
blogDiv.appendChild(descDiv);
blogDiv.appendChild(a);
// 把 blogDiv 最终组装到页面上.
containerDiv.appendChild(blogDiv);
}
}
});
}
// 定义完函数, 还需要调用, 才能执行
getBlogs();
</script>
这里构造的过程还是基于我们之前学过的API:
- querySelector:querySelector 是一个用于获取文档中匹配特定CSS选择器的第一个元素的方法。它返回一个单个元素(或 null,如果未找到匹配的元素)。
例如:document.querySelector(".example-class") 会返回第一个具有类名 "example-class" 的元素。
- createElement:createElement 是一个用于创建新的HTML元素的方法。你可以使用它创建各种元素,如 div、p、img 等。
例如:var newDiv = document.createElement("div") 创建一个新的 div 元素。
- innerHTML:innerHTML 是一个属性,它用于获取或设置元素的HTML内容。你可以用它替换元素的内容。
例如:element.innerHTML = "<p>New content</p>" 会替换元素的内容为一个新的段落。
- className:className 是一个属性,它用于获取或设置元素的类名。你可以用它来更改元素的样式。
例如:element.className = "new-class" 将元素的类名设置为 "new-class"。
- appendChild:appendChild 是一个方法,用于将一个元素添加为另一个元素的子元素,即将一个元素附加到另一个元素的末尾。
例如:parentElement.appendChild(childElement) 将 childElement 添加到 parentElement 的末尾。
HTML中,标签就是由“<”、“>”构成的,所以要使用转义字符:
以下是一些常见的HTML转义字符:
- & 表示 &:用于表示和符号。
- " 表示 ":用于表示双引号。
- ' 表示 ':用于表示单引号。
- 表示空格:用于表示一个不断行的空格。
- © 表示 ©:用于表示版权符号。
- ® 表示 ®:用于表示注册商标符号。
- ™ 表示 ™:用于表示商标符号。
- € 表示 €:用于表示欧元符号。
就比如:
我们点击就会直接跳转:
我们写出来的这个HTML的代码,和前面写JDBC时候的感觉是类似的,看起来很长,但是就是一些 API 的重复使用。
对于前端页面来说生成页面的方式其实有很多种:
我们此处使用的是比较朴素的方式——基于 dom api 的方式: dom api 就属于是浏览器提供的标准的 api,不属于任何的第三方框架和库, 定位就类似于jdbc api。使用原生DOM API编写HTML代码是一种朴素的方式,它是不依赖于第三方框架或库。这种方式需要更多的代码,但有时对于小型项目或特定需求来说是足够的。
前端也有一些框架和库是把 dom api 又进行了封装,如React、Vue.js、Angular等,提供了更高级的抽象,以简化开发任务。这些框架和库的目标是提高开发效率,减少代码重复,并提供更多的便捷性,使用起来能更简单一点。
现在我们切回列表页,点击刷新网页:
emmm,观察网页之后发现,我们当前的代码还存在两个比较重要的问题:
1、时间显示不对:
我们通过抓包了解具体信息:
package model;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
// Blog 对象就是对应到 blog 表中的一条记录.
// 表里有哪些列, 这个类里就应该有哪些属性
public class Blog {
private int blogId;
private String title;
private String content;
private Timestamp postTime;
private int userId;
public int getBlogId() {
return blogId;
}
public void setBlogId(int blogId) {
this.blogId = blogId;
}
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 String getPostTime() {
// Java 标准库提供了一个 SimpleDateFormat 类, 完成时间戳到格式化时间的转换.
// 这个类的使用, 千万不要背!!! 都要去查一下!! 背大概率会背错!!!
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return simpleDateFormat.format(postTime);
}
public void setPostTime(Timestamp postTime) {
this.postTime = postTime;
}
public int getUserId() {
return userId;
}
public void setUserId(int userId) {
this.userId = userId;
}
@Override
public String toString() {
return "Blog{" +
"blogId=" + blogId +
", title='" + title + '\'' +
", content='" + content + '\'' +
", postTime=" + postTime +
", userId=" + userId +
'}';
}
}
SimpleDateFormat 是 Java 中的一个类,用于格式化日期和时间,以及将字符串解析为日期和时间对象。它是 java.text.SimpleDateFormat 类的一部分。
为啥说不要去背?
我们随便搜索一下:SimpleDateFormat用法
反正总结一下就是:
1、规定一下格式化日期和时间:这将把当前日期和时间格式化为 "yyyy-MM-dd HH:mm:ss" 的字符串。
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date now = new Date();
String formattedDate = sdf.format(now);
System.out.println("Formatted Date: " + formattedDate);
2、解析日期和时间:这将把一个 "yyyy-MM-dd" 格式的字符串解析为日期对象。
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String dateStr = "2023-11-08";
try {
Date parsedDate = sdf.parse(dateStr);
System.out.println("Parsed Date: " + parsedDate);
} catch (ParseException e) {
e.printStackTrace();
}
3、自定义日期和时间格式:
你可以使用不同的格式模式来自定义日期和时间的格式。例如:
- "yyyy" 表示年份(如 2023)。
- "MM" 表示月份(01 到 12)。
- "dd" 表示日期(01 到 31)。
- "HH" 表示小时(00 到 23)。
- "mm" 表示分钟(00 到 59)。
- "ss" 表示秒(00 到 59)。
你可以组合这些模式以创建自定义格式。
4、处理时区:这将将时区设置为UTC。
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
SimpleDateFormat 还支持时区的处理,可以使用 TimeZone 类来指定时区,以确保日期和时间正确显示和解析。
但是这里的各种规定很多,比如,Java里面M是月份,m是分钟,但是Linux里面就反过来了……
这背是背不过来的,反而很容易混淆,所以不要去背!
这里更改之后,我们再次重启服务器,刷新页面:
2、返回数据的先后顺序问题:
刷新页面之后观察:
正常情况下,我们希望新的博客应该出现在最上面。
此处的结果顺序是从数据库里查询出来的,所以把我们的List 进行逆序是不可行的。
而一个SQL 如果不加order by,结果是不可预期的。
所以我们现在就加上order by:
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;
// 通过 BlogDao 来完成针对 blog 表的操作
public class BlogDao {
// 1. 新增操作 (提交博客就会用到)
public void insert(Blog blog) {
Connection connection = null;
PreparedStatement statement = null;
try {
// 1. 建立连接
connection = DBUtil.getConnection();
// 2. 构造 SQL
String sql = "insert into blog values (null, ?, ?, now(), ?)";
statement = connection.prepareStatement(sql);
statement.setString(1, blog.getTitle());
statement.setString(2, blog.getContent());
statement.setInt(3, blog.getUserId());
// 3. 执行 SQL
statement.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException();
} finally {
DBUtil.close(connection, statement, null);
}
}
// 2. 查询博客列表 (博客列表页)
// 把数据库里所有的博客都拿到.
public List<Blog> getBlogs() {
List<Blog> blogList = new ArrayList<>();
Connection connection = null;
PreparedStatement statement = null;
ResultSet resultSet = null;
try {
connection = DBUtil.getConnection();
String sql = "select * from blog order by postTime desc";
statement = connection.prepareStatement(sql);
resultSet = statement.executeQuery();
while (resultSet.next()) {
Blog blog = new Blog();
blog.setBlogId(resultSet.getInt("blogId"));
blog.setTitle(resultSet.getString("title"));
// 此处读到的正文是整个文章内容. 太多了. 博客列表页, 只希望显示一小部分. (摘要)
// 此处需要对 content 做一个简单截断. 这个截断长度 100 这是拍脑门出来的. 具体截取多少个字好看, 大家都可以灵活调整.
String content = resultSet.getString("content");
if (content.length() > 100) {
content = content.substring(0, 100) + "...";
}
blog.setContent(content);
blog.setPostTime(resultSet.getTimestamp("postTime"));
blog.setUserId(resultSet.getInt("userId"));
blogList.add(blog);
}
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
DBUtil.close(connection, statement, resultSet);
}
return blogList;
}
// 3. 根据博客 id 查询指定的博客
public Blog getBlog(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();
// 由于此处是拿着 blogId 进行查询. blogId 作为主键, 是唯一的.
// 查询结果非 0 即 1 , 不需要使用 while 来进行遍历
if (resultSet.next()) {
Blog blog = new Blog();
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"));
return blog;
}
} catch (SQLException throwables) {
throwables.printStackTrace();
} finally {
DBUtil.close(connection, statement, resultSet);
}
return null;
}
// 4. 根据博客 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 throwables) {
throwables.printStackTrace();
} finally {
DBUtil.close(connection, statement, null);
}
}
}
重启一下,刷新:
现在就正常多了。
通过以上操作,我们就完成了博客列表页的基本操作。
博客详情页
约定前后端接口
让前端代码发起一个Ajax请求给服务器
我们之前写死的前端代码就可以直接删除了:
一样的,引入Ajax:
然后开始写函数:
打开控制台直接输入就可以看到:
让服务器处理这个请求
所以这里我们就需要回去修改一下我们的BlogServlet:
package servlet;
import com.fasterxml.jackson.databind.ObjectMapper;
import model.Blog;
import model.BlogDao;
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;
import java.util.List;
@WebServlet("/blog")
public class BlogServlet extends HttpServlet {
private ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
BlogDao blogDao = new BlogDao();
String respJson = "";
String blogId = req.getParameter("blogId");
if (blogId == null) {
// 请求中没有 query string, 请求来自博客列表页.
// 查询数据库, 得到博客列表.
List<Blog> blogs = blogDao.getBlogs();
// 把博客列表数据按照 json 格式返回回去.
respJson = objectMapper.writeValueAsString(blogs);
} else {
// 请求中存在 query string, 请求来自博客详情页.
Blog blog = blogDao.getBlog(Integer.parseInt(blogId));
respJson = objectMapper.writeValueAsString(blog);
}
System.out.println("respJson: " + respJson);
resp.setContentType("application/json; charset=utf8");
resp.getWriter().write(respJson);
}
}
前端拿到响应之后把响应数据构造成页面的HTML片段
<!-- 右侧信息 -->
<div class="container-right">
<h3></h3>
<div class="date"></div>
<div class="content">
</div>
</div>
</div>
<script>
function getBlog() {
$.ajax({
type: 'get',
url: 'blog' + location.search, // 这里需要带上 blogId 参数.
success: function(blog) {
// blog 就是返回的一篇博客的内容.
// 形如 { blogId: 1, title: "这是标题", ..... }
let h3 = document.querySelector('.container-right h3');
h3.innerHTML = blog.title;
let dateDiv = document.querySelector('.container-right .date');
dateDiv.innerHTML = blog.postTime;
let contentDiv = document.querySelector('.container-right .content');
contentDiv.innerHTML = blog.content;
}
})
}
getBlog();
</script>
但是我们退出后重新点进一篇之前的博客,会发现详情页里面还是以前的内容!
这个问题是浏览器缓存引起的:
如何克服缓存的干扰,在前端的圈子里有专业的解决方案。
此处我们使用一个简单粗暴的方式就可以解决:Ctrl + F5 强制刷新页面。
对了,如果控制台出现这个报错,不用慌张:
强制刷新之后就可以正常显示了:
当前的博客详情页虽然能够显示出博客的正文了,但是它显示的时正文的markdown原始数据,作为博客网站,它理应显示出markdown渲染之后的效果。
此处的渲染,仍然时通过第三方库(editor.md)
editor.md官方文档上给出了具体的例子来完成上述操作:
我们需要先引入依赖:
<!-- 引入 editor.md 的依赖 -->
<!-- 先引入 jquery, 再引入 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>
修改函数:
<!-- 右侧信息 -->
<div class="container-right">
<h3></h3>
<div class="date"></div>
<div class="content" id="content">
</div>
</div>
</div>
<script>
function getBlog() {
$.ajax({
type: 'get',
url: 'blog' + location.search, // 这里需要带上 blogId 参数.
success: function(blog) {
// blog 就是返回的一篇博客的内容.
// 形如 { blogId: 1, title: "这是标题", ..... }
let h3 = document.querySelector('.container-right h3');
h3.innerHTML = blog.title;
let dateDiv = document.querySelector('.container-right .date');
dateDiv.innerHTML = blog.postTime;
// 这种设置方式, 页面显示的是 md 的原始内容. 希望对这个内容进行渲染成 html
// let contentDiv = document.querySelector('.container-right .content');
// contentDiv.innerHTML = blog.content;
// 这个方法第一个参数, 必须是一个 html 标签的 id 属性.
editormd.markdownToHTML('content', { markdown: blog.content });
}
})
}
getBlog();
</script>
重新运行,刷新界面:
实现登录
在登录页面输入框中填写用户名和密码,点击登录就会给服务器发起HTTP请求(可以使用Ajax,也可以使用from表单,我们之前一直用Ajax,这次用用from表单的形式)。
服务器处理登录请求,读取用户名密码,在数据库查询、匹配,如果正确就登录成功,创建会话,跳转到博客列表页。
并且,由于这里登录成功之后直接进行重定向的跳转,不需要浏览器额外写代码处理就会自动跳转,所以会更加简单。
约定前后端接口
让前端发起请求
<!-- 登录页的版心 -->
<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>
让服务器处理请求并返回响应
package servlet;
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 {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 1. 读取参数中的用户名和密码
req.setCharacterEncoding("utf8");
String username = req.getParameter("username");
String password = req.getParameter("password");
// 验证一下参数, 看下是否合理.
if (username == null || username.length() == 0 || password == null || password.length() == 0) {
resp.setContentType("text/html; charset=utf8");
resp.getWriter().write("您输入的用户名或者密码为空!");
return;
}
// 2. 查询数据库, 看看这里的用户名密码是否正确.
UserDao userDao = new UserDao();
User user = userDao.getUserByName(username);
if (user == null) {
// 用户名不存在!
resp.setContentType("text/html; charset=utf8");
resp.getWriter().write("您输入的用户名或密码不正确!");
return;
}
if (!password.equals(user.getPassword())) {
// 密码不正确!
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");
}
}
输入正确的用户名和密码之后跳转:
实现强制登录
在博客列表页,详情页,编辑页,判定当前用户是否已经登录如果未登录,则强制跳转到登录页。(要求用户必须登录后才能使用)
在这几个页面加载时,给服务器发起 Ajax ,从服务器获取一下当前的登录状态。
约定前后端交互接口
让前端发起请求
这里因为响应比较简单,所以我们直接写了:
<script>
// 把这样的代码封装到函数里.
// js 中定义函数, 使用 function 关键字. 不必写返回值类型. ( ) 也是形参列表. 不必写形参的类型
function getBlogs() {
$.ajax({
type: 'get',
url: 'blog',
success: function(body) {
// 服务器成功响应之后, 调用的回调函数.
let containerDiv = document.querySelector('.container-right');
for (let i = 0; i < body.length; i++) {
// blog 就是一个形如 { blogId: 1, title: "这是标题" .... }
let blog = body[i];
// 构建整个博客
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.innerHTML = '查看全文 >>'
// 点击不同的博客, 要跳转到不同的博客详情页.
a.href = 'blog_detail.html?blogId=' + blog.blogId;
// 上面的操作完成了每个零件的创建. 还需要往一起进行组装.
blogDiv.appendChild(titleDiv);
blogDiv.appendChild(dateDiv);
blogDiv.appendChild(descDiv);
blogDiv.appendChild(a);
// 把 blogDiv 最终组装到页面上.
containerDiv.appendChild(blogDiv);
}
}
});
}
// 定义完函数, 还需要调用, 才能执行
getBlogs();
//定义新的函数,获取登录状态
function getLoginStatus() {
$.ajax({
type: 'get',
url: 'login',
success: function(body) {
// 已经登录的状态.
console.log("已经登录了!");
},
error: function() {
// error 这里对应的回调函数,就是在响应状态码不为 2xx 的时候会触发
// 当服务器返回 403 的时候,就会触发当前这个 error 部分的逻辑了.
// 强制要求页面跳转到博客登录页
//为啥不在服务器直接返回一个 302 这样的重定向响应,直接跳转到登录页呢?
// 302 这种响应,无法被ajax 直接处理。
//(如果是通过提交 form或者点击a标签这种触发的HTTP请求,浏览器可以直接响应302)
// 前端页面跳转的实现方式
location.assign('login.html');
}
})
}
getLoginStatus();
</script>
一个页面,触发的 ajax 是可以有多个的;一个页面通常都会触发多个 ajax,这些 aiax 之间是“并发执行”这样的效果。
JS 中,没有“多线程”这样的机制,而 ajax 是一种特殊情况,能够起到类似于“多线程”的效果。
当页面中发起两个或者多个 ajax 的时候, 这些 ajax 请求就相当于并发的发送出去的,彼此之间不会相互千预(不是串行执行,即不是执行完一个 aiax得到响应之后再执行下一个。而是同时发出去多个请求,谁的响应先回来了,就先执行谁的回调)
让服务器处理上述请求
package servlet;
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 {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 1. 读取参数中的用户名和密码
req.setCharacterEncoding("utf8");
String username = req.getParameter("username");
String password = req.getParameter("password");
// 验证一下参数, 看下是否合理.
if (username == null || username.length() == 0 || password == null || password.length() == 0) {
resp.setContentType("text/html; charset=utf8");
resp.getWriter().write("您输入的用户名或者密码为空!");
return;
}
// 2. 查询数据库, 看看这里的用户名密码是否正确.
UserDao userDao = new UserDao();
User user = userDao.getUserByName(username);
if (user == null) {
// 用户名不存在!
resp.setContentType("text/html; charset=utf8");
resp.getWriter().write("您输入的用户名或密码不正确!");
return;
}
if (!password.equals(user.getPassword())) {
// 密码不正确!
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");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 通过这个方法, 来反馈当前用户的登录状态.
// 一个简单的判定方式, 直接看会话是否存在.
// 此处使用一个更严格的方式. 不仅要求会话要存在, 还要求会话中要能保存 user 对象.
// (之所以给出这种设定, 也是为了后面实现 "退出登录" 这样的功能来做个铺垫)
HttpSession session = req.getSession(false);
if (session == null) {
// 会话不存在, 用户属于未登录状态.
resp.setStatus(403);
return;
}
User user = (User) session.getAttribute("user");
if (user == null) {
// user 对象也不存在. 同样也属于未登录状态
resp.setStatus(403);
return;
}
// 两个都存在, 返回 200
// 此处 200 不写也行. 默认就是 200
resp.setStatus(200);
}
}
重启服务器看看效果:
一敲回车,自动跳转:
抓个包观察一下:
现在我们进行登录:
现在就可以顺利访问博客列表了:
但是,我们一旦重启服务器,就又会变成未登录状态,因为当前的登录状态是通过服务器这里的session来存储的,session是服务器内存中的类似于hashmap的结构,一旦服务器重启就会清空原有内容。
相比之下,我们还有更好的解决方案:
1、把会话进行持久化保存(文件、数据库、redis……);
2、可以使用令牌的方式,把用户信息在服务器中加密,还是保存在浏览器这边,相当于服务器没有在内存中存储当前的用户身份。
以上解决方案我们以后会提到。
当前我们还只是让博客列表页能够有强制登录。
实际上,博客编辑页和博客详情页也应该要有这种机制,所以我们可以把一些公共的 JS 代码单独提取出来放到某个 .js 文件中,然后通过 html 中的 script 标签来引用这样的文件内容。
此时,我们就可以在 html 中调用对应的公共代码了,这就类似于 Java 的 import、或者 C 语言的 #include。
别忘了,还要引入这个.js文件:
//定义新的函数,获取登录状态
function getLoginStatus() {
$.ajax({
type: 'get',
url: 'login',
success: function(body) {
// 已经登录的状态.
console.log("已经登录了!");
},
error: function() {
// error 这里对应的回调函数,就是在响应状态码不为 2xx 的时候会触发
// 当服务器返回 403 的时候,就会触发当前这个 error 部分的逻辑了.
// 强制要求页面跳转到博客登录页
//为啥不在服务器直接返回一个 302 这样的重定向响应,直接跳转到登录页呢?
// 302 这种响应,无法被ajax 直接处理。
//(如果是通过提交 form或者点击a标签这种触发的HTTP请求,浏览器可以直接响应302)
// 前端页面跳转的实现方式
location.assign('login.html');
}
})
}
不仅是博客列表页,还有博客详情页:
还有博客编辑页也是一样:
还是一样的,重启服务器,刷新页面:
均正常跳转:
显示用户信息
- 博客列表页: 显示的是当前登录的用户的信息
- 博客详情页: 显示的是当前文章的作者信息
在页面加载的时候,给服务器发起 ajax 请求,服务器返回对应的用户数据。
根据发起请求不同的页面,服务器返回不同的信息即可。
约定前后端交互接口
先让前端代码发起如下请求
编写服务器代码来处理上述请求
package servlet;
import com.fasterxml.jackson.databind.ObjectMapper;
import model.User;
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("/userInfo")
public class UserInfoServlet extends HttpServlet {
private ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void doGet(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;
}
// 此时把 user 对象转成 json , 并返回给浏览器.
resp.setContentType("application/json; charset=utf8");
// 注意, user 中还有 password 属性呢!! 把密码再返回回去, 不太合适的.
user.setPassword("");
String respJson = objectMapper.writeValueAsString(user);
resp.getWriter().write(respJson);
}
}
package servlet;
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("/getAuthorInfo")
public class AuthorInfoServlet extends HttpServlet {
private ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 1. 先拿到请求中的 blogId
String blogId = req.getParameter("blogId");
if (blogId == null) {
resp.setContentType("text/html; charset=utf8");
resp.getWriter().write("请求中缺少 blogId!");
return;
}
// 2. 在 blog 表中查询到对应的 Blog 对象
BlogDao blogDao = new BlogDao();
Blog blog = blogDao.getBlog(Integer.parseInt(blogId));
if (blog == null) {
resp.setContentType("text/html; charset=utf8");
resp.getWriter().write("blogId 没有找到!");
return;
}
// 3. 根据 blog 对象中的 userId, 从 user 表中查到对应的作者.
UserDao userDao = new UserDao();
User user = userDao.getUserById(blog.getUserId());
if (user == null) {
resp.setContentType("text/html; charset=utf8");
resp.getWriter().write("userId 没有找到!");
return;
}
// 4. 把这个 user 对象, 返回到浏览器这边.
user.setPassword("");
String respJson = objectMapper.writeValueAsString(user);
resp.setContentType("application/json; charset=utf8");
resp.getWriter().write(respJson);
}
}
在前端代码中处理响应数据,把响应数据写到刚才页面的对应位置上
// 获取当前登录的用户信息
function getUserInfo() {
$.ajax({
type: 'get',
url: 'userInfo',
success: function(user) {
// 把拿到的响应数据, 取出其中的 username, 设置到页面的 h3 标签中!
let h3 = document.querySelector('.card h3');
h3.innerHTML = user.username;
}
});
}
getUserInfo();
运行一下看看:
上传文件我们之前实现过了,服务器保存文件地址也还好说,保存一个文件路径,但是下载文件还需要实现一个接口去查询、读取文件内容并返回。稍微有点复杂,我们暂时不讨论,以后会解决的。
因为现在是动态获取信息,所以我们还要把之前写死的东西删掉:
再次运行:
你可能会奇怪,博客列表页的时,我们使用appendChild来动态创建博客列表项,并将其添加到列表容器中。但是后面博客详情页、及其之后的功能,都没用上 appendChild。
那么我们什么时候应该用?什么时候不应该?
这主要是看你处理响应的代码中,是否需要创建新的 html 标签,如果需要,就使用 appendChild;如果只是需要直接修改已有的 html 标签,那我们就不需要使用它。
我们使用appendChild的主要目的是在DOM中创建新的HTML标签并将其添加到指定的父元素中。
而在博客详情页及其后续功能中,可能更多地是对已有的HTML标签进行修改或更新,而不是创建新的标签。这可以通过直接修改已有标签的内容、属性或样式来实现,而无需使用appendChild。
退出登录
在我们的博客列表页、博客详情页和博客编辑页的导航栏里面都有一个“注销”按钮:
这个标签本质上是一个a标签,可以有有一个href属性,点击就会触发一个HTTP请求,并且可能会因去浏览器跳转到另一个页面。
目前因为我们啥也没写,所以点击之后没有效果。
我们需要让用户点击“注销”的时候,能够触发一个 HTTP 请求(GET 请求),服务器收到这个 GET 请求的时候,就把会话里的 user 这个 Attribute 给删了。
由于我们之前在判定用户是否是登录状态的逻辑中,写明了需要同时验证“会话存在”且“这里的 user Attribute 也存在”,所以我们只要破坏一个,就可以使登录状态发生改变了。
那么为啥不直接删除 session 本身??
主要因为Servlet 没有提供删除 session 的方法,虽然有间接的方式(session 可以设置过期时间,可以设置一个非常短的过期时间)也可以起到删除的效果。
而session 提供了 removeAttribute 这样的方法,使我们可以直接把 user 这个 Attribute 给删了。
约定前后端交互接口
编写前端代码,发送请求
不用写Ajax,直接给a标签设置href属性即可。
运行一下:
现在这里已经有路径了:
但是真的跳转过去是这个页面:
所以下一步我们还要编写后端代码。
编写后端代码,处理这个请求,完成退出登录的操作
package servlet;
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 {
@Override
protected void doGet(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 的属性给删除掉
session.removeAttribute("user");
// 跳转到博客登录页
resp.sendRedirect("login.html");
}
}
重启服务器,观察运行效果:
注销,回到登录页:
如果想要通过地址栏强行访问博客列表页,是无法跳转的,所以代表这不是一个虚假的登录页面。
发布博客
当点击提交的时候就需要构造 http 请求,把此时的页面中的标题和正文都传输到服务器这边服务器,把这个数据存入数据库即可。
此处这里的 http 请求可以使用 Ajax,也可以使用 form表单 (这种填写输入框、提交数据的场景使用 form 会更方便)
约定前后端接口
便写前端代码,构造请求
标题本身就是一个我们自己写的input,给它加上name属性很容易。但是博客正文是由editor md构成的一个编辑器,这里我们该如何添加name属性呢?
开发editor md的大佬们也考虑到了这种情况,在官方文档上给出了例子,告诉我们如何在editor md 和 form 表单配合使用。
我们现在就可以来看看效果了:
对了,我最后发现还需要在css里面配置一下背景图片,看上去会美观很多:
编写服务器代码,处理刚刚的请求
package servlet;
import com.fasterxml.jackson.databind.ObjectMapper;
import model.Blog;
import model.BlogDao;
import model.User;
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;
import java.util.List;
@WebServlet("/blog")
public class BlogServlet extends HttpServlet {
private ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
BlogDao blogDao = new BlogDao();
String respJson = "";
String blogId = req.getParameter("blogId");
if (blogId == null) {
// 请求中没有 query string, 请求来自博客列表页.
// 查询数据库, 得到博客列表.
List<Blog> blogs = blogDao.getBlogs();
// 把博客列表数据按照 json 格式返回回去.
respJson = objectMapper.writeValueAsString(blogs);
} else {
// 请求中存在 query string, 请求来自博客详情页.
Blog blog = blogDao.getBlog(Integer.parseInt(blogId));
respJson = objectMapper.writeValueAsString(blog);
}
System.out.println("respJson: " + respJson);
resp.setContentType("application/json; charset=utf8");
resp.getWriter().write(respJson);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 1. 读取请求中的参数
req.setCharacterEncoding("utf8");
String title = req.getParameter("title");
String content = req.getParameter("content");
if (title == null || title.length() == 0 || content == null || content.length() == 0) {
resp.setContentType("text/html; charset=utf8");
resp.getWriter().write("当前传过来的标题或正文为空! 无法新增博客!");
return;
}
// 2. 从会话中, 拿到当前登录的用户的 userId
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;
}
// 3. 构造 blog 对象
Blog blog = new Blog();
blog.setTitle(title);
blog.setContent(content);
// 从会话中拿到 当前正在登录的用户的 userId, 设置进去即可
blog.setUserId(user.getUserId());
// 4. 插入到数据库中
BlogDao blogDao = new BlogDao();
blogDao.insert(blog);
// 5. 返回一个 302 这样的重定向报文, 跳转到博客列表页.
resp.sendRedirect("blog_list.html");
}
}
到此为止,我们的简易版博客系统就算是写完了,重启服务器,最后一次,运行看看:
总的来说,我们当前的这个网站还比较粗糙,但是这也算是一个比较完整的博客系统了,我们把前面学过的很多知识都综合起来运用进来了,还是有一定难度的。
后面我们还会再把这个博客系统写一遍,到那个时候就是基于Spring来编写了,我们会把现在很多粗糙的实现进一步的优化,并且完善一些细节问题,比如数据库怎么操作?请求响应怎么处理?出错情况怎么解决?数据如何组织?……
网站这样的程序如果出了问题该怎么去处理?