Skip to main content

第3节:让 FastAPI 提供网页服务

这一节是连接“后端”和“前端”的桥梁。 目前的状况是:FastAPI 只能返回 JSON 数据,但浏览器需要 HTML 页面来展示好看的界面。

因此,我们需要做两件事:

  1. 准备页面:我们需要两个标准的 HTML 页面,分别是index.html(首页)和detail.html(详情页)。放在 static 文件夹下。
  2. 配置 FastAPI:告诉 FastAPI,“请把 static 文件夹里的文件开放给浏览器访问”。

1.准备静态页面

我们先来准备静态页面。所谓静态页面,就是指整个页面都是写死的内容,不会根据用户操作或后台数据发生变化。

在根目录下创建static文件夹,然后创建index.html和detail.html文件。目录结构如下:

DailyMovie
├── main.py
├── .venv/
└── static/
├── index.html
└── detail.html

index.html首页

我们先来编写首页。注意包含如下功能:

  • 显示一部电影信息。
  • 点击“换一个”按钮,切换到下一个电影信息。
  • 点击“立即观看”,跳转到电影详情页。

index.htlm具体代码如下:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CineDaily - 每日推荐</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script>
// 配置 Tailwind CSS 主题扩展,包括自定义字体和颜色
tailwind.config = {
theme: {
extend: {
fontFamily: { sans: ['Inter', 'sans-serif'] },
colors: { 'brand-red': '#E50914' }
}
}
}
</script>
<style>
/* 定义淡入动画类(当前未在 JS 中使用,可用于未来 CSS 过渡优化) */
.fade-enter { opacity: 0; transform: translateY(20px); }
.fade-enter-active { opacity: 1; transform: translateY(0); transition: opacity 0.5s ease-out, transform 0.5s ease-out; }
</style>
</head>
<body class="bg-black h-screen w-screen overflow-hidden text-white relative">

<!-- 背景层:两个层用于平滑切换背景图片,实现交叉淡入效果 -->
<div id="bg-layer-1" class="absolute inset-0 w-full h-full bg-cover bg-center transition-opacity duration-700 ease-in-out opacity-100 z-0"></div>
<div id="bg-layer-2" class="absolute inset-0 w-full h-full bg-cover bg-center transition-opacity duration-700 ease-in-out opacity-0 z-0"></div>

<!-- 遮罩层:渐变叠加在背景上,提升内容可读性 -->
<div class="absolute inset-0 bg-gradient-to-r from-black via-black/60 to-transparent z-10"></div>

<!-- 导航栏:固定在左上角,品牌标识 -->
<nav class="absolute top-0 left-0 p-6 z-20">
<div class="text-brand-red font-black text-2xl tracking-tighter uppercase">CINE<span class="text-white">DAILY</span></div>
</nav>

<!-- 内容区:居中布局,包含电影信息和交互按钮 -->
<main class="relative z-20 h-full flex flex-col justify-center px-8 md:px-20 max-w-4xl">
<div id="movie-content" class="fade-enter-active">
<div class="flex items-center gap-3 mb-4">
<span id="movie-tag" class="bg-white/20 backdrop-blur border border-white/10 px-2 py-0.5 rounded text-sm">标签</span>
<span class="text-yellow-400 font-bold"><i class="fas fa-star"></i> <span id="movie-score">0.0</span></span>
</div>

<h1 id="movie-title" class="text-5xl md:text-7xl font-extrabold mb-6 leading-tight">电影标题</h1>
<p id="movie-desc" class="text-gray-300 text-lg md:text-xl line-clamp-3 mb-8 max-w-2xl">简介...</p>

<div class="flex gap-4">
<!-- 观看按钮:链接到详情页,href 由 JS 动态更新,携带电影 ID -->
<a id="btn-watch" href="#" class="bg-brand-red hover:bg-red-700 text-white px-8 py-3 rounded font-bold text-lg flex items-center gap-2 transition group">
<i class="fas fa-play"></i> 立即观看
</a>

