from fasthtml.common import *Web 开发人员快速入门
安装
pip install python-fasthtml一个最小的应用
一个最小的 FastHTML 应用看起来像这样
main.py
- 1
- 我们导入了快速开发所需的一切!为方便起见,一组精心挑选的 FastHTML 函数和其他 Python 对象被引入到我们的全局命名空间中。
- 2
-
我们使用
fast_app()工具函数实例化一个 FastHTML 应用。这提供了许多非常有用的默认设置,我们将在教程的后面部分利用这些设置。 - 3
-
我们使用
rt()装饰器告诉 FastHTML 当用户访问浏览器中的/时返回什么。 - 4
-
我们通过定义一个名为
get()的视图函数将此路由连接到 HTTP GET 请求。 - 5
- 一个 Python 函数调用树,它返回编写一个格式正确的网页所需的所有 HTML。你很快就会看到这种方法的强大之处。
- 6
-
serve()工具函数使用一个名为uvicorn的库来配置和运行 FastHTML。
运行代码
python main.py终端将如下所示
INFO: Uvicorn running on http://0.0.0.0:5001 (Press CTRL+C to quit)
INFO: Started reloader process [58058] using WatchFiles
INFO: Started server process [58060]
INFO: Waiting for application startup.
INFO: Application startup complete.通过在浏览器中打开 127.0.0.1:5001 来确认 FastHTML 正在运行。你应该会看到类似下图的画面

