Web 开发人员快速入门

为有经验的 Web 开发人员快速介绍 FastHTML。

安装

pip install python-fasthtml

一个最小的应用

一个最小的 FastHTML 应用看起来像这样

main.py
1from fasthtml.common import *

2app, rt = fast_app()

3@rt("/")
4def get():
5    return Titled("FastHTML", P("Let's do this!"))

6serve()
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
from fasthtml.common import * 

app, rt = fast_app()

1@rt("/")
def get():
  return Titled("FastHTML", P("Let's do this!"))

2@rt("/hello")
def get():
  return Titled("Hello, world!")

serve()
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
from fasthtml.common import * 

app, rt = fast_app()

1@rt("/{name}/{age}")
2def get(name: str, age: int):
3  return Titled(f"Hello {name.title()}, age {age}")

serve()
1
我们指定了两个变量名,nameage
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 将返回一个错误而不是一个网页。

FastHTML URL 路由支持更复杂的类型

我们在这里提供的两个例子使用了 Python 内置的 strint 类型,但你可以使用自己的类型,包括更复杂的类型,例如由 attrspydantic 甚至 sqlmodel 等库定义的类型。

HTTP 方法

FastHTML 将函数名与 HTTP 方法匹配。到目前为止,我们定义的 URL 路由都是针对 HTTP GET 方法的,这是网页最常用的方法。

表单提交通常以 HTTP POST 方式发送。在处理更动态的网页设计时,也就是所谓的单页应用(SPA),可能会需要其他方法,如 HTTP PUT 和 HTTP DELETE。FastHTML 处理这个问题的方式是改变函数名。

main.py
from fasthtml.common import * 

app, rt = fast_app()

@rt("/")  
1def get():
  return Titled("HTTP GET", P("Handle GET"))

@rt("/")  
2def post():
  return Titled("HTTP POST", P("Handle POST"))

serve()
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 中。

其他静态媒体文件位置

如你所见,ScriptLink 专用于 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 组件,并将其与其他组件组合。最简单的方法是将其定义为一个函数。

from fasthtml.common import *
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)
profile
Profile(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__ 方法(对于默认组件如 PH2 等,或者如果你定义了自己的组件)- 如果你传递一个字符串,它将被转义 - 对于其他 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&#x27;s include some HTML here: &lt;div&gt;Some HTML&lt;/div&gt;</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()

Cookie

我们可以使用 cookie() 函数来设置 cookie。在我们的例子中,我们将创建一个 timestamp cookie。

from datetime import datetime
from IPython.display import HTML
@rt("/settimestamp")
def get(req):
    now = datetime.now()
    return P(f'Set to {now}'), cookie('now', datetime.now())

HTML(client.get('/settimestamp').text)
FastHTML 页面 - FastHTML 框架

设置为 2024-09-26 15:33:48.141869

现在让我们用与 cookie 名称相同的参数名来取回它。

@rt('/gettimestamp')
def get(now:parsed_date): return f'Cookie was set at time {now.time()}'

client.get('/gettimestamp').text
'Cookie was set at time 15:33:48.141903'

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')。

认证和授权

在 FastHTML 中,认证和授权的任务由 Beforeware 处理。Beforeware 是在路由处理器被调用之前运行的函数。它们对于全局任务很有用,比如确保用户已认证或有权访问某个视图。

首先,我们编写一个接受 request 和 session 参数的函数

# Status code 303 is a redirect that can change POST to GET,
# so it's appropriate for a login page.
login_redir = RedirectResponse('/login', status_code=303)

def user_auth_before(req, sess):
    # The `auth` key in the request scope is automatically provided
    # to any handler which requests it, and can not be injected
    # by the user using query params, cookies, etc, so it should
    # be secure to use.    
    auth = req.scope['auth'] = sess.get('auth', None)
    # If the session key is not there, it redirects to the login page.
    if not auth: return login_redir

现在我们将我们的 user_auth_before 函数作为第一个参数传递给 Beforeware 类。我们还向 skip 参数传递一个正则表达式列表,旨在允许用户仍然可以访问主页和登录页面。

beforeware = Beforeware(
    user_auth_before,
    skip=[r'/favicon\.ico', r'/static/.*', r'.*\.css', r'.*\.js', '/login', '/']
)

app, rt = fast_app(before=beforeware)

服务器发送事件 (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 函数来发送一个带有 idnotificationsDiv,HTMX 会将它分配给页面上已经有 idnotifications 的元素
7
当 websocket 断开连接时,我们可以调用一个不带任何参数的函数。通常这个函数的作用是通知服务器采取某个行动。在这种情况下,我们向控制台打印一条简单的消息
8
我们使用 app.ws 装饰器来标记 /ws 是我们 websocket 的路由。我们还向这个装饰器传递了两个可选的 conndisconn 参数。作为一个有趣的实验,可以移除 conndisconn 参数,看看会发生什么
9
ws 函数定义为异步函数。这对于 ASGI 能够提供 websocket 服务是必需的。该函数接受两个参数,一个是从浏览器传来的用户输入 msg,以及一个用于向浏览器推送数据的 send 函数
10
这里的 send 函数用于将 HTML 发送回页面。由于该 HTML 的 idnotifications,HTMX 将会用它覆盖页面上具有相同 ID 的内容
11
websocket 函数也可以用来返回值。在这种情况下,它是一个包含两个 HTML 元素的元组。HTMX 会获取这些元素并在适当的位置替换它们。由于两者都指定了 id(分别为 notificationsmsg),它们将替换页面上它们的前辈。

文件上传

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
每个用 Form FT 组件渲染的表单都默认为 enctype="multipart/form-data"
2
别忘了将 Input FT 组件的类型设置为 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
每个用 Form FT 组件渲染的表单都默认为 enctype="multipart/form-data"
2
别忘了将 Input FT 组件的类型设置为 file,并将 multiple 属性设为 True
3
上传视图应该接收一个包含 Starlette UploadFile 类型的 list。你可以添加其他表单变量
4
遍历文件
5
我们可以访问卡的元数据(文件名、大小、content_type、headers),这是一个快速且安全的过程。我们将其添加到 cards 变量中
6
为了访问文件中包含的内容,我们使用 await 方法来 read() 它。由于文件可能很大或包含不良数据,这是一个与访问元数据分开的步骤
7
这一步展示了如何使用 Python 内置的 pathlib.Path 库将文件写入磁盘。