<!-- 换一个按钮:触发下一部电影的切换动画 -->
<button onclick="nextMovie()" class="bg-white/10 hover:bg-white/20 backdrop-blur border border-white/20 text-white px-6 py-3 rounded font-bold text-lg transition">
<i class="fas fa-random"></i> 换一个
</button>
</div>
</div>
</main>

<script>
// 电影数据数组:模拟 API 返回的数据,每项包含标题、标签、评分、描述和背景图片 URL
// 注意:实际项目中应从后端 API 获取数据,此处为演示硬编码
const movies = [
{
id: 1, // 电影唯一 ID,用于详情页路由参数
title: "星际穿越",
tag: "科幻 / 冒险",
score: "9.4",
desc: "在地球环境日益恶化的未来,一组探险家利用新发现的虫洞,超越了人类太空旅行的极限。",
bg: "https://images.unsplash.com/photo-1536440136628-849c177e76a1?q=80&w=2525&auto=format&fit=crop"
},
{
id: 2,
title: "赛博朋克:边缘",
tag: "科幻 / 动画",
score: "9.1",
desc: "在一个沉迷于肉体改造的未来城市中,一名流浪街头的少年为了生存,选择成为一名雇佣兵。",
bg: "https://images.unsplash.com/photo-1626814026160-2237a95fc5a0?q=80&w=2070&auto=format&fit=crop"
},
{
id: 3,
title: "爱乐之城",
tag: "爱情 / 歌舞",
score: "8.5",
desc: "米娅渴望成为一名演员,但至今她仍旧只是片场咖啡厅里的一名平凡巴师。塞巴斯汀醉心于爵士乐。",
bg: "https://images.unsplash.com/photo-1518609878373-06d740f60d8b?q=80&w=2070&auto=format&fit=crop"
}
];

// 当前电影索引:从 0 开始循环遍历数组
let currentIndex = 0;

// DOM 元素缓存:提高性能,避免重复查询
const dom = {
bg1: document.getElementById('bg-layer-1'),
bg2: document.getElementById('bg-layer-2'),
content: document.getElementById('movie-content'),
title: document.getElementById('movie-title'),
tag: document.getElementById('movie-tag'),
score: document.getElementById('movie-score'),
desc: document.getElementById('movie-desc'),
btnWatch: document.getElementById('btn-watch')
};

// 初始化:加载第一部电影并设置初始背景
updateUI(movies[0]);
dom.bg1.style.backgroundImage = `url(${movies[0].bg})`;

// 更新 UI 函数:根据电影对象刷新页面内容
function updateUI(movie) {
dom.title.textContent = movie.title;
dom.tag.textContent = movie.tag;
dom.score.textContent = movie.score;
dom.desc.textContent = movie.desc;
// 更新观看按钮链接:指向详情页,携带电影 ID 参数
dom.btnWatch.href = `detail.html?id=${movie.id}`;
}

// 切换到下一部电影:处理背景淡入淡出和内容动画
function nextMovie() {
// 计算下一个索引,实现循环播放
currentIndex = (currentIndex + 1) % movies.length;
const nextM = movies[currentIndex];

// 背景切换:确定当前活跃/非活跃层,并预加载新图片
const activeBg = dom.bg1.style.opacity === '1' ? dom.bg1 : dom.bg2;
const inactiveBg = dom.bg1.style.opacity === '1' ? dom.bg2 : dom.bg1;
inactiveBg.style.backgroundImage = `url(${nextM.bg})`;

// 触发背景淡出/入(利用 CSS transition,持续 700ms)
activeBg.style.opacity = '0';
inactiveBg.style.opacity = '1';

// 内容淡出:准备动画过渡
dom.content.style.opacity = 0;
dom.content.style.transform = 'translateY(20px)';

// 延迟更新内容:等待 500ms(略短于背景过渡,确保内容在背景稳定后出现)
setTimeout(() => {
updateUI(nextM);
dom.content.style.opacity = 1;
dom.content.style.transform = 'translateY(0)';
}, 500);
}
</script>
</body>
</html>

