from fasthtml.common import *简明参考
关于 FastHTML
FastHTML 是一个 Python 库,它将 Starlette、Uvicorn、HTMX 和 fastcore 的 FT “FastTags” 整合到一个库中,用于创建服务器渲染的超媒体应用程序。FastHTML 类本身继承自 Starlette,并增加了基于装饰器的路由以及许多附加功能、Beforeware(前置件)、自动将 FT 渲染为 HTML 等等。
编写 FastHTML 应用时需要记住的事项
- 不兼容 FastAPI 语法;FastHTML 适用于 HTML 优先的应用,而不是 API 服务(尽管它也可以实现 API)。
- FastHTML 包含对 Pico CSS 和 fastlite sqlite 库的支持,但两者都是可选的;sqlalchemy 可以直接使用,也可以通过 fastsql 库使用,并且可以使用任何 CSS 框架。MonsterUI 是一个功能更丰富的 FastHTML 优先的组件框架,其功能与 shadcn 类似。
- FastHTML 兼容原生 JS Web 组件和任何原生 JS 库,但不兼容 React、Vue 或 Svelte。
- 使用
serve()来运行 uvicorn(不需要if __name__ == "__main__",因为它是自动的)。 - 当响应需要标题时,使用
Titled;请注意,它已经将子元素包裹在Container中,并且已经包含了 meta 标题和 H1 元素。
最小化应用
这里的代码示例使用了 fast.ai 风格:偏好三元运算符、单行文档字符串、最小化垂直空间等。(通常 fast.ai 风格很少使用或不使用注释,但此处添加注释是为了文档说明。)
一个最小化的 FastHTML 应用看起来像这样:
# Meta-package with all key symbols from FastHTML and Starlette. Import it like this at the start of every FastHTML app.
from fasthtml.common import *
# The FastHTML app object and shortcut to `app.route`
app,rt = fast_app()
# Enums constrain the values accepted for a route parameter
name = str_enum('names', 'Alice', 'Bev', 'Charlie')
# Passing a path to `rt` is optional. If not passed (recommended), the function name is the route ('/foo')
# Both GET and POST HTTP methods are handled by default
# Type-annotated params are passed as query params (recommended) unless a path param is defined (which it isn't here)
@rt
def foo(nm: name):
# `Title` and `P` here are FastTags: direct m-expression mappings of HTML tags to Python functions with positional and named parameters. All standard HTML tags are included in the common wildcard import.
# When a tuple is returned, this returns concatenated HTML partials. HTMX by default will use a title HTML partial to set the current page name. HEAD tags (e.g. Meta, Link, etc) in the returned tuple are automatically placed in HEAD; everything else is placed in BODY.
# FastHTML will automatically return a complete HTML document with appropriate headers if a normal HTTP request is received. For an HTMX request, however, just the partials are returned.
return Title("FastHTML"), H1("My web app"), P(f"Hello, {name}!")
# By default `serve` runs uvicorn on port 5001. Never write `if __name__ == "__main__"` since `serve` checks it internally.
serve()运行此 Web 应用:
python main.py # access via localhost:5001JS
Script 函数允许您包含 JavaScript。您可以使用 Python 生成部分 JS 或 JSON,如下所示:
# In future snippets this import will not be shown, but is required
from fasthtml.common import *
app,rt = fast_app(hdrs=[Script(src="https://cdn.plot.ly/plotly-2.32.0.min.js")])
# `index` is a special function name which maps to the `/` route.
@rt
def index():
data = {'somedata':'fill me in…'}
# `Titled` returns a title tag and an h1 tag with the 1st param, with remaining params as children in a `Main` parent.
return Titled("Chart Demo", Div(id="myDiv"), Script(f"var data = {data}; Plotly.newPlot('myDiv', data);"))
# In future snippets `serve() will not be shown, but is required
serve()尽可能优先使用 Python 而不是 JS。绝不使用 React 或 shadcn。
fast_app 响应头
# In future snippets we'll skip showing the `fast_app` call if it has no params
app, rt = fast_app(
pico=False, # The Pico CSS framework is included by default, so pass `False` to disable it if needed. No other CSS frameworks are included.
# These are added to the `head` part of the page for non-HTMX requests.
hdrs=(
Link(rel='stylesheet', href='assets/normalize.min.css', type='text/css'),
Link(rel='stylesheet', href='assets/sakura.css', type='text/css'),
Style("p {color: red;}"),
# `MarkdownJS` and `HighlightJS` are available via concise functions
MarkdownJS(), HighlightJS(langs=['python', 'javascript', 'html', 'css']),
# by default, all standard static extensions are served statically from the web app dir,
# which can be modified using e.g `static_path='public'`
)
)
@rt
def index(req): return Titled("Markdown rendering example",
# This will be client-side rendered to HTML with highlight-js
Div("*hi* there",cls="marked"),
# This will be syntax highlighted
Pre(Code("def foo(): pass")))响应
路由可以返回多种类型:
- FastTags 或 FastTags 元组(自动渲染为 HTML)
- 标准的 Starlette 响应(直接使用)
- 可 JSON 序列化的类型(在纯文本响应中作为 JSON 返回)
@rt("/{fname:path}.{ext:static}")
async def serve_static_file(fname:str, ext:str): return FileResponse(f'public/{fname}.{ext}')
app, rt = fast_app(hdrs=(MarkdownJS(), HighlightJS(langs=['python', 'javascript'])))
@rt
def index():
return Titled("Example",
Div("*markdown* here", cls="marked"),
Pre(Code("def foo(): pass")))路由函数可以用在 href 或 action 等属性中,并将被转换为路径。使用 .to() 来生成带有查询参数的路径。
@rt
def profile(email:str): return fill_form(profile_form, profiles[email])
profile_form = Form(action=profile)(
Label("Email", Input(name="email")),
Button("Save", type="submit")
)
user_profile_path = profile.to(email="[email protected]") # '/profile?email=user%40example.com'from dataclasses import dataclass
app,rt = fast_app()当路由处理函数被用作 fasttag 属性(例如 href、hx_get 或 action)时,它会被转换为该路由的路径。fill_form 用于将对象的匹配属性复制到同名的表单字段中。
@dataclass
class Profile: email:str; phone:str; age:int
email = '[email protected]'
profiles = {email: Profile(email=email, phone='123456789', age=5)}
@rt
def profile(email:str): return fill_form(profile_form, profiles[email])
profile_form = Form(method="post", action=profile)(
Fieldset(
Label('Email', Input(name="email")),
Label("Phone", Input(name="phone")),
Label("Age", Input(name="age"))),
Button("Save", type="submit"))测试
我们可以使用 TestClient 进行测试。
from starlette.testclient import TestClientpath = "/[email protected]"
client = TestClient(app)
htmx_req = {'HX-Request':'1'}
print(client.get(path, headers=htmx_req).text)<form enctype="multipart/form-data" method="post" action="/profile"><fieldset><label>Email <input name="email" value="[email protected]">
</label><label>Phone <input name="phone" value="123456789">
</label><label>Age <input name="age" value="5">
</label></fieldset><button type="submit">Save</button></form>
表单处理和数据绑定
当数据类(dataclass)、命名元组(namedtuple)等用作类型注解时,表单主体将自动解包到匹配的属性名称中。
@rt
def edit_profile(profile: Profile):
profiles[email]=profile
return RedirectResponse(url=path)
new_data = dict(email='[email protected]', phone='7654321', age=25)
print(client.post("/edit_profile", data=new_data, headers=htmx_req).text)<form enctype="multipart/form-data" method="post" action="/profile"><fieldset><label>Email <input name="email" value="[email protected]">
</label><label>Phone <input name="phone" value="7654321">
</label><label>Age <input name="age" value="25">
</label></fieldset><button type="submit">Save</button></form>
fasttag 渲染规则
在元组或 fasttag 子元素中渲染子元素的一般规则是: - 将调用 __ft__ 方法(对于默认组件如 P、H2 等,或者如果您定义了自己的组件) - 如果传递字符串,它将被转义 - 对于其他 Python 对象,将调用 str()
如果你想直接将普通 HTML 标签包含到例如 Div() 中,它们默认会被转义(作为防止代码注入的安全措施)。这可以通过使用 Safe(...) 来避免,例如要显示一个数据帧,请使用 Div(NotStr(df.to_html()))。
异常
FastHTML 允许自定义异常处理程序。
def not_found(req, exc): return Titled("404: I don't exist!")
exception_handlers = {404: not_found}
app, rt = fast_app(exception_handlers=exception_handlers)请求和会话对象
FastHTML 通过使用特殊的 session 参数名(或该名称的任何前缀),自动提供对 Starlette 会话中间件的访问。
@rt
def profile(req, sess, user_id: int=None):
ip = req.client.host
sess['last_visit'] = datetime.now().isoformat()
visits = sess.setdefault('visit_count', 0) + 1
sess['visit_count'] = visits
user = get_user(user_id or sess.get('user_id'))
return Titled(f"Profile: {user.name}",
P(f"Visits: {visits}"),
P(f"IP: {ip}"),
Button("Logout", hx_post=logout))处理函数可以返回 HtmxResponseHeaders 对象来设置 HTMX 特定的响应头。
@rt
def htmlredirect(app): return HtmxResponseHeaders(location="http://example.org")APIRouter
APIRouter 让你可以在 FastHTML 应用中跨多个文件组织路由。
# products.py
ar = APIRouter()
@ar
def details(pid: int): return f"Here are the product details for ID: {pid}"
@ar
def all_products(req):
return Div(
Div(
Button("Details",hx_get=details.to(pid=42),hx_target="#products_list",hx_swap="outerHTML",),
), id="products_list")# main.py
from products import ar,all_products
app, rt = fast_app()
ar.to_app(app)
@rt
def index():
return Div(
"Products",
hx_get=all_products, hx_swap="outerHTML")Toast 提示
Toast 提示可以有四种类型:
- 信息 (info)
- 成功 (success)
- 警告 (warning)
- 错误 (error)
Toast 提示需要使用 setup_toasts() 函数,并且每个处理程序都需要:
- session 参数
- 必须返回 FT 组件
setup_toasts(app)
@rt
def toasting(session):
add_toast(session, f"cooked", "info")
add_toast(session, f"ready", "success")
return Titled("toaster")setup_toasts(duration) 允许您指定 toast 在消失前可见的时长。默认为 10 秒。
身份验证和授权通过 Beforeware 处理,Beforeware 是在路由处理程序被调用之前运行的函数。
认证
def user_auth_before(req, sess):
# `auth` key in the request scope is automatically provided to any handler which requests it and can not be injected
auth = req.scope['auth'] = sess.get('auth', None)
if not auth: return RedirectResponse('/login', status_code=303)
beforeware = Beforeware(
user_auth_before,
skip=[r'/favicon\.ico', r'/static/.*', r'.*\.css', r'.*\.js', '/login', '/']
)
app, rt = fast_app(before=beforeware)服务器发送事件 (SSE)
FastHTML 支持 HTMX SSE 扩展。
import random
hdrs=(Script(src="https://unpkg.com/[email protected]/sse.js"),)
app,rt = fast_app(hdrs=hdrs)
@rt
def index(): return Div(hx_ext="sse", sse_connect="/numstream", hx_swap="beforeend show:bottom", sse_swap="message")
# `signal_shutdown()` gets an event that is set on shutdown
shutdown_event = signal_shutdown()
async def number_generator():
while not shutdown_event.is_set():
data = Article(random.randint(1, 100))
yield sse_message(data)
@rt
async def numstream(): return EventStream(number_generator())Websockets
FastHTML 为 HTMX 的 websockets 扩展提供了有用的工具。
# These HTMX extensions are available through `exts`:
# head-support preload class-tools loading-states multi-swap path-deps remove-me ws chunked-transfer
app, rt = fast_app(exts='ws')
def mk_inp(): return Input(id='msg', autofocus=True)
@rt
async def index(request):
# `ws_send` tells HTMX to send a message to the nearest websocket based on the trigger for the form element
cts = Div(
Div(id='notifications'),
Form(mk_inp(), id='form', ws_send=True),
hx_ext='ws', ws_connect='/ws')
return Titled('Websocket Test', cts)
async def on_connect(send): await send(Div('Hello, you have connected', id="notifications"))
async def on_disconnect(ws): print('Disconnected!')
@app.ws('/ws', conn=on_connect, disconn=on_disconnect)
async def ws(msg:str, send):
# websocket hander returns/sends are treated as OOB swaps
await send(Div('Hello ' + msg, id="notifications"))
return Div('Goodbye ' + msg, id="notifications"), mk_inp()一个使用 FastHTML 的 setup_ws 函数的聊天机器人示例:
app = FastHTML(exts='ws')
rt = app.route
msgs = []
@rt('/')
def home():
return Div(hx_ext='ws', ws_connect='/ws')(
Div(Ul(*[Li(m) for m in msgs], id='msg-list')),
Form(Input(id='msg'), id='form', ws_send=True)
)
async def ws(msg:str):
msgs.append(msg)
await send(Ul(*[Li(m) for m in msgs], id='msg-list'))
send = setup_ws(app, ws)单文件上传
Form 默认为 “multipart/form-data”。一个 Starlette UploadFile 对象会被传递给处理程序。
upload_dir = Path("filez")
@rt
def index():
return (
Form(hx_post=upload, hx_target="#result")(
Input(type="file", name="file"),
Button("Upload", type="submit")),
Div(id="result")
)
# Use `async` handlers where IO is used to avoid blocking other clients
@rt
async def upload(file: UploadFile):
filebuffer = await file.read()
(upload_dir / file.filename).write_bytes(filebuffer)
return P('Size: ', file.size)对于多文件上传,使用 Input(..., multiple=True),并在处理程序中使用 list[UploadFile] 的类型注解。
Fastlite
Fastlite 及其所基于的 MiniDataAPI 规范是一个面向 CRUD 的 API,用于操作 SQLite。它使用 APSW 和 apswutils 连接 SQLite,并为速度和清晰的错误处理进行了优化。
from fastlite import *db = database(':memory:') # or database('data/app.db')表通常用类来构造,字段类型通过类型提示指定。
class Book: isbn: str; title: str; pages: int; userid: int
# The transform arg instructs fastlite to change the db schema when fields change.
# Create only creates a table if the table doesn't exist.
books = db.create(Book, pk='isbn', transform=True)
class User: id: int; name: str; active: bool = True
# If no pk is provided, id is used as the primary key.
users = db.create(User, transform=True)
users<Table user (id, name, active)>
Fastlite CRUD 操作
fastlite 中的每个操作都返回一个完整的数据类功能的超集。
user = users.insert(name='Alex',active=False)
userUser(id=1, name='Alex', active=0)
# List all records
users()[User(id=1, name='Alex', active=0)]
# Limit, offset, and order results:
users(order_by='name', limit=2, offset=1)
# Filter on the results
users(where="name='Alex'")
# Placeholder for avoiding injection attacks
users("name=?", ('Alex',))
# A single record by pk
users[user.id]User(id=1, name='Alex', active=0)
通过对主键使用 in 关键字来测试记录是否存在。
1 in usersTrue
更新操作(接受字典或类型化对象)返回更新后的记录。
user.name='Lauren'
user.active=True
users.update(user)User(id=1, name='Lauren', active=1)
使用 .xtra() 来自动约束此后的查询、更新和插入操作。
users.xtra(active=True)
users()[User(id=1, name='Lauren', active=1)]
按主键删除:
users.delete(user.id)<Table user (id, name, active)>
通过主键 []、更新和删除操作会引发 NotFoundError。
try: users['Amy']
except NotFoundError: print('User not found')User not found
MonsterUI
MonsterUI 是一个类似 shadcn 的 FastHTML 组件库。它为 FastHTML 添加了基于 Tailwind 的 FrankenUI 和 DaisyUI 库,以及用于 Markdown 的 Python mistletoe、用于代码高亮的 HighlightJS 和用于 LaTeX 支持的 Katex,并尽可能遵循语义化 HTML 模式。当您希望超越 FastHTML 内置 pico 支持的基础功能时,推荐使用它。
一个最小化应用:
from fasthtml.common import *
from monsterui.all import *
app, rt = fast_app(hdrs=Theme.blue.headers(highlightjs=True)) # Use MonsterUI blue theme and highlight code in markdown
@rt
def index():
socials = (('github','https://github.com/AnswerDotAI/MonsterUI'),)
return Titled("App",
Card(
P("App", cls=TextPresets.muted_sm),
# LabelInput, DivLAigned, and UkIconLink are non-semantic MonsterUI FT Components,
LabelInput('Email', type='email', required=True),
footer=DivLAligned(*[UkIconLink(icon,href=url) for icon,url in socials])))MonsterUI 建议
- 尽可能使用默认值,例如 monsterui 中的
Container已经有默认的外边距。 - 使用
*T来保持按钮样式的一致性,例如cls=ButtonT.destructive用于红色的删除按钮,或cls=ButtonT.primary用于号召性用语(CTA)按钮。 - 尽可能使用
Label*函数来创建表单(例如LabelInput、LabelRange),它会创建并适当地链接FormLabel和用户输入,以避免样板代码。
Flex 布局元素(例如 DivLAligned 和 DivFullySpaced)可用于简洁地创建布局。
def TeamCard(name, role, location="Remote"):
icons = ("mail", "linkedin", "github")
return Card(
DivLAligned(
DiceBearAvatar(name, h=24, w=24),
Div(H3(name), P(role))),
footer=DivFullySpaced(
DivHStacked(UkIcon("map-pin", height=16), P(location)),
DivHStacked(*(UkIconLink(icon, height=16) for icon in icons))))表单已为您设置好样式和间距,无需大量额外的类。
def MonsterForm():
relationship = ["Parent",'Sibling', "Friend", "Spouse", "Significant Other", "Relative", "Child", "Other"]
return Div(
DivCentered(
H3("Emergency Contact Form"),
P("Please fill out the form completely", cls=TextPresets.muted_sm)),
Form(
Grid(LabelInput("Name",id='name'),LabelInput("Email", id='email')),
H3("Relationship to patient"),
Grid(*[LabelCheckboxX(o) for o in relationship], cols=4, cls='space-y-3'),
DivCentered(Button("Submit Form", cls=ButtonT.primary))),
cls='space-y-4')使用 MonsterUI,文本可以自动用 markdown 设置样式。
render_md("""
# My Document
> Important note here
+ List item with **bold**
+ Another with `code`
```python
def hello():
print("world")
```
""")'<div><h1 class="uk-h1 text-4xl font-bold mt-12 mb-6">My Document</h1>\n<blockquote class="uk-blockquote pl-4 border-l-4 border-primary italic mb-6">\n<p class="text-lg leading-relaxed mb-6">Important note here</p>\n</blockquote>\n<ul class="uk-list uk-list-bullet space-y-2 mb-6 ml-6 text-lg">\n<li class="leading-relaxed">List item with <strong>bold</strong></li>\n<li class="leading-relaxed">Another with <code class="uk-codespan px-1">code</code></li>\n</ul>\n<pre class="bg-base-200 rounded-lg p-4 mb-6"><code class="language-python uk-codespan px-1 uk-codespan px-1 block overflow-x-auto">def hello():\n print("world")\n</code></pre>\n</div>'
或者使用语义化 HTML:
def SemanticText():
return Card(
H1("MonsterUI's Semantic Text"),
P(
Strong("MonsterUI"), " brings the power of semantic HTML to life with ",
Em("beautiful styling"), " and ", Mark("zero configuration"), "."),
Blockquote(
P("Write semantic HTML in pure Python, get modern styling for free."),
Cite("MonsterUI Team")),
footer=Small("Released February 2025"),)