虽然一些 linter 和开发者会抱怨通配符导入,但这在这里是特意设计的,并且非常安全。FastHTML 在 fasthtml.common 中导出的对象是经过深思熟虑的。如果这让你感到困扰,你可以单独导入你需要的对象,尽管这会使代码更冗长,可读性更差。
如果你想了解更多关于 FastHTML 如何处理导入的信息,我们在这里有介绍:为什么使用导入。
一个最小的图表应用
Script 函数允许你包含 JavaScript。你可以使用 Python 生成你的 JS 或 JSON 的一部分,就像这样
import json
from fasthtml.common import *
app, rt = fast_app(hdrs=(Script(src="https://cdn.plot.ly/plotly-2.32.0.min.js"),))
data = json.dumps({
"data": [{"x": [1, 2, 3, 4],"type": "scatter"},
{"x": [1, 2, 3, 4],"y": [16, 5, 11, 9],"type": "scatter"}],
"title": "Plotly chart in FastHTML ",
"description": "This is a demo dashboard",
"type": "scatter"
})
@rt("/")
def get():
return Titled("Chart Demo", Div(id="myDiv"),
Script(f"var data = {data}; Plotly.newPlot('myDiv', data);"))
serve()调试模式
当我们无法解决 FastHTML 中的 bug 时,我们可以在 DEBUG 模式下运行它。当抛出错误时,错误屏幕会显示在浏览器中。这个错误设置绝不应该在已部署的应用中使用。
from fasthtml.common import *
1app, rt = fast_app(debug=True)
@rt("/")
def get():
2 1/0
return Titled("FastHTML Error!", P("Let's error!"))
serve()- 1
-
debug=True开启调试模式。 - 2
- 当 Python 尝试用一个整数除以零时,会抛出一个错误。
路由
FastHTML 在 FastAPI 友好的装饰器模式基础上构建,用于指定 URL,并增加了额外的功能
main.py
- 1
- 第 5 行的“/”URL 是项目的主页。这可以通过 127.0.0.1:5001 访问。
- 2
- 如果用户访问 127.0.0.1:5001/hello,项目将找到第 9 行的“/hello”URL。
看起来 get() 被定义了两次,但事实并非如此。每个用 rt 装饰的函数都是完全独立的,并被注入到路由器中。我们不是在模块的命名空间(locals())中调用它们,而是使用 rt 装饰器将它们加载到路由机制中。
你还可以做得更多!继续阅读,了解如何使 URL 的部分内容动态化。
URL 中的变量
你可以通过用 {variable_name} 标记来向 URL 添加可变部分。然后你的函数会接收到 {variable_name} 作为一个关键字参数,但前提是它的类型正确。下面是一个例子
main.py
- 1
-
我们指定了两个变量名,
name和age。 - 2
- 我们定义了两个与变量同名的函数参数。你会注意到我们指定了要传递的 Python 类型。
- 3
- 我们在项目中使用这些函数。
通过访问这个地址来尝试一下:127.0.0.1:5001/uma/5。你应该会得到一个页面,上面写着:
“Hello Uma, age 5”。
如果我们输入了错误的数据会怎样?
127.0.0.1:5001/uma/5 这个 URL 之所以有效,是因为 5 是一个整数。如果我们输入了非整数的内容,例如 127.0.0.1:5001/uma/five,那么 FastHTML 将返回一个错误而不是一个网页。
HTTP 方法
FastHTML 将函数名与 HTTP 方法匹配。到目前为止,我们定义的 URL 路由都是针对 HTTP GET 方法的,这是网页最常用的方法。
表单提交通常以 HTTP POST 方式发送。在处理更动态的网页设计时,也就是所谓的单页应用(SPA),可能会需要其他方法,如 HTTP PUT 和 HTTP DELETE。FastHTML 处理这个问题的方式是改变函数名。
main.py
- 1
-
在第 6 行,因为使用了
get()函数名,这将处理发送到/URI 的 HTTP GET 请求。 - 2
-
在第 10 行,因为使用了
post()函数名,这将处理发送到/URI 的 HTTP POST 请求。
CSS 文件和内联样式
这里我们修改默认的 headers,来演示如何使用 Sakura CSS 微框架,而不是 FastHTML 默认的 Pico CSS。
main.py
from fasthtml.common import *
app, rt = fast_app(
1 pico=False,
hdrs=(
Link(rel='stylesheet', href='assets/normalize.min.css', type='text/css'),
2 Link(rel='stylesheet', href='assets/sakura.css', type='text/css'),
3 Style("p {color: red;}")
))
@app.get("/")
def home():
return Titled("FastHTML",
P("Let's do this!"),
)
serve()- 1
-
通过将
pico设置为False,FastHTML 将不会包含pico.min.css。 - 2
-
这将生成一个 HTML
<link>标签,用于引入 Sakura 的 CSS。 - 3
-
如果你想要内联样式,
Style()函数会把结果放入 HTML 中。
其他静态媒体文件位置
如你所见,Script 和 Link 专用于 Web 应用中最常见的静态媒体用例:包含 JavaScript、CSS 和图片。但它也适用于视频和其他静态媒体文件。默认行为是在根目录中查找这些文件 - 通常我们不需要做任何特殊操作来包含它们。我们可以通过向 fast_app 函数添加 static_path 参数来更改查找文件的默认目录。
app, rt = fast_app(static_path='public')FastHTML 还允许我们定义一个使用 FileResponse 的路由,以在指定路径提供文件。这对于从不同目录提供图片、视频和其他媒体文件非常有用,而无需更改许多文件的路径。因此,如果我们移动了包含媒体文件的目录,我们只需要在一个地方更改路径。在下面的例子中,我们从一个名为 public 的目录中调用图片。
@rt("/{fname:path}.{ext:static}")
async def get(fname:str, ext:str):
return FileResponse(f'public/{fname}.{ext}')渲染 Markdown
from fasthtml.common import *
hdrs = (MarkdownJS(), HighlightJS(langs=['python', 'javascript', 'html', 'css']), )
app, rt = fast_app(hdrs=hdrs)
content = """
Here are some _markdown_ elements.
- This is a list item
- This is another list item
- And this is a third list item
**Fenced code blocks work here.**
"""
@rt('/')
def get(req):
return Titled("Markdown rendering example", Div(content,cls="marked"))
serve()代码高亮
以下是如何在没有任何 markdown 配置的情况下高亮显示代码。
from fasthtml.common import *
# Add the HighlightJS built-in header
hdrs = (HighlightJS(langs=['python', 'javascript', 'html', 'css']),)
app, rt = fast_app(hdrs=hdrs)
code_example = """
import datetime
import time
for i in range(10):
print(f"{datetime.datetime.now()}")
time.sleep(1)
"""
@rt('/')
def get(req):
return Titled("Markdown rendering example",
Div(
# The code example needs to be surrounded by
# Pre & Code elements
Pre(Code(code_example))
))
serve()定义新的 ft 组件
我们可以构建自己的 ft 组件,并将其与其他组件组合。最简单的方法是将其定义为一个函数。
def hero(title, statement):
return Div(H1(title),P(statement), cls="hero")
# usage example
Main(
hero("Hello World", "This is a hero statement")
)<main> <div class="hero">
<h1>Hello World</h1>
<p>This is a hero statement</p>
</div>
</main>透传组件
当我们想定义一个新组件,该组件允许嵌套零到多个组件时,我们可以借助 Python 的 *args 和 **kwargs 机制。这对于创建页面布局控件很有用。
def layout(*args, **kwargs):
"""Dashboard layout for all our dashboard views"""
return Main(
H1("Dashboard"),
Div(*args, **kwargs),
cls="dashboard",
)
# usage example
layout(
Ul(*[Li(o) for o in range(3)]),
P("Some content", cls="description"),
)<main class="dashboard"> <h1>Dashboard</h1>
<div>
<ul>
<li>0</li>
<li>1</li>
<li>2</li>
</ul>
<p class="description">Some content</p>
</div>
</main>作为 ft 组件的数据类
虽然函数易于阅读,但对于更复杂的组件,有些人可能会发现使用数据类(dataclass)更容易。
from dataclasses import dataclass
@dataclass
class Hero:
title: str
statement: str
def __ft__(self):
""" The __ft__ method renders the dataclass at runtime."""
return Div(H1(self.title),P(self.statement), cls="hero")
# usage example
Main(
Hero("Hello World", "This is a hero statement")
)<main> <div class="hero">
<h1>Hello World</h1>
<p>This is a hero statement</p>
</div>
</main>在 notebook 中测试视图
由于 ASGI 事件循环,目前无法在 notebook 中运行 FastHTML。但是,我们仍然可以测试视图的输出。为此,我们利用了 Starlette,这是一个 FastHTML 使用的 ASGI 工具包。
# First we instantiate our app, in this case we remove the
# default headers to reduce the size of the output.
app, rt = fast_app(default_hdrs=False)
# Setting up the Starlette test client
from starlette.testclient import TestClient
client = TestClient(app)
# Usage example
@rt("/")
def get():
return Titled("FastHTML is awesome",
P("The fastest way to create web apps in Python"))
print(client.get("/").text) <!doctype html>
<html>
<head>
<title>FastHTML is awesome</title> </head>
<body>
<main class="container"> <h1>FastHTML is awesome</h1>
<p>The fastest way to create web apps in Python</p>
</main> </body>
</html>
表单
要验证来自用户的数据,首先定义一个数据类(dataclass)来表示你想要检查的数据。这里有一个表示注册表单的例子。
from dataclasses import dataclass
@dataclass
class Profile: email:str; phone:str; age:int创建一个 FT 组件来表示该表单的空版本。不要传入任何值来填充表单,这会在后面处理。
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"),
)
profile_form<form enctype="multipart/form-data" method="post" action="/profile"><fieldset><label>Email <input name="email">
</label><label>Phone <input name="phone">
</label><label>Age <input name="age">
</label></fieldset><button type="submit">Save</button></form>一旦数据类和表单函数完成,我们就可以向表单添加数据。为此,实例化 profile 数据类
profile = Profile(email='[email protected]', phone='123456789', age=5)
profileProfile(email='[email protected]', phone='123456789', age=5)
然后使用 FastHTML 的 fill_form 类将该数据添加到 profile_form 中
fill_form(profile_form, profile)<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>带视图的表单
当 FastHTML 表单与 FastHTML 视图结合使用时,它们的用处就更加明显了。我们将使用上面的测试客户端来展示这是如何工作的。首先,让我们创建一个 SQlite 数据库
db = database("profiles.db")
profiles = db.create(Profile, pk="email")现在我们向数据库中插入一条记录
profiles.insert(profile)Profile(email='[email protected]', phone='123456789', age=5)
然后我们可以在代码中演示表单被填充并显示给用户。
@rt("/profile/{email}")
def profile(email:str):
1 profile = profiles[email]
2 filled_profile_form = fill_form(profile_form, profile)
return Titled(f'Profile for {profile.email}', filled_profile_form)
print(client.get(f"/profile/[email protected]").text)- 1
-
使用 profile 表的
email主键获取个人资料 - 2
- 填充表单以供显示。
<!doctype html>
<html>
<head>
<title>Profile for [email protected]</title> </head>
<body>
<main class="container"> <h1>Profile for [email protected]</h1>
<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></main> </body>
</html>
现在让我们来演示如何对数据进行更改。
@rt("/profile")
1def post(profile: Profile):
2 profiles.update(profile)
3 return RedirectResponse(url=f"/profile/{profile.email}")
new_data = dict(email='[email protected]', phone='7654321', age=25)
4print(client.post("/profile", data=new_data).text)- 1
-
我们使用
Profile数据类定义来设置传入profile内容的类型。这会验证传入数据的字段类型 - 2
- 利用我们验证过的数据,我们更新了 profiles 表
- 3
- 我们将用户重定向回他们的个人资料视图
- 4
- 显示的是个人资料表单视图,其中显示了数据的变化。
<!doctype html>
<html>
<head>
<title>Profile for [email protected]</title> </head>
<body>
<main class="container"> <h1>Profile for [email protected]</h1>
<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></main> </body>
</html>
字符串和转换顺序
渲染的一般规则是:- 将调用 __ft__ 方法(对于默认组件如 P、H2 等,或者如果你定义了自己的组件)- 如果你传递一个字符串,它将被转义 - 对于其他 Python 对象,将调用 str()
因此,如果你想直接将纯 HTML 标签包含在例如一个 Div() 中,它们默认会被转义(作为一种避免代码注入的安全措施)。这可以通过使用 NotStr() 来避免,这是一种重用返回已是 HTML 的 Python 代码的便捷方式。如果你使用 pandas,你可以使用 pandas.DataFrame.to_html() 来获得一个漂亮的表格。要包含 FastHTML 的输出,请将其包装在 NotStr() 中,例如 Div(NotStr(df.to_html()))。
上面我们看到了一个定义了 __ft__ 方法的数据类的行为。对于一个普通的数据类,str() 将被调用(但不会被转义)。
from dataclasses import dataclass
@dataclass
class Hero:
title: str
statement: str
# rendering the dataclass with the default method
Main(
Hero("<h1>Hello World</h1>", "This is a hero statement")
)<main>Hero(title='<h1>Hello World</h1>', statement='This is a hero statement')</main># This will display the HTML as text on your page
Div("Let's include some HTML here: <div>Some HTML</div>")<div>Let's include some HTML here: <div>Some HTML</div></div># Keep the string untouched, will be rendered on the page
Div(NotStr("<div><h1>Some HTML</h1></div>"))<div><div><h1>Some HTML</h1></div></div>自定义异常处理器
FastHTML 允许自定义异常处理器,但方式很优雅。这意味着它默认会包含所有显示美观内容所需的 <html> 标签。试试看!
from fasthtml.common import *
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)
@rt('/')
def get():
return (Titled("Home page", P(A(href="/oops")("Click to generate 404 error"))))
serve()我们也可以使用 lambda 来使代码更简洁
from fasthtml.common import *
exception_handlers={
404: lambda req, exc: Titled("404: I don't exist!"),
418: lambda req, exc: Titled("418: I'm a teapot!")
}
app, rt = fast_app(exception_handlers=exception_handlers)
@rt('/')
def get():
return (Titled("Home page", P(A(href="/oops")("Click to generate 404 error"))))
serve()Session
为方便和安全起见,FastHTML 有一种机制可以在用户浏览器中存储少量数据。我们可以通过向路由添加 session 参数来实现这一点。FastHTML 的 session 是 Python 字典,我们可以加以利用。下面的例子展示了如何简洁地设置和获取 session。
@rt('/adder/{num}')
def get(session, num: int):
session.setdefault('sum', 0)
session['sum'] = session.get('sum') + num
return Response(f'The sum is {session["sum"]}.')Toast(也称为消息)
Toast,有时也称为“消息”,是通常出现在彩色框中的小通知,用于通知用户发生了某事。Toast 可以有四种类型
- 信息
- 成功
- 警告
- 错误
Toast 的例子可能包括
- “支付已接受”
- “数据已提交”
- “请求已批准”
Toast 需要使用 setup_toasts() 函数,并且每个视图都需要具备这两个特性
- session 参数
- 必须返回 FT 组件
1setup_toasts(app)
@rt('/toasting')
2def get(session):
# Normally one toast is enough, this allows us to see
# different toast types in action.
add_toast(session, f"Toast is being cooked", "info")
add_toast(session, f"Toast is ready", "success")
add_toast(session, f"Toast is getting a bit crispy", "warning")
add_toast(session, f"Toast is burning!", "error")
3 return Titled("I like toast")- 1
-
setup_toasts是一个添加 toast 依赖的辅助函数。通常这会在fast_app()之后立即声明 - 2
- Toast 需要 session
- 3
- 带 Toast 的视图必须返回 FT 或 FtResponse 组件。
💡 setup_toasts 接受一个 duration 输入,允许你指定 toast 在消失前可见多长时间。例如 setup_toasts(duration=5) 将 toast 的持续时间设置为 5 秒。默认情况下,toast 在 10 秒后消失。
⚠️ Toast 不适用于替换整个 body 的 SPA 式导航,例如这个导航触发器 A('About', hx_get="/about", hx_swap="outerHTML", hx_push_url="true", hx_target="body")。作为替代方案,将你的路由内容包装在一个包含 id 的元素中,并将此 id 设置为导航触发器的目标(即 hx_target='#container_id')。
服务器发送事件 (SSE)
通过服务器发送事件,服务器可以随时向网页发送新数据,通过向网页推送消息。与 WebSockets 不同,SSE 只能单向传输:从服务器到客户端。SSE 也是 HTTP 规范的一部分,而 WebSockets 则使用自己的规范。
FastHTML 引入了几个用于处理 SSE 的工具,下面的例子中有所介绍。虽然简洁,但这个函数中有很多内容,所以我们做了很多注释。
import random
from asyncio import sleep
from fasthtml.common import *
1hdrs=(Script(src="https://unpkg.com/[email protected]/sse.js"),)
app,rt = fast_app(hdrs=hdrs)
@rt
def index():
return Titled("SSE Random Number Generator",
P("Generate pairs of random numbers, as the list grows scroll downwards."),
2 Div(hx_ext="sse",
3 sse_connect="/number-stream",
4 hx_swap="beforeend show:bottom",
5 sse_swap="message"))
6shutdown_event = signal_shutdown()
7async def number_generator():
8 while not shutdown_event.is_set():
data = Article(random.randint(1, 100))
9 yield sse_message(data)
await sleep(1)
@rt("/number-stream")
10async def get(): return EventStream(number_generator())- 1
- 导入 HTMX SSE 扩展
- 2
- 告诉 HTMX 加载 SSE 扩展
- 3
-
在
/number-stream端点查找 SSE 内容 - 4
- 当新项目从 SSE 端点传来时,将它们添加到 div 内当前内容的末尾。如果超出屏幕,则向下滚动
- 5
- 指定事件的名称。FastHTML 的默认事件名称是“message”。仅当你在一个视图中有多个对 SSE 端点的调用时才需要更改
- 6
- 设置 asyncio 事件循环
- 7
-
别忘了把这个函数设为
async! - 8
- 遍历 asyncio 事件循环
- 9
- 我们 yield 数据。数据最好由 FT 组件组成,因为这样可以很好地与浏览器中的 HTMX 对接
- 10
-
端点视图需要是一个返回
EventStream的异步函数
Websocket
通过 websocket,我们可以在浏览器和客户端之间进行双向通信。Websocket 对于聊天和某些类型的游戏等场景很有用。虽然 websocket 可以用于从服务器发送单向消息(例如,告诉用户某个进程已完成),但该任务可能更适合使用 SSE。
FastHTML 提供了有用的工具,可将 websocket 添加到您的页面中。
from fasthtml.common import *
from asyncio import sleep
1app, rt = fast_app(exts='ws')
2def mk_inp(): return Input(id='msg', autofocus=True)
@rt('/')
async def get(request):
cts = Div(
Div(id='notifications'),
3 Form(mk_inp(), id='form', ws_send=True),
4 hx_ext='ws', ws_connect='/ws')
return Titled('Websocket Test', cts)
5async def on_connect(send):
print('Connected!')
6 await send(Div('Hello, you have connected', id="notifications"))
7async def on_disconnect(ws):
print('Disconnected!')
8@app.ws('/ws', conn=on_connect, disconn=on_disconnect)
9async def ws(msg:str, send):
10 await send(Div('Hello ' + msg, id="notifications"))
await sleep(2)
11 return Div('Goodbye ' + msg, id="notifications"), mk_inp()- 1
-
要在 FastHTML 中使用 websocket,您必须在实例化应用时将
exts设置为 ‘ws’ - 2
-
因为我们想用 websocket 来重置表单,所以我们定义了
mk_input函数,以便可以从多个位置调用 - 3
-
我们创建表单并用
ws_send属性标记它,该属性在 HTMX websocket 规范 中有文档。这告诉 HTMX 根据表单元素的触发器向最近的 websocket 发送消息,对于表单来说,触发器是按下enter键,这个操作被视为表单提交 - 4
-
这里是加载 HTMX 扩展(
hx_ext='ws')和定义最近的 websocket(ws_connect='/ws')的地方 - 5
-
当 websocket 首次连接时,我们可以选择性地让它调用一个接受
send参数的函数。send参数会向浏览器推送一条消息。 - 6
-
这里我们使用传递给
on_connect函数的send函数来发送一个带有id为notifications的Div,HTMX 会将它分配给页面上已经有id为notifications的元素 - 7
- 当 websocket 断开连接时,我们可以调用一个不带任何参数的函数。通常这个函数的作用是通知服务器采取某个行动。在这种情况下,我们向控制台打印一条简单的消息
- 8
-
我们使用
app.ws装饰器来标记/ws是我们 websocket 的路由。我们还向这个装饰器传递了两个可选的conn和disconn参数。作为一个有趣的实验,可以移除conn和disconn参数,看看会发生什么 - 9
-
将
ws函数定义为异步函数。这对于 ASGI 能够提供 websocket 服务是必需的。该函数接受两个参数,一个是从浏览器传来的用户输入msg,以及一个用于向浏览器推送数据的send函数 - 10
-
这里的
send函数用于将 HTML 发送回页面。由于该 HTML 的id是notifications,HTMX 将会用它覆盖页面上具有相同 ID 的内容 - 11
-
websocket 函数也可以用来返回值。在这种情况下,它是一个包含两个 HTML 元素的元组。HTMX 会获取这些元素并在适当的位置替换它们。由于两者都指定了
id(分别为notifications和msg),它们将替换页面上它们的前辈。
文件上传
Web 开发中的一个常见任务是上传文件。下面的示例是关于将文件上传到托管服务器,并将有关上传文件的信息呈现给用户。
文件上传可能成为滥用(无论是意外还是故意)的目标。这意味着用户可能会尝试上传过大或存在安全风险的文件。这对于面向公众的应用尤其值得关注。文件上传安全超出了本教程的范围,目前我们建议阅读 OWASP 文件上传备忘单。
单文件上传
from fasthtml.common import *
from pathlib import Path
app, rt = fast_app()
upload_dir = Path("filez")
upload_dir.mkdir(exist_ok=True)
@rt('/')
def get():
return Titled("File Upload Demo",
Article(
1 Form(hx_post=upload, hx_target="#result-one")(
2 Input(type="file", name="file"),
Button("Upload", type="submit", cls='secondary'),
),
Div(id="result-one")
)
)
def FileMetaDataCard(file):
return Article(
Header(H3(file.filename)),
Ul(
Li('Size: ', file.size),
Li('Content Type: ', file.content_type),
Li('Headers: ', file.headers),
)
)
@rt
3async def upload(file: UploadFile):
4 card = FileMetaDataCard(file)
5 filebuffer = await file.read()
6 (upload_dir / file.filename).write_bytes(filebuffer)
return card
serve()- 1
-
每个用
FormFT 组件渲染的表单都默认为enctype="multipart/form-data" - 2
-
别忘了将
InputFT 组件的类型设置为file - 3
- 上传视图应该接收一个 Starlette UploadFile 类型。你可以添加其他表单变量
- 4
- 我们可以访问卡的元数据(文件名、大小、content_type、headers),这是一个快速且安全的过程。我们将其设置为 card 变量
- 5
-
为了访问文件中包含的内容,我们使用
await方法来 read() 它。由于文件可能很大或包含不良数据,这是一个与访问元数据分开的步骤 - 6
-
这一步展示了如何使用 Python 内置的
pathlib.Path库将文件写入磁盘。
多文件上传
from fasthtml.common import *
from pathlib import Path
app, rt = fast_app()
upload_dir = Path("filez")
upload_dir.mkdir(exist_ok=True)
@rt('/')
def get():
return Titled("Multiple File Upload Demo",
Article(
1 Form(hx_post=upload_many, hx_target="#result-many")(
2 Input(type="file", name="files", multiple=True),
Button("Upload", type="submit", cls='secondary'),
),
Div(id="result-many")
)
)
def FileMetaDataCard(file):
return Article(
Header(H3(file.filename)),
Ul(
Li('Size: ', file.size),
Li('Content Type: ', file.content_type),
Li('Headers: ', file.headers),
)
)
@rt
3async def upload_many(files: list[UploadFile]):
cards = []
4 for file in files:
5 cards.append(FileMetaDataCard(file))
6 filebuffer = await file.read()
7 (upload_dir / file.filename).write_bytes(filebuffer)
return cards
serve()- 1
-
每个用
FormFT 组件渲染的表单都默认为enctype="multipart/form-data" - 2
-
别忘了将
InputFT 组件的类型设置为file,并将 multiple 属性设为True - 3
-
上传视图应该接收一个包含 Starlette UploadFile 类型的
list。你可以添加其他表单变量 - 4
- 遍历文件
- 5
- 我们可以访问卡的元数据(文件名、大小、content_type、headers),这是一个快速且安全的过程。我们将其添加到 cards 变量中
- 6
-
为了访问文件中包含的内容,我们使用
await方法来 read() 它。由于文件可能很大或包含不良数据,这是一个与访问元数据分开的步骤 - 7
-
这一步展示了如何使用 Python 内置的
pathlib.Path库将文件写入磁盘。