这个代码实现了一个名为“CineDaily”的电影推荐网页应用,使用HTML、Tailwind CSS和JavaScript构建。页面以全屏黑底布局,展示随机电影的背景图片、标题、标签、评分和简介。核心功能包括:初始化加载第一部电影;点击“换一个”按钮循环切换下一部电影,伴随背景淡入淡出(700ms)和内容淡出淡入(500ms)动画;“立即观看”按钮动态链接至详情页(携带ID)。数据硬编码3部电影模拟API,支持响应式设计和字体图标,提升用户沉浸式浏览体验。

上面的代码中,HTML实现页面框架,tailwindcss 实现页面样式,js代码实现页面特效(如换一个淡入淡出等)。如果大家对js代码不熟,可以先快速学习一下js基础,这里只要求大家根据注释能看懂即可。

使用浏览器打开index.html文件,显示效果如下图所示。

image-20251124135539521

说明:

点击“立即观看“,会提示”无法访问您的文件“。这是因为点击”立即观看“按钮,页面跳转到detail.html页面,由于还没编写这个文件,所以提示”无法访问您的文件“。

detail.html详情页

继续编写详情页,主要内容包括:

  • 电影详细信息
  • 播放电影

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>电影详情 - CineDaily</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: { sans: ['Inter', 'sans-serif'] },
colors: { 'brand-red': '#E50914', 'dark-bg': '#141414' }
}
}
}
</script>
</head>
<body class="bg-dark-bg text-gray-100 min-h-screen font-sans">

<!-- 顶部导航 -->
<nav class="sticky top-0 z-50 bg-black/90 border-b border-gray-800 px-6 py-4 flex justify-between items-center">
<a href="index.html" class="text-gray-400 hover:text-white flex items-center gap-2 transition">
<i class="fas fa-arrow-left"></i> 返回推荐
</a>
<div class="text-brand-red font-black text-xl uppercase">CINE<span class="text-white">DAILY</span></div>
</nav>

<!-- 电影主要展示区 -->
<main class="container mx-auto px-4 py-8 max-w-6xl">

<!-- 视频播放器容器 -->
<div class="relative w-full aspect-video bg-black rounded-xl overflow-hidden shadow-2xl border border-gray-800 mb-8 group">
<!-- 视频 -->
<!-- 这里使用了开源的测试视频源 -->
<video id="video-player" class="w-full h-full object-contain" controls autoplay>
<source src="" type="video/mp4">
您的浏览器不支持 Video 标签。
</video>
</div>

<!-- 电影信息网格 -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-12">

<!-- 左侧:详细文本 -->
<div class="lg:col-span-2 space-y-6">
<div>
<h1 id="d-title" class="text-4xl md:text-5xl font-bold text-white mb-2">标题加载中...</h1>
<div class="flex items-center gap-4 text-sm text-gray-400">
<span id="d-year">2023</span>
<span class="border border-gray-600 px-1 rounded text-xs">HD</span>
<span id="d-duration">2h 0m</span>
<span class="text-green-500 font-bold">98% 推荐</span>
</div>
</div>

<!-- 标签 -->
<div id="d-tags" class="flex gap-2">
<!-- JS 插入 -->
</div>

<p id="d-desc" class="text-gray-300 text-lg leading-relaxed">
简介加载中...
</p>

<!-- 按钮组 -->
<div class="flex gap-4 pt-4 border-t border-gray-800">
<button class="bg-white text-black px-6 py-2 rounded font-bold hover:bg-gray-200 transition">
<i class="fas fa-share"></i> 分享给朋友
</button>
<button class="bg-gray-800 text-white px-6 py-2 rounded font-bold hover:bg-gray-700 transition">
<i class="fas fa-plus"></i> 加入收藏
</button>
</div>
</div>

<!-- 右侧:演职员表 -->
<div class="space-y-8">
<div class="bg-[#1f1f1f] p-6 rounded-lg">
<h3 class="text-gray-500 text-sm font-bold uppercase tracking-widest mb-4">导演</h3>
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-gray-700 rounded-full flex items-center justify-center">
<i class="fas fa-video text-gray-400"></i>
</div>
<span id="d-director" class="text-white font-medium">Director Name</span>
</div>
</div>

