JS 应用详解

如何使用 FastHTML 和自定义 JavaScript 一步步构建网站

安装

你需要以下软件来完成本教程,请继续阅读具体的安装说明

  1. Python
  2. Python 包管理器,例如 pip(通常随 Python 一起安装)或 uv
  3. FastHTML
  4. 网页浏览器
  5. Railway.app 账户

如果你以前没有接触过 Python,我们建议从 Miniconda 开始。

请注意,每个环境中你只需要执行一次安装部分的步骤。如果创建了一个新的仓库,你不需要重新进行这些操作。

安装 FastHTML

对于 Mac、Windows 和 Linux,请输入

pip install python-fasthtml

第一步

在本节结束时,你将拥有自己的 FastHTML 网站,并带有测试,部署在 railway.app 上。

创建一个 hello world

创建一个新文件夹来组织你项目的所有文件。在此文件夹内,创建一个名为 main.py 的文件,并将以下代码添加到其中

main.py
from fasthtml.common import *

app = FastHTML()
rt = app.route

@rt('/')
def get():
    return 'Hello, world!'

serve()

最后,在你的终端中运行 python main.py,然后在浏览器中打开出现的“链接”。

QuickDraw:一次 FastHTML 探险 🎨✨

本教程的最终成果将是 QuickDraw,一个使用 FastHTML 的实时协作绘图应用。最终网站的外观如下

QuickDraw

绘图室

绘图室是我们应用的核心概念。每个房间代表一个独立的绘图空间,用户可以在其中尽情挥洒他们内心的毕加索。以下是详细分解

  1. 房间的创建与存储
main.py
db = database('data/drawapp.db')
rooms = db.t.rooms
if rooms not in db.t:
    rooms.create(id=int, name=str, created_at=str, pk='id')
Room = rooms.dataclass()

@patch
def __ft__(self:Room):
    return Li(A(self.name, href=f"/rooms/{self.id}"))

或者你可以使用我们的 fast_app 函数,用一行代码创建一个带有 SQLite 数据库和 dataclass 的 FastHTML 应用

main.py
def render(room):
    return Li(A(room.name, href=f"/rooms/{room.id}"))

app,rt,rooms,Room = fast_app('data/drawapp.db', render=render, id=int, name=str, created_at=str, pk='id')

我们正在指定一个渲染函数,将我们的 dataclass 转换为 HTML,这与我们之前使用的 patch 装饰器扩展 __ft__ 方法是相同的。在本教程的剩余部分,我们将使用这种方法,因为它更简洁、更易于阅读。

  • 我们使用 SQLite 数据库(通过 FastLite)来存储我们的房间。
  • 每个房间都有一个 id(整数)、一个 name(字符串)和一个 created_at 时间戳(字符串)。
  • Room dataclass 会根据这个结构自动生成。
  1. 创建一个房间
main.py
@rt("/")
def get():
    # The 'Input' id defaults to the same as the name, so you can omit it if you wish
    create_room = Form(Input(id="name", name="name", placeholder="New Room Name"),
                       Button("Create Room"),
                       hx_post="/rooms", hx_target="#rooms-list", hx_swap="afterbegin")
    rooms_list = Ul(*rooms(order_by='id DESC'), id='rooms-list')
    return Titled("DrawCollab", 
                  H1("DrawCollab"),
                  create_room, rooms_list)

@rt("/rooms")
async def post(room:Room):
    room.created_at = datetime.now().isoformat()
    return rooms.insert(room)
  • 当用户提交“创建房间”表单时,此路由被调用。
  • 它会创建一个新的 Room 对象,设置创建时间,并将其插入数据库。
  • 它返回一个带有新房间链接的 HTML 列表项,借助 HTMX,该列表项会动态添加到主页的房间列表中。
  1. 让我们为房间赋予形态
main.py
@rt("/rooms/{id}")
async def get(id:int):
    room = rooms[id]
    return Titled(f"Room: {room.name}", H1(f"Welcome to {room.name}"), A(Button("Leave Room"), href="/"))
  • 此路由渲染特定房间的界面。
  • 它从数据库中获取房间信息,并渲染一个标题、一个标题和一个段落。

以下是到目前为止的完整代码

main.py
from fasthtml.common import *
from datetime import datetime

def render(room):
    return Li(A(room.name, href=f"/rooms/{room.id}"))

app,rt,rooms,Room = fast_app('data/drawapp.db', render=render, id=int, name=str, created_at=str, pk='id')

