FastHTML 最佳实践

FastHTML 应用程序与使用 FastAPI/React、Django 等框架的应用程序不同。不要假设 FastHTML 的最佳实践与其他框架相同。最佳实践体现了 fast.ai 的哲学:减少繁文缛节,利用智能默认值,编写既简洁又清晰的代码。以下是一些人和语言模型有时会忽略的特殊机会。

数据库表的创建

之前

todos = db.t.todos
if not todos.exists():
todos.create(id=int, task=str, completed=bool, created=str, pk='id')

之后

class Todo: id:int; task:str; completed:bool; created:str
todos = db.create(Todo)

FastLite 的 create() 是幂等的——它会在需要时创建表,并无论如何都会返回表对象。使用数据类(dataclass)风格的定义更简洁,也更符合 Python 风格。id 字段自动成为主键。

路由命名约定

之前

@rt("/")
def get(): return Titled("Todo List", ...)

@rt("/add")
def post(task: str): ...

之后

@rt
def index(): return Titled("Todo List", ...) # Special name for "/"
@rt
def add(task: str): ... # Function name becomes route

使用不带参数的 @rt,让函数名定义路由。特殊名称 index 会映射到 /

优先使用查询参数而非路径参数

之前

@rt("/toggle/{todo_id}")
def post(todo_id: int): ...
# URL: /toggle/123

之后

@rt
def toggle(id: int): ...
# URL: /toggle?id=123

在 FastHTML 中,查询参数更符合语言习惯,并能避免在路径中重复参数名称。

善用返回值

之前

@rt
def add(task: str):
  new_todo = todos.insert(task=task, completed=False, created=datetime.now().isoformat())
  return todo_item(todos[new_todo])

@rt
def toggle(id: int):
  todo = todos[id]
  todos.update(completed=not todo.completed, id=id)
  return todo_item(todos[id])

之后

@rt
def add(task: str):
  return todo_item(todos.insert(task=task, completed=False, created=datetime.now().isoformat()))

@rt
def toggle(id: int):
  return todo_item(todos.update(completed=not todos[id].completed, id=id))

insert()update() 都会返回受影响的对象,从而实现函数式链式调用。

使用 .to() 生成 URL

之前

hx_post=f"/toggle?id={todo.id}"

之后

hx_post=toggle.to(id=todo.id)

.to() 方法能以类型安全的方式生成 URL,并且对重构友好。

PicoCSS 免费附带

之前

style = Style("""
.todo-container { max-width: 600px; margin: 0 auto; padding: 20px; }
/* ... many more lines ... */
""")

之后

# Just use semantic HTML - Pico styles it automatically
Container(...), Article(...), Card(...), Group(...)

fast_app() 默认包含 PicoCSS。请使用 Pico 能自动设置样式的语义化 HTML 元素。对于更复杂的 UI 需求,请使用 MonsterUI(类似 shadcn,但专为 FastHTML 设计)。

智能默认值

之前

return Titled("Todo List", Container(...))

if __name__ == "__main__":
  serve()

之后

return Titled("Todo List", ...)  # Container is automatic

serve()  # No need for if __name__ guard

Titled 已经将内容包裹在 Container 中,而 serve() 会在内部处理主程序入口的检查。

FastHTML 处理可迭代对象

之前

Section(*[todo_item(todo) for todo in all_todos], id="todo-list")

之后

Section(map(todo_item, all_todos), id="todo-list")

FastHTML 组件直接接受可迭代对象——无需使用 * 进行解包。

函数式编程模式

列表推导式很好用,但对于简单的转换,map() 通常更简洁,特别是与 FastHTML 的可迭代对象处理相结合时。

精简代码

之前

@rt
def delete(id: int):
  # Delete from database
  todos.delete(id)
  # Return empty response
  return ""

之后

@rt
def delete(id: int): todos.delete(id)
  • 当代码能够自解释时,省略注释。
  • 不要返回空字符串——默认会返回 None
  • 一行代码只表达一个思想。

所有修改操作都使用 POST

之前

hx_delete=f"/delete?id={todo.id}"

之后

hx_post=delete.to(id=todo.id)

FastHTML 路由默认只处理 GET 和 POST 请求。只使用这两种 HTTP 方法更符合语言习惯,也更简单。

现代 HTMX 事件语法

之前

hx_on="htmx:afterRequest: this.reset()"

之后

hx_on__after_request="this.reset()"

这样做是可行的,因为:

  • hx-on="event: code" 已被弃用;推荐使用 hx-on-event="code"
  • FastHTML 会将 _ 转换为 -(因此 hx_on__after_request 变为 hx-on--after-request)。
  • 在 HTMX 中,:: 可以用作 :htmx: 的简写。
  • HTMX 原生接受 - 来替代 :(所以 -htmx- 的作用与 :htmx: 相同)。
  • HTMX 接受例如 after-request 作为驼峰式命名 afterRequest 的替代方案。