<div class="bg-[#1f1f1f] p-6 rounded-lg">
<h3 class="text-gray-500 text-sm font-bold uppercase tracking-widest mb-4">主演</h3>
<div id="d-actors" class="space-y-3">
<!-- JS 插入 -->
</div>
</div>
</div>
</div>

</main>

<!-- 页脚 -->
<footer class="border-t border-gray-800 mt-12 py-8 text-center text-gray-500 text-sm">
<p>© 2024 CineDaily Demo. 视频源自 Google/Blender Foundation 测试库。</p>
</footer>

<script>
// 完整的数据源 (与 index.html 保持一致,实际开发中应从服务器获取)
const moviesData = [
{
id: 1,
title: "星际穿越",
year: "2014",
duration: "2h 49m",
tag: "科幻 / 冒险",
director: "Christopher Nolan",
actors: ["Matthew McConaughey", "Anne Hathaway", "Jessica Chastain"],
desc: "在地球环境日益恶化的未来,一组探险家利用新发现的虫洞,超越了人类太空旅行的极限,试图在广袤的宇宙中寻找人类的新家园。这是一场关于爱、时间与生存的宏大叙事。",
// 使用免费的 Big Buck Bunny 作为演示视频
videoUrl: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"
},
{
id: 2,
title: "赛博朋克:边缘",
year: "2077",
duration: "24m / Ep",
tag: "动画 / 科幻",
director: "Hiroyuki Imaishi",
actors: ["KENN", "Aoi Yuki", "Hiroki Touchi"],
desc: "在一个沉迷于肉体改造的未来城市中,一名流浪街头的少年为了生存,选择成为一名雇佣兵——也就是众所周知的“赛博朋克”。",
// 使用 Elephants Dream 作为演示视频
videoUrl: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4"
},
{
id: 3,
title: "爱乐之城",
year: "2016",
duration: "2h 8m",
tag: "爱情 / 歌舞",
director: "Damien Chazelle",
actors: ["Ryan Gosling", "Emma Stone", "John Legend"],
desc: "米娅渴望成为一名演员,但至今她仍旧只是片场咖啡厅里的一名平凡巴师。塞巴斯汀醉心于爵士乐,但在现实面前只能在餐厅里弹奏解闷的乐曲。",
// 使用 For Bigger Blazes 作为演示视频
videoUrl: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4"
}
];

// 1. 获取 URL 参数中的 ID
const params = new URLSearchParams(window.location.search);
const movieId = parseInt(params.get('id'));

// 2. 查找对应电影 (如果没有找到,默认显示第1个)
const movie = moviesData.find(m => m.id === movieId) || moviesData[0];

// 3. 渲染页面
document.getElementById('d-title').textContent = movie.title;
document.getElementById('d-year').textContent = movie.year;
document.getElementById('d-duration').textContent = movie.duration;
document.getElementById('d-desc').textContent = movie.desc;
document.getElementById('d-director').textContent = movie.director;

// 渲染标签
const tagsContainer = document.getElementById('d-tags');
movie.tag.split(' / ').forEach(tag => {
const span = document.createElement('span');
span.className = 'bg-gray-800 text-gray-300 text-xs px-2 py-1 rounded';
span.textContent = tag;
tagsContainer.appendChild(span);
});

// 渲染演员
const actorsContainer = document.getElementById('d-actors');
movie.actors.forEach(actor => {
const div = document.createElement('div');
div.className = 'flex items-center gap-3';
div.innerHTML = `
<div class="w-8 h-8 bg-gray-700 rounded-full flex items-center justify-center text-xs"><i class="fas fa-user"></i></div>
<span class="text-gray-300 text-sm">${actor}</span>
`;
actorsContainer.appendChild(div);
});

// 设置视频源
const videoPlayer = document.getElementById('video-player');
videoPlayer.src = movie.videoUrl;

