1. 为什么选择前后端分离?聊聊我的技术选型心路
大家好,我是老张,一个在技术圈摸爬滚打了十多年的老码农。今天想和大家聊聊一个特别“复古”又特别“现代”的话题——自己动手,从零搭建一个前后端分离的个人博客系统。你可能会问,现在各种现成的博客平台、静态网站生成器那么多,为什么还要自己折腾?我的回答是:为了那份完全掌控的乐趣,以及一次绝佳的全栈技术实战演练。这就像自己动手装修房子,虽然过程繁琐,但一砖一瓦都按自己的心意来,成就感是完全不同的。
我见过很多朋友,尤其是刚入行的开发者,对“前后端分离”这个词耳熟能详,但真要自己动手搭一个,总觉得无从下手。网上的教程要么太旧,要么只讲前端或只讲后端,很难串成一个完整的、能跑起来的项目。所以,我决定结合自己这些年踩过的坑、趟过的河,写一篇真正能让你从零到一、手把手实现的实战指南。我们不追求大而全的复杂功能,就聚焦于一个核心目标:构建一个具备登录、写文章、看文章、删文章等基本功能的个人博客,并且前后端完全分离。
在开始敲代码之前,我们先得把“前后端分离”这事儿聊透。简单说,传统的博客开发(比如用JSP、PHP直接混编),前端页面(HTML)和后端逻辑(Java/PHP代码)是糅合在一起的。这种模式开发快,但前后端工程师互相“打架”,维护起来也头疼。而前后端分离,就像把餐厅的前厅和后厨彻底分开。前端(前厅) 只负责用HTML、CSS、JavaScript把页面做得漂亮,并通过Ajax等技术与后端“点菜”、“传菜”。后端(后厨) 则专心处理业务逻辑,比如操作数据库、用户验证,并通过API接口(通常返回JSON数据)把“做好的菜”递给前端。
这种架构的好处太多了:前后端可以并行开发,互不干扰;后端API可以被多种前端(Web、手机App、小程序)复用;技术栈选择也更自由,前端可以用Vue、React,后端可以用Spring Boot、Node.js。对于我们这个个人项目来说,最大的好处是结构清晰,便于学习和理解现代Web开发的完整流程。接下来,我们就从最基础的“盖房子”步骤开始。
2. 万丈高楼平地起:项目初始化与环境搭建
2.1 工欲善其事,必先利其器
在画设计图之前,咱们得先把工具备齐。别担心,都是免费且常用的工具,我会把安装和配置的关键步骤都告诉你。
开发环境清单:
- IDE(集成开发环境):后端我推荐使用 IntelliJ IDEA Community Edition(免费版),它对Java和Maven的支持非常友好。前端写HTML/JS/CSS,用轻量级的 Visual Studio Code 就足够了,插件丰富,启动快。
- Java开发工具包(JDK):确保安装JDK 8或以上版本。安装后,在命令行输入
java -version和javac -version验证一下。 - 项目管理与构建工具:我们使用 Apache Maven 来管理后端项目的依赖(第三方库)和构建流程。IDEA通常内置了Maven,但建议单独安装并配置环境变量。
- 数据库:选择轻量且流行的 MySQL。你可以安装MySQL 5.7或8.0版本。安装过程中记住你设置的root用户密码。同时,我强烈建议安装一个图形化管理工具,比如 MySQL Workbench 或 Navicat,后面操作数据库表会直观很多。
- Web服务器:为了运行我们的Java Web后端,需要一个Servlet容器。这里我们用 Tomcat。我推荐使用IDEA插件“Smart Tomcat”,它比自带的Tomcat配置更简单,或者你也可以直接下载Tomcat 9.x的压缩包。
我的踩坑提醒:安装MySQL时,如果遇到端口3306被占用,可以修改端口号,或者停掉占用程序。安装Tomcat后,一定要在IDEA里正确配置它的路径,否则项目无法部署启动。这些初期配置看似琐碎,但却是后续一切顺利的基础,耐心一点。
2.2 创建Maven项目与规划项目结构
打开IDEA,我们来创建项目的“骨架”。选择新建一个Maven项目,不要用模板(archetype),创建一个最简单的项目。我给项目起名叫 my-personal-blog。
创建好后,我们需要手动补充一个标准的Java Web项目结构。这步很关键,很多新手会在这里迷糊。你的项目目录最终应该大致长这样:
my-personal-blog
├── src
│ ├── main
│ │ ├── java // 存放Java源代码(Servlet、工具类等)
│ │ │ └── com
│ │ │ └── yourname
│ │ │ └── blog
│ │ │ ├── controller // Servlet类
│ │ │ ├── dao // 数据库操作类
│ │ │ ├── model // 实体类(User, Blog)
│ │ │ └── util // 工具类(数据库连接池)
│ │ ├── resources // 配置文件,如数据库连接信息
│ │ └── webapp // **核心!** 存放前端页面和静态资源
│ │ ├── WEB-INF // 存放web.xml(可选,新版本Servlet可注解替代)
│ │ ├── css
│ │ ├── js
│ │ ├── lib // 第三方前端库,如jQuery, editor.md
│ │ └── *.html // 所有HTML页面
│ └── test // 测试代码(本篇暂不涉及)
├── pom.xml // Maven项目核心配置文件
└── target // 编译输出目录(自动生成)
重点在 webapp 目录,它是Tomcat默认寻找网页的根目录。我们所有前端页面(login.html, blog_list.html, blog_detail.html, blog_edit.html)都将直接放在这里或其子目录下。WEB-INF 目录下的内容受保护,浏览器无法直接访问,通常放配置或安全相关的文件。
接下来,打开项目根目录的 pom.xml 文件,这是Maven的“采购清单”。我们需要在这里声明项目依赖的第三方库。初始的 pom.xml 可能很空,我们需要添加以下关键依赖:
<dependencies>
<!-- Servlet API -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- JSON处理库(Jackson) -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
</dependencies>
添加后,IDEA通常会提示你导入更改(Import Changes),点击它,Maven就会自动从中央仓库下载这些jar包到你的本地仓库。看到项目外部库里出现了这些包,第一步就成功了。
3. 数据是基石:数据库设计与核心后端逻辑
3.1 如何设计简单高效的数据表?
任何动态网站,数据库设计都是重中之重。设计得好,代码写起来行云流水;设计得差,后期改起来哭爹喊娘。根据我们博客的核心功能(用户、博客文章),至少需要两张表。记住一个原则:一个实体一张表。
让我们打开MySQL客户端,创建一个名为 personal_blog 的数据库:CREATE DATABASE personal_blog CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; 使用 utf8mb4 字符集是为了更好地支持Emoji等特殊字符。
用户表 (user): 这张表存储博客的作者(也就是你)的信息。字段不用多,够用就好。
CREATE TABLE user (
userId INT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID,主键自增',
username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名,唯一',
password VARCHAR(100) NOT NULL COMMENT '密码(存储加密后的密文)',
createTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
这里我加了 createTime 记录注册时间,虽然不是核心功能,但加上是个好习惯。AUTO_INCREMENT 让数据库自动为我们生成递增的ID,省心。
博客表 (blog): 这是核心表,存放每一篇博客文章。
CREATE TABLE blog (
blogId INT PRIMARY KEY AUTO_INCREMENT COMMENT '博客ID,主键自增',
title VARCHAR(200) NOT NULL COMMENT '博客标题',
content TEXT NOT NULL COMMENT '博客正文(Markdown格式)',
postTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '发布时间',
userId INT NOT NULL COMMENT '作者ID,关联user表',
FOREIGN KEY (userId) REFERENCES user(userId) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='博客表';
这里有几个关键点:
content字段类型是TEXT,它能存储很大的文本量,足够放长篇文章。postTime默认值设为当前时间,这样插入数据时不用手动传时间。userId是外键,关联到user表的userId。这建立了“一篇博客属于一个用户”的关系。ON DELETE CASCADE表示如果用户被删除,他写的所有博客也会自动级联删除(根据业务需求可选,这里先加上)。
设计完表,我建议你手动插入一条测试用户数据(密码可以先明文,后面我们再加密):INSERT INTO user (username, password) VALUES ('zhangsan', '123456');。有了数据,我们后面的测试才有依据。
3.2 编写Java实体类与数据库操作层(DAO)
数据库表建好了,在Java世界里,我们需要用对象来映射它们,这就是实体类(Model)。在 src/main/java/com/yourname/blog/model/ 目录下,创建 User.java 和 Blog.java。
// User.java
public class User {
private int userId;
private String username;
private String password;
private Timestamp createTime;
// 标准的Getter和Setter方法这里省略,但必须要有!
// 可以使用IDEA快捷键 Alt+Insert 快速生成。
}
Blog.java 也类似,包含 blogId, title, content, postTime, userId 这些属性及其Getter/Setter。
实体类就是数据的搬运工。接下来,我们需要一个稳定可靠的数据库连接池。为什么不用每次都新建连接?因为创建和关闭数据库连接是重量级操作,非常耗时。连接池就像预先雇好一批快递员(连接),有任务(数据库操作)时直接派活,干完活回来待命,避免了频繁招聘和解雇的开销。我们使用业界公认可靠的 HikariCP,它性能非常好。在 pom.xml 中添加依赖:<groupId>com.zaxxer</groupId><artifactId>HikariCP</artifactId><version>5.0.1</version>。
然后在 util 包下创建 DBUtil.java,使用单例模式初始化一个全局的 DataSource:
public class DBUtil {
private static volatile DataSource dataSource = null;
public static DataSource getDataSource() {
if (dataSource == null) {
synchronized (DBUtil.class) {
if (dataSource == null) {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/personal_blog?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai");
config.setUsername("root");
config.setPassword("你的密码");
config.setMaximumPoolSize(20);
dataSource = new HikariDataSource(config);
}
}
}
return dataSource;
}
// 获取连接的方法
public static Connection getConnection() throws SQLException {
return getDataSource().getConnection();
}
}
注意替换你的数据库密码。时区设置 serverTimezone=Asia/Shanghai 很重要,能避免很多时间相关的问题。
有了连接池,我们就可以编写数据访问对象(DAO) 了。DAO封装了所有对某张表的增删改查操作,让业务逻辑层(Servlet)更干净。在 dao 包下创建 BlogDao.java 和 UserDao.java。
以 BlogDao 为例,提供一个“根据博客ID查询博客详情”的方法:
public Blog selectById(int blogId) {
String sql = "SELECT blogId, title, content, postTime, userId FROM blog WHERE blogId = ?";
try (Connection connection = DBUtil.getConnection();
PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setInt(1, blogId);
try (ResultSet resultSet = statement.executeQuery()) {
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 e) {
e.printStackTrace();
}
return null;
}
这里使用了 Try-with-Resources 语法,能确保 Connection, PreparedStatement 和 ResultSet 在使用后被自动关闭,避免资源泄露,代码也更简洁。PreparedStatement 能有效防止SQL注入攻击,务必养成习惯。
4. 前后端握手:API接口设计与核心功能实现
4.1 约定大于配置:设计清晰的RESTful风格API
前后端分离的核心在于“接口约定”。后端提供一系列API,前端像点菜一样调用它们。我们尽量遵循 RESTful 风格的设计理念,让接口看起来直观。简单理解就是,用HTTP方法(GET/POST/PUT/DELETE)表示操作,用URL路径表示资源。
我们来为博客列表功能设计接口:
- 功能:获取所有博客文章的摘要,用于列表页展示。
- 请求:
GET /blog - 响应:JSON格式的博客数组。每个博客对象包含
id,title,摘要内容,发布时间,作者名等。注意,这里不返回完整的content,以提升加载速度。
这个“约定”需要前后端开发者(虽然可能都是你)共同确认,并形成文档。在实际开发中,可以使用Swagger等工具来管理和可视化API,但初期我们心里有数就行。
4.2 实现第一个Servlet:获取博客列表
现在,我们在 controller 包下创建第一个Servlet——BlogListServlet。现代Servlet(3.0+)可以使用 @WebServlet 注解,无需在 web.xml 中配置。
@WebServlet("/blog")
public class BlogListServlet extends HttpServlet {
private ObjectMapper objectMapper = new ObjectMapper(); // Jackson的核心类,用于对象和JSON互转
private BlogDao blogDao = new BlogDao();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("application/json; charset=utf-8");
// 1. 查询数据库,获取博客列表(这里先简单查询全部,后期加分页)
List<Blog> blogs = blogDao.selectAll();
// 2. 将List<Blog>转换为JSON字符串
String respJson = objectMapper.writeValueAsString(blogs);
// 3. 将JSON字符串写回给前端
resp.getWriter().write(respJson);
}
}
但是,直接返回 Blog 对象列表有个问题:postTime 是 Timestamp 类型,Jackson默认会序列化成很长的时间戳数字,前端不好处理。我们需要一个视图对象(VO) 或 数据传输对象(DTO) 来定制返回给前端的数据格式。创建 BlogVO.java:
public class BlogVO {
private int id;
private String title;
private String summary; // 摘要,由content截取而来
private String author; // 作者名,需要联表查询
private String postTime; // 格式化的时间字符串
// 构造方法:可以从Blog对象和作者名构造
public BlogVO(Blog blog, String author) {
this.id = blog.getBlogId();
this.title = blog.getTitle();
// 生成摘要:截取content前150个字符
this.summary = blog.getContent().length() > 150 ?
blog.getContent().substring(0, 150) + "..." : blog.getContent();
this.author = author;
// 格式化时间
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
this.postTime = sdf.format(blog.getPostTime());
}
// Getter...
}
然后在Servlet中,查询出 List<Blog> 后,再根据每个 blog 的 userId 查询出作者名,构造出 List<BlogVO>,再序列化成JSON返回。这样,前端拿到的就是干净、格式化的数据了。
4.3 前端调用:用Ajax获取并渲染数据
后端API准备好了,现在轮到前端“点菜”。我们在 webapp 下创建 blog_list.html。页面加载时,通过JavaScript发起Ajax请求。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>我的博客</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div id="nav">导航栏...</div>
<div id="blog-list-container">
<!-- 博客列表将由JS动态插入到这里 -->
</div>
<script src="https://cdn.bootcss.com/jquery/3.6.0/jquery.min.js"></script>
<script>
$(document).ready(function() {
// 发起GET请求到 /blog
$.ajax({
url: '/blog', // 注意,如果前端页面不在根目录,可能需要调整路径
type: 'GET',
dataType: 'json',
success: function(blogList) {
// blogList 就是后端返回的JSON数组
let container = $('#blog-list-container');
container.empty(); // 清空容器
for (let blog of blogList) {
// 动态创建DOM元素并填充数据
let blogElement = `
<div class="blog-item">
<h3><a href="blog_detail.html?blogId=${blog.id}">${blog.title}</a></h3>
<p class="summary">${blog.summary}</p>
<div class="meta">
<span>作者:${blog.author}</span>
<span>发布时间:${blog.postTime}</span>
</div>
<hr>
</div>
`;
container.append(blogElement);
}
},
error: function(xhr, status, error) {
console.error("获取博客列表失败:", error);
alert('加载失败,请刷新重试');
}
});
});
</script>
</body>
</html>
这里我用了jQuery的 $.ajax 来简化操作。关键点在于 success 回调函数,它在前端收到后端成功的JSON响应后触发。我们遍历数据,用模板字符串拼接出HTML片段,然后插入到页面容器中。这样,一个动态的博客列表页就完成了。你可以启动Tomcat,部署项目,然后在浏览器访问 http://localhost:8080/my-personal-blog/blog_list.html 看看效果(记得先在数据库里插几篇测试博客)。如果列表没出来,按F12打开浏览器开发者工具,查看“网络(Network)”标签页,看看 /blog 这个请求是否成功,响应数据是什么,这是调试前后端交互的黄金法则。
5. 功能闭环:登录、详情、编辑与删除
5.1 用户登录与会话管理
一个完整的博客,作者需要登录才能管理文章。登录的本质是验证用户名密码,然后在服务器端创建一个会话(Session)来记住这个用户。
后端实现 (LoginServlet):
- 接收前端POST过来的用户名和密码。
- 在
UserDao中增加一个selectByUsernameAndPassword方法,查询数据库验证。 - 重要:密码千万不要明文存储和比较! 即使在这个小项目中,我们也应该使用MD5、SHA-256或更安全的BCrypt进行加密。我们在存入数据库时,就应该存加密后的密文。验证时,将用户输入的密码用同样算法加密后,与数据库中的密文比较。
- 验证通过后,通过
HttpServletRequest.getSession().setAttribute("user", userObject)将用户对象存入Session。 - 返回一个简单的JSON,如
{“ok”: true}或{“ok”: false, “reason”: “密码错误”}。
前端实现 (login.html):
一个简单的表单,提交时阻止默认提交行为,改用Ajax发送用户名和密码到 LoginServlet。根据返回的JSON结果,如果成功,就用 window.location.href = ‘blog_list.html’ 跳转到列表页;如果失败,提示错误信息。
强制登录拦截:
在博客列表、详情、编辑页的JavaScript中,页面加载时先发起一个Ajax请求到 /checkLogin 这样的接口。后端在这个接口里检查Session中是否有user对象。如果有,返回用户信息或简单的成功状态;如果没有,可以返回403状态码。前端收到403后,强制跳转到登录页。这就是所谓的“前端路由守卫”,保证了未登录用户无法进入管理页面。
5.2 博客详情页与Markdown渲染
列表页的“查看全文”链接到了 blog_detail.html?blogId=1。详情页需要做两件事:
- 根据URL中的blogId参数,请求后端获取完整的博客数据。后端接口可以是
GET /blog?blogId=1(与列表接口区分,可通过查询参数)或更RESTful的GET /blog/1。 - 将获取到的Markdown格式的
content渲染成HTML。我们不可能自己写渲染器,引入开源库是最好选择。Editor.md 不仅是一个编辑器,也提供了一个强大的客户端Markdown解析渲染功能。我们在详情页引入它的相关JS和CSS文件,然后调用editormd.markdownToHTML()方法,将Markdown字符串转换并插入到指定的HTML容器中,就能看到漂亮的排版、代码高亮、甚至流程图了。
5.3 博客编辑与发布
编辑页 (blog_edit.html) 我们同样使用 Editor.md 来提供一个所见即所得的Markdown编辑器。它的初始化配置稍微复杂一点,但官方文档很详细。核心是配置 saveHTMLToTextarea: true,这样编辑器里的内容会同步到一个隐藏的 textarea 中,方便表单提交。
发布博客是一个 POST /blog 请求。前端将标题和Markdown内容通过Ajax发送。后端 BlogServlet 的 doPost 方法需要:
- 检查用户是否登录(从Session取user)。
- 解析请求中的JSON数据,构造Blog对象,其中
userId来自Session。 - 调用
BlogDao.insert(blog)插入数据库。 - 返回操作结果(成功或失败)。
这里有个细节:插入成功后,是跳转到新文章的详情页,还是返回列表页?通常可以返回新创建博客的ID,让前端自行跳转。
5.4 删除功能与权限校验
删除按钮通常放在博客详情页,只有作者本人能看到。它触发一个 DELETE /blog?blogId=xxx 请求(也可以用GET,但语义上DELETE更准确)。后端在删除前必须进行权限二次校验:即使前端隐藏了按钮,用户也可能直接构造请求。所以后端逻辑是:
- 获取要删除的
blogId和当前登录用户的userId。 - 先根据
blogId查询出这篇博客,核对它的userId是否与当前登录用户一致。 - 一致才执行删除
blogDao.deleteById(blogId),否则返回“无权操作”的错误。
至此,一个具备完整CRUD(增删改查)功能、并带有用户认证的前后端分离博客系统核心就实现了。整个过程,我们像搭积木一样,从数据库、到后端API、再到前端交互,一步步构建起来。我强烈建议你不要只看,而是跟着步骤亲手敲一遍代码。过程中遇到的每一个报错,都是你深入理解HTTP、数据库、Java Web的宝贵机会。比如,跨域问题、JSON解析异常、时间格式处理、静态资源路径问题,这些都是实战中必然会遇到的“坑”,跨过去,你的经验值就涨了一大截。

7094

被折叠的 条评论
为什么被折叠?



