文章目录
-
- 前言
- 一、环境准备
-
- [1.1 项目结构](#1.1 项目结构)
- [1.2 安装Python库](#1.2 安装Python库)
- [1.3 准备MySQL数据库](#1.3 准备MySQL数据库)
- 二、编写代码
-
- [2.1 创建案例数据](#2.1 创建案例数据)
- [2.2 编写主应用文件 `main.py`](#2.2 编写主应用文件
main.py) - [2.3 创建新闻列表页 `templates/index.html`](#2.3 创建新闻列表页
templates/index.html) - [2.4 创建新闻详情页 `templates/detail.html`](#2.4 创建新闻详情页
templates/detail.html) - [2.5 运行和测试](#2.5 运行和测试)
前言
该项目页面打开是新闻列表页,截图如下:

点击详情可以进入详情页,截图如下:

该项目属于基础入门项目。
一、环境准备
1.1 项目结构
news-app/
├── main.py # FastAPI应用主文件
├── requirements.txt # 项目依赖
├── .env # 环境变量文件
├── create_sample_data.py # 脚本:用于创建示例数据
└── templates/
├── index.html # 新闻列表页
└── detail.html # 新闻详情页
1.2 安装Python库
bash
pip install fastapi "uvicorn[standard]" jinja2 sqlalchemy pymysql python-dotenv
1.3 准备MySQL数据库
确保MySQL服务正在运行,并且有相应数据库,然后在项目根目录创建.env文件,存储数据库连接信息。
ini
DATABASE_URL="mysql+pymysql://your_username:your_password@127.0.0.1:3306/news_app"
将 your_username 和 your_password 替换为MySQL凭据。
二、编写代码
2.1 创建案例数据
1、编写 create_sample_data.py :这个脚本会帮助我们创建数据库表并插入一些示例新闻,方便我们测试。
python
import os
import random
from datetime import datetime, timedelta
from dotenv import load_dotenv
from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
# 加载环境变量
load_dotenv()
DATABASE_URL = os.getenv("DATABASE_URL")
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
# 新闻模型
class News(Base):
__tablename__ = "fastapi_news"
id = Column(Integer, primary_key=True, index=True)
title = Column(String(255), nullable=False)
content = Column(Text, nullable=False)
author = Column(String(100), nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
# 创建表
Base.metadata.create_all(bind=engine)
# 生成示例数据
def generate_sample_data():
db = SessionLocal()
# 如果已有数据,则不重复生成
if db.query(News).count() > 0:
print("数据库中已有数据,跳过生成。")
db.close()
return
titles = [
"FastAPI框架:下一代Python Web开发的王者?",
"深入理解Python异步编程:asyncio完全指南",
"MySQL性能优化实战:从慢查询到毫秒响应",
"Docker容器化部署:让应用开发如虎添翼",
"人工智能浪潮下,程序员的未来在哪里?",
"前端框架大比拼:Vue vs React vs Angular",
"微服务架构设计:如何构建高可用系统",
"数据科学入门:Pandas与NumPy核心技巧",
"网络安全新挑战:如何防范常见的Web攻击",
"区块链技术原理与应用场景深度剖析",
"云计算时代:AWS、Azure、Google Cloud如何选择?",
"机器学习项目实战:从数据清洗到模型部署",
"TypeScript:大型JavaScript项目的必备利器",
"Kubernetes入门:容器编排的艺术",
"代码审查最佳实践:提升团队代码质量",
"DevOps文化:打通开发与运维的任督二脉",
"图数据库Neo4j:探索关系型数据的新维度",
"Rust语言:系统级编程的未来之星?",
"量子计算离我们还有多远?技术前沿展望",
"设计模式精髓:写出优雅可维护的代码"
]
authors = ["科技前沿", "开发者日报", "AI观察员", "架构师之道", "代码诗人"]
print("正在生成50条示例新闻数据...")
for i in range(50):
title = random.choice(titles) + f" (第{i + 1}期)"
content = f"""
<p>这是关于 <strong>{title}</strong> 的详细报道。</p>
<p>在当今快速发展的技术世界中,{random.choice(['创新', '变革', '演进', '突破'])}是永恒的主题。
本文将深入探讨相关的技术细节、市场趋势以及未来的发展方向。我们采访了多位行业专家,
他们分享了宝贵的见解和经验。</p>
<p>首先,我们需要了解其核心原理。{random.choice(['从架构设计来看', '从算法层面分析', '从用户体验角度出发'])},
这项技术为我们带来了前所未有的可能性。它不仅提高了效率,还降低了成本。</p>
<p>然而,挑战依然存在。{random.choice(['如何规模化应用', '如何保证安全性', '如何进行人才培养'])}
是我们需要共同面对的问题。未来,我们期待看到更多的{random.choice(['创新应用', '跨界合作', '开源贡献'])}。</p>
<p>总之,{title}代表了当前技术发展的一个重要方向,值得我们持续关注和深入研究。</p>
"""
author = random.choice(authors)
# 生成过去30天内的随机时间
created_at = datetime.utcnow() - timedelta(days=random.randint(0, 30), hours=random.randint(0, 23))
news_item = News(title=title, content=content, author=author, created_at=created_at)
db.add(news_item)
db.commit()
print("50条示例数据生成成功!")
db.close()
if __name__ == "__main__":
generate_sample_data()
2、运行脚本生成数据
在终端中运行此脚本:
bash
python create_sample_data.py
2.2 编写主应用文件 main.py
这是整个应用的核心,整合了API、数据库操作和页面渲染。
python
# main.py
import os
import math
import uvicorn
from typing import List, Optional
from dotenv import load_dotenv
from fastapi import FastAPI, Request, Depends, HTTPException, status
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse
from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime, func
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session
from datetime import datetime
# --- 1. 基础配置 ---
load_dotenv()
DATABASE_URL = os.getenv("DATABASE_URL")
if not DATABASE_URL:
raise ValueError("DATABASE_URL not found in .env file")
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
app = FastAPI(title="News App with MySQL")
templates = Jinja2Templates(directory="templates")
# --- 2. 数据库模型 ---
class News(Base):
__tablename__ = "fastapi_news"
id = Column(Integer, primary_key=True, index=True)
title = Column(String(255), nullable=False)
content = Column(Text, nullable=False)
author = Column(String(100), nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
# --- 3. 依赖注入 ---
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
# --- 4. 分页辅助类 ---
class Pagination:
def __init__(self, query, page: int, per_page: int):
self.query = query
self.page = page
self.per_page = per_page
self.total = query.count()
self.items = query.offset((page - 1) * per_page).limit(per_page).all()
@property
def pages(self):
return int(math.ceil(self.total / float(self.per_page)))
def has_prev(self):
return self.page > 1
def has_next(self):
return self.page < self.pages
def iter_pages(self, left_edge=2, left_current=2, right_current=3, right_edge=2):
last = 0
for num in range(1, self.pages + 1):
if num <= left_edge or (num > self.page - left_current - 1 and num < self.page + right_current) \
or num > self.pages - right_edge:
if last + 1 != num:
yield None
yield num
last = num
# --- 5. 页面路由 ---
@app.get("/", response_class=HTMLResponse)
def read_news_list(request: Request, page: int = 1, db: Session = Depends(get_db)):
per_page = 10 # 每页显示10条新闻
news_query = db.query(News).order_by(News.created_at.desc())
pagination = Pagination(news_query, page, per_page)
return templates.TemplateResponse("index.html", {
"request": request,
"news_list": pagination
})
@app.get("/news/{news_id}", response_class=HTMLResponse)
def read_news_detail(request: Request, news_id: int, db: Session = Depends(get_db)):
news = db.query(News).filter(News.id == news_id).first()
if not news:
raise HTTPException(status_code=404, detail="News not found")
return templates.TemplateResponse("detail.html", {
"request": request,
"news": news
})
# --- 6. API 路由 (可选,用于未来扩展) ---
@app.get("/api/news", response_model=List[dict])
def api_get_all_news(db: Session = Depends(get_db)):
news = db.query(News).order_by(News.created_at.desc()).all()
return [
{"id": n.id, "title": n.title, "author": n.author, "created_at": n.created_at}
for n in news
]
@app.get("/api/news/{news_id}", response_model=dict)
def api_get_news(news_id: int, db: Session = Depends(get_db)):
news = db.query(News).filter(News.id == news_id).first()
if not news:
raise HTTPException(status_code=404, detail="News not found")
return {
"id": news.id,
"title": news.title,
"content": news.content,
"author": news.author,
"created_at": news.created_at
}
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
2.3 创建新闻列表页 templates/index.html
html
<!-- templates/index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>今日头条 - FastAPI新闻</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; background-color: #f4f5f6; color: #222; }
.container { max-width: 800px; margin: 20px auto; background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
header { text-align: center; padding-bottom: 20px; border-bottom: 2px solid #ed4040; margin-bottom: 20px; }
header h1 { color: #ed4040; margin: 0; font-size: 2.5em; }
.news-item { border-bottom: 1px solid #eee; padding: 15px 0; cursor: pointer; transition: background-color 0.2s; }
.news-item:hover { background-color: #f9f9f9; }
.news-item:last-child { border-bottom: none; }
.news-title { font-size: 1.2em; font-weight: bold; color: #333; margin-bottom: 5px; }
.news-meta { font-size: 0.9em; color: #888; }
.news-meta span { margin-right: 15px; }
.pagination { display: flex; justify-content: center; align-items: center; margin-top: 30px; gap: 10px; }
.pagination a, .pagination span { padding: 8px 12px; border: 1px solid #ddd; text-decoration: none; color: #333; border-radius: 4px; }
.pagination a:hover { background-color: #ed4040; color: white; border-color: #ed4040; }
.pagination .current { background-color: #ed4040; color: white; border-color: #ed4040; font-weight: bold; }
</style>
</head>
<body>
<div class="container">
<header>
<h1>今日头条</h1>
</header>
<main>
{% for news in news_list.items %}
<div class="news-item" onclick="location.href='/news/{{ news.id }}'">
<div class="news-title">{{ news.title }}</div>
<div class="news-meta">
<span>作者: {{ news.author }}</span>
<span>时间: {{ news.created_at.strftime('%Y-%m-%d %H:%M') }}</span>
</div>
</div>
{% endfor %}
</main>
<div class="pagination">
{% if news_list.has_prev %}
<a href="/?page={{ news_list.prev_num }}"><< 上一页</a>
{% else %}
<span style="color: #ccc;"><< 上一页</span>
{% endif %}
{% for page_num in news_list.iter_pages() %}
{% if page_num %}
{% if page_num != news_list.page %}
<a href="/?page={{ page_num }}">{{ page_num }}</a>
{% else %}
<span class="current">{{ page_num }}</span>
{% endif %}
{% else %}
<span>...</span>
{% endif %}
{% endfor %}
{% if news_list.has_next %}
<a href="/?page={{ news_list.next_num }}">下一页 >></a>
{% else %}
<span style="color: #ccc;">下一页 >></span>
{% endif %}
</div>
</div>
</body>
</html>
2.4 创建新闻详情页 templates/detail.html
html
<!-- templates/detail.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ news.title }} - 今日头条</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; background-color: #f4f5f6; color: #222; line-height: 1.6; }
.container { max-width: 800px; margin: 20px auto; background: #fff; padding: 30px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
header { text-align: center; padding-bottom: 20px; border-bottom: 2px solid #ed4040; margin-bottom: 20px; }
.back-link { display: inline-block; margin-bottom: 20px; color: #ed4040; text-decoration: none; }
.back-link:hover { text-decoration: underline; }
.news-title { font-size: 2.2em; font-weight: bold; color: #333; margin-bottom: 10px; }
.news-meta { font-size: 0.9em; color: #888; margin-bottom: 25px; }
.news-meta span { margin-right: 20px; }
.news-content { font-size: 1.1em; }
.news-content p { margin-bottom: 1em; }
.news-content strong { color: #000; }
</style>
</head>
<body>
<div class="container">
<a href="/" class="back-link">← 返回新闻列表</a>
<header>
<h1 class="news-title">{{ news.title }}</h1>
<div class="news-meta">
<span>作者: {{ news.author }}</span>
<span>发布时间: {{ news.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</span>
</div>
</header>
<main class="news-content">
{{ news.content | safe }}
</main>
</div>
</body>
</html>
2.5 运行和测试
-
启动应用 :
在news-app目录下,运行:bashuvicorn main:app --reload -
访问应用 :
- 新闻列表页 :打开浏览器访问
http://127.0.0.1:8000。你将看到新闻列表,底部有分页导航。 - 新闻详情页:点击任意一条新闻的标题,会跳转到详情页。
- 新闻列表页 :打开浏览器访问
该功能完整的新闻网站,具备了现代Web应用的核心特性,并且结构清晰,易于扩展。