// 自动滚动到顶部
window.scrollTo(0, 0);

</script>
</body>
</html>

这个代码实现了一个名为“CineDaily”的电影详情网页,使用HTML、Tailwind CSS和JavaScript构建。页面从URL参数“id”获取电影ID,从硬编码数据数组(3部电影)中匹配信息,动态渲染标题、年份、时长、标签、简介、导演、主演列表及分享/收藏按钮。核心功能包括嵌入式视频播放器(使用开源测试视频源,如Big Buck Bunny),支持自动播放和控件;顶部导航返回推荐页;响应式布局,确保移动端友好。整体提供沉浸式电影浏览体验,模拟流媒体详情页。

使用浏览器打开detail.html页面,效果如下图所示。

image-20251124140321602

2.挂载静态资源

在 Web 开发中,“静态文件”指的是那些内容固定、不依赖后台逻辑变化的资源,比如 HTML、CSS、JavaScript、图片等。它们只需要被浏览器直接读取和展示即可。如我们前面创建的index.html和detail.html文件。

FastAPI 的核心职责是处理请求与业务逻辑,但它同样支持提供静态资源。通过内置的 StaticFiles 组件,我们可以将某个文件夹挂载到应用中,让 FastAPI 同时充当一个静态文件服务器,为前端页面提供访问入口。

修改main.py文件,代码如下:

from fastapi import FastAPI, HTTPException
from fastapi.staticfiles import StaticFiles # 1. 引入静态文件库
import random
import os

app = FastAPI()

# --- 模拟数据库 ---
movies_data = [

]

# --- 注释原先的路由,因为它的优先级比更高,会覆盖掉下面的mount ---
# @app.get("/")
# def read_root():
# return {"message": "Hello Movie", "status": "OK"}

# --- 2. 挂载静态文件 (放在最后) ---
# 确保 static 文件夹存在,否则会报错
if not os.path.exists("static"):
os.makedirs("static")

# mount 意味着:当用户访问 "/" 下的路径时,去 "static" 文件夹里找对应的文件
# html=True 意味着如果用户访问目录,会自动寻找 index.html
app.mount("/", StaticFiles(directory="static", html=True), name="static")

我们把原来的read_root()注释掉,然后在 main.py 中加了这行代码:

app.mount("/", StaticFiles(directory="static", html=True), name="static")
  • app.mount:挂载,把一个应用安装到主应用上。
  • "/":挂载到根路径。
  • directory="static":告诉 FastAPI,文件都在 static 文件夹里。
  • html=True:如果用户访问 /,FastAPI 会自动找 index.html 并返回,省去了用户手动输入 index.html 的麻烦。

3. 验证步骤

  1. 确保你的目录结构长这样:

    /你的项目文件夹
    ├── main.py
    └── static/
    ├── index.html
    └── detail.html
  2. 重启服务器:

    如果你的 uvicorn 还在运行,它应该会自动重启。如果没有,请手动执行:

    uvicorn main:app --reload
  3. 访问页面:

    打开浏览器访问:http://127.0.0.1:8000

    预期结果:

    你不再看到 { "message": ... } 的 JSON 数据了,而是看到了index.html静态页的内容。

  4. 访问详情页:

    在首页中点击”理解观看“按钮,或是在浏览器地址栏输入:http://127.0.0.1:8000/detail.html?id=1

    会显示详情页的内容。

  5. 如果详细页id不存在,例如在浏览器地址栏输入:http://127.0.0.1:8000/detail.html?id=100,页面效果如下所示。

image-20251124143549691

✅ 本章作业

  1. 成功通过浏览器看到上述两个 HTML 页面。
  2. 确认 /api/movie/random 接口依然可用(在浏览器输入这个地址,应该还能看到 JSON 数据)。
    • 原理:FastAPI 匹配路由时,会先看有没有定义 @app.get,如果没有,再去看静态文件。所以 API 依然优先。

下一节,我们就要进入最激动人心的第4章:用 Vue.js 让页面”动起来“!