最近在研究htmx 库的时候突发奇想,利用 htmx 和我之前发布的 Python 库html-dsl 应该可以做到只使用 Python 代码构建可交互的 Web 应用。在稍作尝试后,我实现了一个简单的 Todo 应用todopy 。
技术栈 FastAPI 项目后端使用了FastAPI 框架。
html-dsl html-dsl 是我在数年前开发的一个简单的 Python 库,可以利用 Python 代码构建 HTML 页面,使用比较简单。
htmx (由 Github Copilot 生成) htmx 是一个 JavaScript 库,它允许您使用 HTML 扩展现有的 Web 应用程序,而无需编写任何 JavaScript。它使用现有的 Web 标准(例如 HTML、CSS 和 JavaScript)来实现 Ajax、WebSockets、Server-Sent Events 和其他现代 Web 功能。htmx 的目标是使 Web 开发更快、更简单、更容易,并提高 Web 应用程序的可访问性。
1 2 3 <script src ="https://unpkg.com/htmx.org@1.9.6" > </script > <button hx-post ="/clicked" hx-swap ="outerHTML" > Click Me</button >
在上面的示例中,点击按钮后,htmx 将向服务器发送一个 POST 请求,该请求将被路由到/clicked。服务器将返回一个 HTML 片段,该片段将替换按钮的外部 HTML。
tailwindcss tailwindcss 是一个实用的 CSS 库,它提供了一组实用的 CSS 类,可以快速构建页面。
构建页面 整个页面比较简单,核心是一个输入新待办项的表单和一个待办项列表。
整体布局 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @app.get("/" ) def root (): return render( HTML(lang="en" )[ HEAD[ META['charset="utf-8"' ], META['name="viewport" content="width=device-width, initial-scale=1"' ], TITLE["Todo App" ], SCRIPT(src="https://unpkg.com/htmx.org@1.9.6" ), SCRIPT(src="https://cdn.tailwindcss.com" ), STYLE[GLOBAL_STYLE], ], BODY[ DIV(_class ="max-w-md max-auto" )[ H1(_class ="text-2xl font-bold mb-4" )["Todo List" ], FORM(_class ="mb-4" , hx_post="/todos" , hx_target="#todo-list" )[ DIV(_class ="flex items-center" )[ INPUT( type ="text" , name="task" , _class ="border rounded-l px-3 py-2 w-full" , placeholder="Add new todo" , ), BUTTON( type ="submit" , _class ="bg-blue-500 hover:bg-blue-700 text-white font-bold rounded-r px-4 py-2" , )["Add" ], ] ], DIV(id ="todo-list" , hx_get="/todos" , hx_trigger="load" ), ], ], ] )
待办项列表由 id 为 todo-list 的 div 元素渲染,当页面加载完成后,htmx 会向服务器发送一个 GET 请求,服务器返回一个待办项列表的 HTML 片段,然后将其插入到 todo-list 元素中。
表单的提交也由 htmx 处理,当用户点击提交按钮时,htmx 会向服务器发送一个 POST 请求,服务器将新的待办项添加到数据库中,然后返回一个待办项列表的 HTML 片段,htmx 将其插入到 todo-list 元素中。
渲染待办项列表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 def render_todos (todos ): return render( UL(_class ="border rounded-lg overflow-hidden" )[ [ LI(_class ="flex items-center justify-between px-3 py-2 border-b" )[ SPAN(_class ="text-lg" )[todo], BUTTON( hx_delete=f"/todo/{id } " , hx_target="#todo-list" , _class ="text-red-500 hover:text-red-700" , )["delete" ], ] for id , todo in todos.items() ] ] )
页面加载、添加新待办项,以及待办项列表中的删除按钮都会触发重新渲染待办项列表,于是我封装了一个 render_todos 函数,用于渲染待办项列表的 HTML 片段。其中每一个待办项都是一个 li 元素,包含一个 span 元素和一个删除按钮。删除按钮的点击事件由 htmx 处理,当用户点击删除按钮时,htmx 会向服务器发送一个 DELETE 请求,服务器将待办项从数据库中删除,然后返回一个待办项列表的 HTML 片段,htmx 将其插入到 todo-list 元素中。
后端接口 整体比较简单,只有三个接口,分别用于获取待办项列表、添加新待办项和删除待办项。 与常规的 restful 接口不同的是,这里的接口都返回 HTML 片段,而不是 JSON 数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @app.get("/todos" ) def todos_list (todos: Annotated[dict , Depends(todos )] ): return render_todos(todos) @app.post("/todos" ) def todos_add (todos: Annotated[dict , Depends(todos )], task: str = Form("task" ) ): id = uuid.uuid4().hex todos[id ] = task return render_todos(todos) @app.delete("/todo/{id}" ) def todos_delete (todos: Annotated[dict , Depends(todos )], id : str ): del todos[id ] return render_todos(todos)
总结 这个 todo 应用只是一个玩具项目,不过 htmx 还是很强大的,即使不使用 html-dsl 这种纯 Python 的 HTML 构建库,也可以利用常规的 HTML 模板引擎(例如 Jinjia2)来构建页面,赋予了纯后端开发人员构建可交互 Web 应用的能力。