@rt("/")
def get():
    create_room = Form(Input(id="name", name="name", placeholder="New Room Name"),
                       Button("Create Room"),
                       hx_post="/rooms", hx_target="#rooms-list", hx_swap="afterbegin")
    rooms_list = Ul(*rooms(order_by='id DESC'), id='rooms-list')
    return Titled("DrawCollab", create_room, rooms_list)

@rt("/rooms")
async def post(room:Room):
    room.created_at = datetime.now().isoformat()
    return rooms.insert(room)

@rt("/rooms/{id}")
async def get(id:int):
    room = rooms[id]
    return Titled(f"Room: {room.name}", H1(f"Welcome to {room.name}"), A(Button("Leave Room"), href="/"))

serve()

现在在你的终端中运行 python main.py,然后在浏览器中打开出现的“链接”。你应该会看到一个页面,上面有一个创建新房间的表单和现有房间的列表。

画布 - 让我们开始绘画吧!🖌️

是时候添加实际的绘图功能了。我们将为此使用 Fabric.js

main.py
# ... (keep the previous imports and database setup)

@rt("/rooms/{id}")
async def get(id:int):
    room = rooms[id]
    canvas = Canvas(id="canvas", width="800", height="600")
    color_picker = Input(type="color", id="color-picker", value="#3CDD8C")
    brush_size = Input(type="range", id="brush-size", min="1", max="50", value="10")
    
    js = """
    var canvas = new fabric.Canvas('canvas');
    canvas.isDrawingMode = true;
    canvas.freeDrawingBrush.color = '#3CDD8C';
    canvas.freeDrawingBrush.width = 10;
    
    document.getElementById('color-picker').onchange = function() {
        canvas.freeDrawingBrush.color = this.value;
    };
    
    document.getElementById('brush-size').oninput = function() {
        canvas.freeDrawingBrush.width = parseInt(this.value, 10);
    };
    """
    
    return Titled(f"Room: {room.name}",
                  A(Button("Leave Room"), href="/"),
                  canvas,
                  Div(color_picker, brush_size),
                  Script(src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js"),
                  Script(js))

# ... (keep the serve() part)

现在我们有了一个绘图画布!FastHTML 使得包含外部库和添加自定义 JavaScript 变得容易。

保存和加载画布 💾

现在我们有了一个可用的绘图画布,让我们添加保存和加载绘图的功能。我们将修改数据库模式以包含一个 canvas_data 字段,并添加新的路由来保存和加载画布数据。以下是我们更新代码的方式

  1. 修改数据库模式
main.py
app,rt,rooms,Room = fast_app('data/drawapp.db', render=render, id=int, name=str, created_at=str, canvas_data=str, pk='id')
  1. 添加一个保存按钮,获取画布状态并将其发送到服务器
main.py
@rt("/rooms/{id}")
async def get(id:int):
    room = rooms[id]
    canvas = Canvas(id="canvas", width="800", height="600")
    color_picker = Input(type="color", id="color-picker", value="#3CDD8C")
    brush_size = Input(type="range", id="brush-size", min="1", max="50", value="10")
    save_button = Button("Save Canvas", id="save-canvas", hx_post=f"/rooms/{id}/save", hx_vals="js:{canvas_data: JSON.stringify(canvas.toJSON())}")
    # ... (rest of the function remains the same)
  1. 添加用于保存和加载画布数据的路由
main.py
@rt("/rooms/{id}/save")
async def post(id:int, canvas_data:str):
    rooms.update({'canvas_data': canvas_data}, id)
    return "Canvas saved successfully"

@rt("/rooms/{id}/load")
async def get(id:int):
    room = rooms[id]
    return room.canvas_data if room.canvas_data else "{}"
  1. 更新 JavaScript 以加载现有的画布数据
main.py
js = f"""
    var canvas = new fabric.Canvas('canvas');
    canvas.isDrawingMode = true;
    canvas.freeDrawingBrush.color = '#3CDD8C';
    canvas.freeDrawingBrush.width = 10;
    // Load existing canvas data
    fetch(`/rooms/{id}/load`)
    .then(response => response.json())
    .then(data => {{
        if (data && Object.keys(data).length > 0) {{
            canvas.loadFromJSON(data, canvas.renderAll.bind(canvas));
        }}
    }});
    
    // ... (rest of the JavaScript remains the same)
"""

通过这些更改,用户现在可以保存他们的绘图,并在返回房间时加载它们。画布数据以 JSON 字符串的形式存储在数据库中,便于序列化和反序列化。试试看!创建一个新房间,画一幅画,保存它,然后重新加载页面。你应该会看到你的画作重新出现,可以进行进一步的编辑。

这是完整的代码

main.py
from fasthtml.common import *
from datetime import datetime

def render(room):
    return Li(A(room.name, href=f"/rooms/{room.id}"))

app,rt,rooms,Room = fast_app('data/drawapp.db', render=render, id=int, name=str, created_at=str, canvas_data=str, pk='id')

@rt("/")
def get():
    create_room = Form(Input(id="name", name="name", placeholder="New Room Name"),
                       Button("Create Room"),
                       hx_post="/rooms", hx_target="#rooms-list", hx_swap="afterbegin")
    rooms_list = Ul(*rooms(order_by='id DESC'), id='rooms-list')
    return Titled("QuickDraw", 
                  create_room, rooms_list)

@rt("/rooms")
async def post(room:Room):
    room.created_at = datetime.now().isoformat()
    return rooms.insert(room)

@rt("/rooms/{id}")
async def get(id:int):
    room = rooms[id]
    canvas = Canvas(id="canvas", width="800", height="600")
    color_picker = Input(type="color", id="color-picker", value="#000000")
    brush_size = Input(type="range", id="brush-size", min="1", max="50", value="10")
    save_button = Button("Save Canvas", id="save-canvas", hx_post=f"/rooms/{id}/save", hx_vals="js:{canvas_data: JSON.stringify(canvas.toJSON())}")

    js = f"""
    var canvas = new fabric.Canvas('canvas');
    canvas.isDrawingMode = true;
    canvas.freeDrawingBrush.color = '#000000';
    canvas.freeDrawingBrush.width = 10;

    // Load existing canvas data
    fetch(`/rooms/{id}/load`)
    .then(response => response.json())
    .then(data => {{
        if (data && Object.keys(data).length > 0) {{
            canvas.loadFromJSON(data, canvas.renderAll.bind(canvas));
        }}
    }});
    
    document.getElementById('color-picker').onchange = function() {{
        canvas.freeDrawingBrush.color = this.value;
    }};
    
    document.getElementById('brush-size').oninput = function() {{
        canvas.freeDrawingBrush.width = parseInt(this.value, 10);
    }};
    """
    
    return Titled(f"Room: {room.name}",
                  A(Button("Leave Room"), href="/"),
                  canvas,
                  Div(color_picker, brush_size, save_button),
                  Script(src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js"),
                  Script(js))

@rt("/rooms/{id}/save")
async def post(id:int, canvas_data:str):
    rooms.update({'canvas_data': canvas_data}, id)
    return "Canvas saved successfully"

@rt("/rooms/{id}/load")
async def get(id:int):
    room = rooms[id]
    return room.canvas_data if room.canvas_data else "{}"

serve()

部署到 Railway

你可以将你的网站部署到许多托管服务提供商,在本教程中,我们将使用 Railway。首先,请确保你创建了一个 账户 并安装了 Railway CLI。安装后,请务必运行 railway login 登录你的账户。

为了使部署你的网站尽可能简单,FastHTML 内置了一个 CLI 工具,可以为你处理大部分部署过程。要部署你的网站,请在项目的根目录下的终端中运行以下命令

fh_railway_deploy quickdraw
注意

你的应用必须位于一个 main.py 文件中才能正常工作。

结论:你现在是 FastHTML 艺术家了!🎨🚀

恭喜!你刚刚使用 FastHTML 构建了一个时尚、交互式的 Web 应用程序。让我们回顾一下我们学到的内容

  1. FastHTML 允许你用最少的代码创建动态 Web 应用。
  2. 我们使用 FastHTML 的路由系统来处理不同的页面和操作。
  3. 我们集成了 SQLite 数据库来存储房间信息和画布数据。
  4. 我们利用 Fabric.js 创建了一个交互式绘图画布。
  5. 我们实现了颜色选择、画笔大小调整和画布保存等功能。
  6. 我们使用 HTMX 实现了无缝的局部页面更新,无需完全重新加载。
  7. 我们学习了如何将我们的 FastHTML 应用部署到 Railway 以便轻松托管。

你已经迈出了进入 FastHTML 开发世界的第一步。从这里开始,可能性是无限的!你可以通过添加以下功能进一步增强绘图应用

  • 实现不同的绘图工具(例如,形状、文本)
  • 添加用户身份验证
  • 创建一个已保存绘图的画廊
  • 使用 WebSockets 实现实时协作绘图

无论你接下来选择构建什么,FastHTML 都会支持你。现在,去创造一些了不起的东西吧!编程愉快!🖼️🚀