使用 FastAPI 和 HTML/CSS/JavaScript 构建博客系统示例
本文介绍如何使用 FastAPI 作为后端框架,配合 HTML、CSS 和 JavaScript 构建前后端分离的博客系统。项目包含 RESTful API 设计、数据模型管理、响应式界面及暗色模式切换等功能。通过示例代码展示了文章的增删改查(CRUD)逻辑,并提供了运行环境配置说明,适合学习 Web 全栈开发基础架构。

本文介绍如何使用 FastAPI 作为后端框架,配合 HTML、CSS 和 JavaScript 构建前后端分离的博客系统。项目包含 RESTful API 设计、数据模型管理、响应式界面及暗色模式切换等功能。通过示例代码展示了文章的增删改查(CRUD)逻辑,并提供了运行环境配置说明,适合学习 Web 全栈开发基础架构。

本项目展示了一个前后端分离的博客系统架构,后端采用 FastAPI 框架,前端使用原生 HTML、CSS 和 JavaScript。
blog_system/
├── backend/
│ ├── main.py # FastAPI 后端入口
│ ├── models.py # 数据模型定义
│ ├── database.py # 数据库/状态管理
│ └── requirements.txt # 依赖列表
├── frontend/
│ ├── index.html # 主页面
│ ├── css/
│ │ └── style.css # 样式文件
│ └── js/
│ ├── app.js # 主应用逻辑
│ ├── api.js # API 调用封装
│ └── ui.js # UI 组件渲染
from datetime import datetime
from typing import Optional
from pydantic import BaseModel
class BlogPost(BaseModel):
id: Optional[int] = None
title: str
content: str
author: str
created_at: Optional[str] = None
updated_at: Optional[str] = None
from datetime import datetime
from typing import List, Dict, Optional
from models import BlogPost
class BlogDatabase:
def __init__(self):
self.posts = []
self.post_id_counter = 1
self.init_sample_data()
def init_sample_data(self):
"""初始化示例数据"""
if not self.posts:
sample_posts = [
BlogPost(id=1, title="欢迎来到我的博客", content="这是我的第一篇博客文章,欢迎阅读!", author="管理员", created_at="2024-01-01 10:00:00", updated_at="2024-01-01 10:00:00"),
BlogPost(id=2, title="NiceGUI 3.X 新特性介绍", content="NiceGUI 3.X 带来了很多令人兴奋的新特性...", author="技术达人", created_at="2024-01-02 14:30:00", updated_at="2024-01-02 14:30:00"),
BlogPost(id=3, title="Python Web 开发趋势", content="近年来,Python 在 Web 开发领域发展迅速...", author="Python 爱好者", created_at="2024-01-03 09:15:00", updated_at="2024-01-03 09:15:00")
]
for post in sample_posts:
self.posts.append(post.dict())
self.post_id_counter = 4
def get_all_posts(self) -> List[Dict]:
"""获取所有文章"""
return self.posts
def get_post(self, post_id: int) -> Optional[Dict]:
"""根据 ID 获取文章"""
for post in self.posts:
if post["id"] == post_id:
return post
return None
def create_post(self, post_data: Dict) -> Dict:
"""创建新文章"""
post = BlogPost(**post_data)
post.id = self.post_id_counter
post.created_at = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
post.updated_at = post.created_at
post_dict = post.dict()
self.posts.append(post_dict)
self.post_id_counter += 1
return post_dict
def update_post(self, post_id: int, post_data: Dict) -> Optional[Dict]:
"""更新文章"""
for i, existing_post in enumerate(self.posts):
if existing_post["id"] == post_id:
post_data["id"] = post_id
post_data["created_at"] = existing_post["created_at"]
post_data["updated_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.posts[i] = post_data
return post_data
return None
def delete_post(self, post_id: int) -> bool:
"""删除文章"""
for i, post in enumerate(self.posts):
if post["id"] == post_id:
self.posts.pop(i)
return True
return False
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from typing import List
import uvicorn
from models import BlogPost
from database import BlogDatabase
# 创建 FastAPI 应用
app = FastAPI(
title="博客系统 API",
description="一个简单的博客系统后端",
version="1.0.0"
)
# 添加 CORS 中间件
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 在生产环境中应该指定具体域名
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 初始化数据库
db = BlogDatabase()
# API 路由
@app.get("/")
async def root():
return {
"message": "欢迎访问博客系统 API",
"docs": "/docs",
"endpoints": {
"获取所有文章": "GET /api/posts",
"获取单篇文章": "GET /api/posts/{id}",
"创建文章": "POST /api/posts",
"更新文章": "PUT /api/posts/{id}",
"删除文章": "DELETE /api/posts/{id}"
}
}
@app.get("/api/posts", response_model=[BlogPost])
():
db.get_all_posts()
():
post = db.get_post(post_id)
post:
HTTPException(status_code=, detail=)
post
():
db.create_post(post.())
():
updated = db.update_post(post_id, post.())
updated:
HTTPException(status_code=, detail=)
updated
():
db.delete_post(post_id):
{: }
HTTPException(status_code=, detail=)
():
{: , : (db.get_all_posts())}
__name__ == :
uvicorn.run(, host=, port=, reload=)
fastapi==0.104.1
uvicorn==0.24.0
pydantic==2.5.0
python-multipart==0.0.6
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>博客系统</title>
<link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/purify.min.js"></script>
</head>
<body>
<!-- 导航栏 -->
< =>
博客系统
首页
管理
写文章
/* 基础样式 */
:root {
--primary-color: #3b82f6;
--primary-dark: #2563eb;
--secondary-color: #6b7280;
--success-color: #10b981;
--danger-color: #ef4444;
--warning-color: #f59e0b;
--light-color: #f9fafb;
--dark-color: #1f2937;
--border-color: #e5e7eb;
--shadow-color: rgba(0, 0, 0, 0.1);
--card-bg: #ffffff;
--body-bg: #f3f4f6;
--text-color: #374151;
}
[data-theme="dark"] {
--primary-color: #60a5fa;
--primary-dark: #3b82f6;
--secondary-color: #9ca3af;
--success-color: #34d399;
--danger-color: #f87171;
--warning-color: #fbbf24;
--light-color: #374151;
--dark-color: #111827;
--border-color: #4b5563;
--shadow-color: (, , , );
: ;
: ;
: ;
}
* {
: ;
: ;
: border-box;
}
{
: , sans-serif;
: (--body-bg);
: (--text-color);
: ;
: background-color , color ;
}
{
: ;
: auto;
: ;
}
{
: (--primary-color);
: white;
: ;
: (--shadow-color);
: sticky;
: ;
: ;
}
{
: ;
: auto;
: ;
: flex;
: space-between;
: center;
}
{
: flex;
: center;
: ;
: ;
: bold;
}
{
: ;
}
{
: flex;
: center;
: ;
}
{
: white;
: none;
: ;
: ;
: background-color ;
}
{
: (, , , );
}
{
: ;
: none;
: ;
: pointer;
: ;
: ;
: all ;
: inline-flex;
: center;
: ;
: none;
}
{
: (-);
: (--shadow-color);
}
{
: (--primary-color);
: white;
}
{
: (--primary-dark);
}
{
: (--success-color);
: white;
}
{
: (--danger-color);
: white;
}
{
: (--secondary-color);
: white;
}
{
: transparent;
: solid (--primary-color);
: (--primary-color);
}
{
: (--primary-color);
: white;
}
{
: none;
: none;
: white;
: ;
: pointer;
: ;
: ;
: background-color ;
}
{
: (, , , );
}
{
: (--card-bg);
: ;
: ;
: ;
: (--shadow-color);
: transform , box-shadow ;
: solid (--border-color);
}
{
: (-);
: (--shadow-color);
}
{
: ;
}
{
: block;
: ;
: ;
: (--text-color);
}
{
: ;
: ;
: solid (--border-color);
: ;
: ;
: border-color ;
: (--card-bg);
: (--text-color);
}
{
: none;
: (--primary-color);
}
{
: ;
: vertical;
: , sans-serif;
}
{
: inline-block;
: ;
: ;
: ;
: ;
}
{
: (--primary-color);
: white;
}
{
: (--success-color);
: white;
}
{
: fixed;
: ;
: ;
: ;
: ;
: (, , , );
: none;
: center;
: center;
: ;
}
{
: ;
: ;
: solid (--border-color);
: (--primary-color);
: ;
: spin linear infinite;
}
spin {
{ : (); }
}
{
: fixed;
: ;
: ;
: ;
: ;
: (, , , );
: none;
: center;
: center;
: ;
: ;
}
{
: (--card-bg);
: ;
: ;
: ;
: ;
: auto;
}
{
: ;
: solid (--border-color);
: flex;
: space-between;
: center;
}
{
: ;
: bold;
}
{
: none;
: none;
: ;
: pointer;
: (--secondary-color);
}
{
: ;
}
{
: ;
: solid (--border-color);
: flex;
: flex-end;
: ;
}
{
: grid;
: ;
}
{
: (--card-bg);
: ;
: ;
: solid (--border-color);
: all ;
}
{
: (-);
: (--shadow-color);
}
{
: ;
: bold;
: ;
: (--text-color);
}
{
: flex;
: ;
: ;
: (--secondary-color);
: ;
}
{
: (--text-color);
: ;
: ;
: -webkit-box;
-webkit--clamp: ;
-webkit-: vertical;
: hidden;
}
{
: flex;
: ;
: ;
}
(: ) {
{
: column;
: ;
}
{
: ;
: center;
: wrap;
}
{
: ;
}
{
: column;
: ;
}
}
// API 基础配置
const API_BASE_URL = 'http://localhost:8000';
const API_ENDPOINTS = {
POSTS: '/api/posts',
POST: (id) => `/api/posts/${id}`
};
// 显示/隐藏加载指示器
function showLoading() {
document.getElementById('loading-overlay').style.display = 'flex';
}
function hideLoading() {
document.getElementById('loading-overlay').style.display = 'none';
}
// 统一的错误处理
function handleApiError(error) {
console.error('API Error:', error);
let message = '网络错误,请稍后重试';
if (error.response) {
message = error.response.data?.detail || `服务器错误:${error.response.status}`;
} else if (error.request) {
message = '无法连接到服务器,请检查网络连接';
}
alert(message);
error;
}
= {
() {
{
();
response = ();
(!response.) ();
data = response.();
data;
} (error) {
(error);
} {
();
}
},
() {
{
();
response = ();
(!response.) ();
data = response.();
data;
} (error) {
(error);
} {
();
}
},
() {
{
();
response = (, {
: ,
: {
: ,
},
: .(postData)
});
(!response.) ();
data = response.();
data;
} (error) {
(error);
} {
();
}
},
() {
{
();
response = (, {
: ,
: {
: ,
},
: .(postData)
});
(!response.) ();
data = response.();
data;
} (error) {
(error);
} {
();
}
},
() {
{
();
response = (, {
:
});
(!response.) ();
data = response.();
data;
} (error) {
(error);
} {
();
}
}
};
// UI 组件库
// 显示通知
function showNotification(message, type = 'info') {
const colors = {
success: '#10b981',
error: '#ef4444',
warning: '#f59e0b',
info: '#3b82f6'
};
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 15px 20px;
background-color: ${colors[type]};
color: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
z-index: 1002;
animation: slideIn 0.3s ease-out;
max-width: 400px;
`;
notification.innerHTML = `
<div>
<i class="fas fa-${type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-circle' : type === 'warning' ? 'exclamation-triangle' : 'info-circle'}"></i>
<span>${message}</span>
</div>
`;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.animation = 'slideOut 0.3s ease-out forwards';
setTimeout(() => notification.remove(), );
}, );
}
() {
modal = .();
modal. = ;
modal.. = ;
modal. = ;
.().(modal);
modal.(, {
(e. === modal) {
modal.();
}
});
modal;
}
() {
modal = ({
: ,
: message,
:
});
modal;
}
() {
contentPreview = post.. > ? post..(, ) + : post.;
;
}
() {
cleanContent = .(marked.(post.));
;
}
() {
isEdit = !!post;
;
}
() {
(posts. === ) {
;
}
rows = posts.( ).();
;
}
// 应用主逻辑
let currentPage = 'home';
// 初始化应用
async function initApp() {
// 设置主题
const savedTheme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', savedTheme);
// 加载首页
await showPage('home');
}
// 切换主题
function toggleTheme() {
const currentTheme = document.documentElement.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
// 更新按钮图标
const themeButton = document.querySelector('.btn-toggle-theme i');
themeButton.className = newTheme === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
}
// 显示页面
() {
currentPage = page;
mainContent = .();
{
(page === ) {
();
} (page === ) {
();
} (page === ) {
();
} (page.()) {
postId = (page.()[]);
(postId);
} (page.()) {
postId = (page.()[]);
(postId);
}
} (error) {
.(, error);
mainContent. = ;
}
}
() {
mainContent = .();
mainContent. = ;
{
posts = .();
(posts. === ) {
.(). = ;
} {
postsHTML = posts
.()
.()
.( (post))
.();
.(). = ;
}
} (error) {
.(). = ;
}
}
() {
mainContent = .();
mainContent. = ;
{
post = .(postId);
mainContent. = (post);
} (error) {
mainContent. = ;
}
}
() {
mainContent = .();
mainContent. = ();
}
() {
mainContent = .();
mainContent. = ;
{
post = .(postId);
mainContent. = (post);
} (error) {
mainContent. = ;
}
}
() {
mainContent = .();
mainContent. = ;
{
posts = .();
.(). = (posts);
} (error) {
.(). = ;
}
}
() {
();
}
() {
();
}
() {
(, () => {
{
.(postId);
(, );
(currentPage === ) {
();
} (currentPage === ) {
();
} {
();
}
} (error) {
( + error., );
}
});
}
() {
event.();
title = .()..();
author = .()[]..();
content = .()..();
(!title || !author || !content) {
(, );
;
}
postData = { title, author, content };
{
(postId) {
.(postId, postData);
(, );
();
} {
newPost = .(postData);
(, );
();
}
} (error) {
( + error., );
}
}
style = .();
style. = ;
..(style);
# 进入后端目录
cd backend
# 安装依赖
pip install -r requirements.txt
# 启动后端服务器
python main.py
后端服务将在 http://localhost:8000 启动,可以通过 http://localhost:8000/docs 查看 API 文档。
由于前端是纯静态文件,可以使用任意 HTTP 服务器。最简单的方法是使用 Python 的内置服务器:
# 进入前端目录
cd frontend
# 使用 Python 启动 HTTP 服务器
python -m http.server 3000
或者使用 Node.js 的 http-server:
# 全局安装 http-server
npm install -g http-server
# 启动服务器
http-server -p 3000
前端服务将在 http://localhost:3000 启动。
打开浏览器访问 http://localhost:3000 即可使用博客系统。
标准的 Web 开发架构,便于维护和扩展。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online