网页开发人员快速入门

为有经验的网页开发人员提供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.

通过打开您的web浏览器到 127.0.0.1:5001 来确认FastHTML正在运行。您应该看到类似下面的图像:

注意

虽然一些代码检查工具和开发者会抱怨通配符导入,但在这里这是有意为之且完全安全的。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中找到一个错误时,可以在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。你应该会看到一个页面,上面写着,

“你好,Uma,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
因为使用了post()函数名称,所以在第10行,这将处理发送到/ URI的HTTP POST请求。

CSS 文件和内联样式

在这里,我们修改默认的头部,以演示如何使用 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 标签,用于引入Sakura的css。
3
如果您想要内联样式,Style() 函数将结果放入HTML中。

其他静态媒体文件位置

正如您所看到的, ScriptLink 是Web应用中最常见的静态媒体用例的特定格式:包括JavaScript、CSS和图像。但它也适用于视频和其他静态媒体文件。默认行为是在根目录中查找这些文件 - 通常我们不会做特别的事情来包含它们。我们可以通过将 static_path 参数添加到 fast_app 函数来更改查找文件的默认目录。

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>

数据类作为功能组件

虽然函数易于阅读,但对于更复杂的组件,有些人可能会发现使用数据类更简单。

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>

在笔记本中测试视图

由于ASGI事件循环,目前无法在笔记本中运行FastHTML。然而,我们仍然可以测试视图的输出。为此,我们利用FastHTML使用的ASGI工具包Starlette。

# 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>

表单

为了验证来自用户的数据,首先定义一个数据类来表示您想要检查的数据。以下是表示注册表单的示例。

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(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
使用个人资料表的 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
使用我们经过验证的数据,我们更新了配置文件表
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允许自定义异常处理程序,但这样做非常优雅。这意味着默认情况下,它包括所有的 标签,以显示吸引人的内容。试试吧!

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()

.cookies

我们可以使用 cookie() 函数设置 cookies。在我们的示例中,我们将创建一个 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 page

设置为 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'

会话

为了方便和安全,FastHTML 具有一个将少量数据存储在用户浏览器中的机制。我们可以通过在路由中添加一个 session 参数来实现这一点。FastHTML 会话是 Python 字典,我们可以利用这一点。下面的示例展示了如何简洁地设置和获取会话。

@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"]}.')

吐司(也称为消息)

吐司,有时称为“消息”,是通常用于通知用户某些事件发生的小通知,通常以彩色框的形式展示。吐司可以分为四种类型:

  • 信息
  • 成功
  • 警告
  • 错误

示例的吐司可能包括:

  • “支付已接受”
  • “数据已提交”
  • “请求已批准”

吐司需要使用 setup_toasts() 函数,并且每个视图需要这两个功能:

  • 会话参数
  • 必须返回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 是一个辅助函数,用于添加吐司依赖项。通常这会在 fast_app() 之后声明。
2
吐司需要会话
3
使用 Toasts 的视图必须返回 FT 或 FtResponse 组件。

💡 setup_toasts 接受一个 duration 输入,允许您指定吐司显示多长时间后消失。例如 setup_toasts(duration=5) 将吐司的持续时间设置为5秒。默认情况下,吐司在10秒后消失。

身份验证与授权

在FastHTML中,身份验证和授权的任务由Beforeware处理。Beforeware是指在路由处理程序被调用之前运行的函数。它们对于确保用户已通过身份验证或具有访问视图的权限等全局任务非常有用。

首先,我们编写一个接受请求和会话参数的函数:

# 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端点进入时,将它们添加到当前内容的末尾。如果它们超出屏幕,请向下滚动
5
指定事件的名称。FastHTML的默认事件名称是“message”。只有在视图中有多个对SSE端点的调用时才会更改。
6
设置 asyncio 事件循环
7
不要忘记将这变成一个 async 函数!
8
遍历 asyncio 事件循环
9
我们生成数据。理想情况下,数据应该由FT组件组成,因为这可以很好地插入浏览器中的HTMX
10
端点视图需要是一个异步函数,返回一个 EventStream

网络套接字

通过websockets,我们可以在浏览器和客户端之间实现双向通信。websockets在聊天和某些类型的游戏中非常有用。虽然websockets可以用于从服务器发送单向消息(即,通知用户某个过程已完成),但这项任务可以说更适合使用SSE。

FastHTML 提供了有用的工具,可以将 websockets 添加到您的页面。

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

文件上传

在网页开发中,一个常见的任务是上传文件。下面的示例是将文件上传到托管服务器,上传的文件信息会呈现给用户。

生产环境中的文件上传可能很危险

文件上传可能成为滥用的目标,无论是无意还是故意的。这意味着用户可能会尝试上传过大或存在安全风险的文件。这对于公共应用程序尤其令人担忧。文件上传安全超出了本教程的范围,目前我们建议阅读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
我们可以访问卡片的元数据(文件名、大小、内容类型、头部),这是一种快速且安全的过程。我们将其设置为卡片变量
5
为了访问文件中包含的内容,我们使用await方法来读取它。由于文件可能相当大或包含坏数据,这与访问元数据是一个单独的步骤。
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
上传视图应该接收一个 list,包含 Starlette UploadFile 类型。你可以添加其他表单变量
4
遍历文件
5
我们可以访问卡片的元数据(文件名、大小、内容类型、头信息),这是一个快速且安全的过程。我们将其添加到卡片变量中
6
为了访问文件中的内容,我们使用await方法来读取它。由于文件可能相当大或包含错误数据,因此这一步骤与访问元数据是分开的
7
这一步展示了如何使用Python内置的 pathlib.Path 库将文件写入磁盘。