FastAPI + Jinja2 + HTMX — 纯 Python 全栈 SSR
本项目是从 py4web/tagged_posts 移植而来,旨在验证使用 零 JavaScript 的方式移植完整应用的可能性。
| 方面 | py4web 原版 | PyNuxt 版本 |
|---|---|---|
| 前端框架 | Vue.js | Jinja2 + HTMX |
| 页面渲染 | SPA | SSR |
| JavaScript | 必须 | 零 JavaScript |
| 认证方式 | Session | JWT + Cookie |
| 标签过滤 | JavaScript | 纯服务端 |
| 文件路由 | py4web 机制 | Nuxt 风格 |
| 架构模式 | 单一应用 | BFF 分离 |
- 用户注册与登录(Cookie + JWT 认证)
- 发布帖子(支持
#tag格式标签) - 自动提取帖子内容中的标签
- 标签过滤(纯服务端 HTMX 方案)
- 删除自己的帖子
- 待办事项管理
- 零 JavaScript(HTMX 无刷新交互)
- 零 JavaScript:纯服务端渲染,HTMX 处理所有交互
- Cookie 认证:JWT Token 存 Cookie,支持多端(Web、APP)
- BFF 架构:前端 BFF 调用后端 API,后端专注数据层
- 文件系统路由:类似 Nuxt.js,自动映射页面路由
浏览器 ──HTMX──► 前端服务 (:3000) ──HTTP──► 后端服务 (:8012)
│ │
├── Jinja2 整页渲染 ├── REST API (JSON)
├── Jinja2 片段渲染 └── SQLAlchemy ORM
└── Cookie 认证 └── JWT 认证
frontend/
├── pages/ # 文件系统路由
│ ├── index.html # 首页 - 标签帖子
│ └── login.html # 登录页
├── layouts/ # 布局
│ └── default.html
├── components/ # 组件
│ ├── post_list.html
│ ├── post_item.html
│ ├── tag_filter.html
│ └── ...
└── static/
└── js/
└── htmx.min.js # HTMX(唯一的 JS 依赖)
backend/ 目录下的代码直接复制自 app+3,一行不许改动。
数据库文件存放在项目根目录 data.db。
所有打磨和优化都在 frontend/ 目录下进行。
# 安装依赖
pip install -r requirements.txt --break-system-packages
# 启动后端(端口 8012)
cd backend
uvicorn main:app --port 8012 --reload
# 启动前端(端口 3000,新终端)
cd frontend
uvicorn main:app --port 3000 --reload或一键启动(PowerShell):
.\start-all.ps1 # 启动前后端
.\stop-all.ps1 # 停止等同 Nuxt 的 pages/ 约定:在 pages/ 下放 .html 文件,自动映射为页面路由,无需手动注册。
/ -> pages/index.html
/about -> pages/about.html
/users/list -> pages/users/list.html
/posts/123 -> pages/posts/[id].html (动态路由)
/users/jeff/profile -> pages/users/[uid]/profile.html (嵌套动态)
动态路由:文件名或目录名用 [param] 形式,自动捕获为路由参数,注入模板上下文:
<!-- pages/posts/[id].html -->
<h1>Post ID: {{ id }}</h1>匹配优先级(从高到低):
- 精确文件匹配:
/about→pages/about.html - 精确目录匹配:
/users/list→pages/users/list.html - 动态文件匹配:
/posts/123→pages/posts/[id].html - 动态目录匹配:
/users/jeff/profile→pages/users/[uid]/profile.html
实现原理(借鉴 app-6):
main.py用 catch-all 路由/{path:path}捕获所有非 API 请求- 路由引擎递归匹配路径:先尝试静态精确匹配,再尝试
[param]动态匹配 - 动态参数注入模板上下文,模板中直接用
{{ param }}访问 - 通过
jinja2.Environment+FileSystemLoader渲染,模板按相对路径解析 - Jinja2 的
{% extends %}/{% include %}直接从 loader 根目录解析,跨目录引用正常工作 - HTMX API 路由(
/api/*)注册在 catch-all 之前,优先匹配
PyNuxt/
├── backend/ # ⚠️ 复制自 app+3,不许改动
│ ├── main.py
│ ├── config.py
│ ├── database.py
│ ├── models.py
│ ├── schemas.py
│ └── routers/
│ ├── todos.py
│ ├── auth.py # JWT 认证
│ └── posts.py # 帖子 API
├── frontend/ # 🎯 打磨优化目标
│ ├── main.py # 入口(API 路由 + install_file_routing)
│ ├── config.py # 集中配置(等同 nuxt.config)
│ ├── bff_core.py # BFF 业务子类(PostBFF, TodoBFF, AuthBFF)
│ ├── pages/ # 文件系统路由(等同 Nuxt pages/)
│ │ ├── index.html # 首页 - 标签帖子
│ │ └── login.html # 登录页
│ ├── layouts/
│ │ └── default.html
│ ├── components/
│ │ ├── post_list.html
│ │ ├── post_item.html
│ │ ├── tag_filter.html
│ │ ├── todo_list.html
│ │ └── ...
│ └── pynuxt/ # 🔧 框架包(不用改)
│ ├── routing.py # 文件系统路由引擎
│ ├── bff.py # BFF 基座(HTTP 客户端 + 模板渲染)
│ ├── auth.py # 认证工具(Cookie/JWT)
│ └── errors.py # 错误处理
│ │
│ └── static/
│ ├── css/style.css
│ └── js/
│ └── htmx.min.js # 唯一的 JS 依赖
├── data.db # SQLite 数据库(根目录)
├── start-all.ps1
├── stop-all.ps1
├── requirements.txt
└── README.md
框架代码与业务代码分离,借鉴 Nuxt 自身的设计:
| 模块 | 职责 | 等同 Nuxt |
|---|---|---|
pynuxt/routing.py |
文件系统路由引擎 | @nuxt/router |
pynuxt/bff.py |
BFF 基座(HTTP + 渲染) | server/api 引擎 |
pynuxt/helpers.py |
HTMX 辅助 | composables/useHtmx() |
用户代码只做两件事:
- 继承
BFFBase写业务方法(bff_core.py) - 在
pages/下放模板,自动映射路由
| 接口 | 方法 | 说明 |
|---|---|---|
| 认证 | ||
/api/auth/register |
POST | 注册用户 |
/api/auth/login |
POST | 登录 |
/api/auth/me |
GET | 获取当前用户 |
| 帖子 | ||
/api/posts |
GET | 获取帖子列表(支持 ?tags= 过滤) |
/api/posts |
POST | 创建帖子 |
/api/posts/{id} |
DELETE | 删除帖子 |
/api/posts/tags |
GET | 获取所有标签 |
| 待办 | ||
/api/todos |
GET/POST | 获取/创建待办 |
/api/todos/{id} |
GET/PUT/DELETE | 获取/更新/删除 |
/api/todos/{id}/done |
PUT | 切换完成状态 |
| 接口 | 方法 | 说明 |
|---|---|---|
/api/posts |
GET/POST | 获取/创建帖子 HTML |
/api/posts/tags |
GET | 获取标签列表 HTML |
/api/todos |
GET/POST | 获取/创建待办 HTML |
/api/todos/{id} |
GET/PUT/DELETE | 获取/更新/删除待办 HTML |
/api/auth/login |
POST | 登录表单提交 |
/api/auth/register |
POST | 注册表单提交 |
- FileSystemLoader 根目录:
frontend/(config.py 中TEMPLATE_DIRS = ["."]) - 跨目录引用:从根出发,如
{% extends "layouts/default.html" %} - 同目录引用:直接用文件名,如
{% import "post_item.html" as item %} - 组件传参:用
{% macro %}宏 +{% import %}引入 - 页面模板:放
pages/,通过env.get_template()按相对路径渲染
-
Jinja2 与 HTMX 的协同
- 问题:需要一种既能服务端渲染又能保持前端交互最小化的模板方案
- 解决:Jinja2 负责服务端 HTML 渲染,HTMX 处理无刷新交互,完全不需要 JavaScript
-
标签过滤的 JavaScript 依赖
- 问题:初始实现使用 JavaScript 管理标签状态
- 解决:改用纯服务端 HTMX 方案,标签点击通过 HTMX 请求服务端渲染
-
JWT Token 管理
- 问题:需要支持多端(Web、APP),但又不想写 JavaScript
- 解决:JWT Token 存 Cookie,浏览器自动发送,无需 JavaScript 操作
以下功能已实现零 JavaScript:
- ✅ 标签过滤(纯 HTMX)
- ✅ 帖子列表加载(HTMX)
- ✅ 待办事项管理(HTMX)
- ✅ 登录/登出(Cookie 自动携带)
- ✅ 页面导航(传统链接)
唯一的 JavaScript 依赖:HTMX 库本身(htmx.min.js)