[博客翻译]使用不同Web框架构建同一应用


原文地址:https://eugeneyan.com/writing/web-frameworks/


最近,我在考虑是否应该从当前的Web应用堆栈(FastAPI、HTML、CSS和一些JavaScript)迁移到现代Web框架。我特别对FastHTML、Next.js以及Svelte产生了兴趣。

  • FastHTML:自从Jeremy Howard在一个月前发布以来,许多人已经开始使用它进行构建。它的目标是通过纯Python来实现现代Web应用。
  • Next.js:我遇到了许多使用它构建的应用程序,比如cal.comroomGPT。它拥有庞大的生态系统,并且因其能够构建生产级别的Web应用而广受欢迎。
  • SvelteKit:这个轻量级框架已受到开发者们的欢迎(Stack Overflow, TSH, State of JS),在过去几年中我的朋友Swyx也在他的文章为什么我喜欢Svelte中提到过它。

为了更深入了解这些框架,我用每个框架构建了相同的Web应用。我称之为“查看您的数据”的这款应用让使用者可以:

  • 上传一个CSV文件以初始化SQLite数据库表
  • 查看浏览器中的表格
  • 编辑表格中的个别字段
  • 删除表格中的个别行
  • 下载更新后的表格数据为新的CSV文件

通过在这几个框架中实现这些CRUD(创建、读取、更新和删除)操作,我希望能够了解每个框架的独特特点及相关的开发体验。为了简化问题,我将使用SQLite作为数据库。作为基线,我会先用我所熟悉的FastAPI构建这款应用。

Twitter和LinkedIn上的投票

我在TwitterLinkedIn上就这三个框架进行了投票。

FastAPI + Jinja + HTML + CSS + JavaScript

使用FastAPI构建这个应用相当直接(代码)。主要的组件包括:

  • main.py:用于上传/下载数据、更新字段、删除行的路由
  • index.html:定义脚本、表格和按钮的HTML文档
  • style.css:视觉样式如列宽、文本换行、滚动条等
  • script.js:客户端功能以上传CSV文件、加载显示数据、更新/删除行以及下载更新后的数据为CSV格式

FastAPI应用结构

以下是该Web应用的外观。虽然看起来并不美观,但它满足了上述要求。我有意保持了最少的视觉样式(对于当前和后续的应用程序),以便能专注于框架和功能而不是设计。

FastAPI的Web应用

FastHTML

为了学习FastHTML,我首先查阅了文档并按照此教程构建了一个简单的待办事项应用。我对不熟悉的部分提供链接至相关文档如ft componentshtmxpico.css作为上下文信息。在FastHTML的帮助下,整个应用程序可以在一个main.py和一个小的style.css中实现(代码)。

FastHTML应用结构

以下是该应用的样子。

FastHTML的Web应用

初版完成后,Hamel慷慨地提议与我一起编写这个应用的初始版本。他还邀请了FastHTML的创造者Jeremy Howard加入我们。他们向我分享了一些技巧,例如为支持LLM的文档(FastHTML的llms-ctx.txt和FastLite的html.md)提供Context帮助信息,还为使用htmx和Hyperview构建简单应用提供了很好的资源。Jeremy甚至花了时间演示如何仅用50行代码就完成了应用的构建!

from fasthtml.common import *

db = database(':memory:')
tbl = None
hdrs = (Style('''
button,input { margin: 0 1rem; }
[role="group"] { border: 1px solid #ccc; }
'''), )
app, rt = fast_app(live=True, hdrs=hdrs)

@rt("/")
async def get():
    return Titled("CSV Uploader",
        Group(
            Input(type="file", name="csv_file", accept=".csv"),
            Button("Upload", hx_post="/upload", hx_target="#results",
                   hx_encoding="multipart/form-data", hx_include='previous input'),
            A('Download', href='/download', type="button")
        ),
        Div(id="results"))

def render_row(row):
    vals = [Td(Input(value=v, name=k)) for k,v in row.items()]
    vals.append(Td(Group(Button('delete', hx_get=remove.rt(id=row['id'])),
                   Button('update', hx_post='/update', hx_include="closest tr"))))
    return Tr(*vals, hx_target='closest tr', hx_swap='outerHTML')

@rt
async def download():
    csv_data = [",".join(map(str, tbl.columns_dict))]
    csv_data += [",".join(map(str, row.values())) for row in tbl()]
    headers = {'Content-Disposition': 'attachment; filename="data.csv"'}
    return Response("\n".join(csv_data), media_type="text/csv", headers=headers)

@rt('/update')
def post(d:dict): return render_row(tbl.update(d))

@rt
def remove(id:int): tbl.delete(id)

@rt("/upload")
async def post(csv_file: UploadFile):
    global tbl
    if not csv_file.filename.endswith('.csv'): return "Please upload a CSV file"
    tbl = db.import_file('test', await csv_file.read(), pk='id')
    header = Tr(*map(Th, tbl.columns_dict))
    vals = [render_row(row) for row in tbl()]
    return Table(Thead(header), Tbody(*vals))

serve()

以下是Jeremy的应用程序:

Jeremy的应用程序

Jeremy's version of the FastHTML app

Next.JS

为了学习 Next.js,我完成了React Foundations和[Next.js](https://nextjs.org/learn-dashboard-ap