JS 应用程序介绍

如何逐步使用FastHTML构建具有自定义JavaScript的网站

安装

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

  1. Python
  2. 一个 Python 包管理器,例如 pip(通常随 Python 一起提供)或 uv
  3. 快速HTML
  4. 网页浏览器
  5. Railway.app 账户

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

请注意,您只需在每个环境中按照安装部分的步骤进行一次。如果您创建一个新的代码库,则无需重新执行这些步骤。

安装 FastHTML

对于Mac、Windows和Linux,请输入:

pip install python-fasthtml

第一步

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

创建一个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 并打开出现的“链接”在您的浏览器中。

快速绘画:快速HTML冒险 🎨✨

本教程的最终结果将是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 数据库和数据类的 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')

我们指定一个渲染函数将我们的数据类转换为HTML,这与我们之前使用的patch装饰器中扩展__ft__方法是一样的。我们将在接下来的教程中使用这个方法,因为它更简洁,更易于阅读。

  • 我们使用SQLite数据库(通过FastLite)来存储我们的房间。
  • 每个房间都有一个 id(整数)、一个 name(字符串)和一个 created_at 时间戳(字符串)。
  • Room数据类是基于此结构自动生成的。
  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)
  • 当用户提交“创建房间”表单时,将调用此路由。
  • 它创建一个新的房间对象,设置创建时间,并将其插入到数据库中。
  • 它返回一个带有链接到新房间的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。要开始,请确保您创建一个 account 并安装 Railway CLI。安装完成后,请确保运行 railway login 以登录到您的帐户。

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

fh_railway_deploy quickdraw
注意

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

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

恭喜!您刚刚使用FastHTML构建了一个时尚、互动的web应用程序。让我们回顾一下我们所学到的内容:

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

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

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

无论你选择构建什么,FastHTML 都会支持你。现在去创造一些令人惊叹的东西吧!编码愉快! 🖼️🚀