最近,我在考虑是否应该从当前的Web应用堆栈(FastAPI、HTML、CSS和一些JavaScript)迁移到现代Web框架。我特别对FastHTML、Next.js以及Svelte产生了兴趣。
- FastHTML:自从Jeremy Howard在一个月前发布以来,许多人已经开始使用它进行构建。它的目标是通过纯Python来实现现代Web应用。
- Next.js:我遇到了许多使用它构建的应用程序,比如cal.com和roomGPT。它拥有庞大的生态系统,并且因其能够构建生产级别的Web应用而广受欢迎。
- SvelteKit:这个轻量级框架已受到开发者们的欢迎(Stack Overflow, TSH, State of JS),在过去几年中我的朋友Swyx也在他的文章为什么我喜欢Svelte中提到过它。
为了更深入了解这些框架,我用每个框架构建了相同的Web应用。我称之为“查看您的数据”的这款应用让使用者可以:
上传
一个CSV文件以初始化SQLite数据库表查看
浏览器中的表格编辑
表格中的个别字段删除
表格中的个别行下载
更新后的表格数据为新的CSV文件
通过在这几个框架中实现这些CRUD(创建、读取、更新和删除)操作,我希望能够了解每个框架的独特特点及相关的开发体验。为了简化问题,我将使用SQLite作为数据库。作为基线,我会先用我所熟悉的FastAPI构建这款应用。
我在Twitter和LinkedIn上就这三个框架进行了投票。
FastAPI + Jinja + HTML + CSS + JavaScript
使用FastAPI构建这个应用相当直接(代码)。主要的组件包括:
main.py
:用于上传/下载数据、更新字段、删除行的路由index.html
:定义脚本、表格和按钮的HTML文档style.css
:视觉样式如列宽、文本换行、滚动条等script.js
:客户端功能以上传CSV文件、加载显示数据、更新/删除行以及下载更新后的数据为CSV格式
以下是该Web应用的外观。虽然看起来并不美观,但它满足了上述要求。我有意保持了最少的视觉样式(对于当前和后续的应用程序),以便能专注于框架和功能而不是设计。
FastHTML
为了学习FastHTML,我首先查阅了文档并按照此教程构建了一个简单的待办事项应用。我对不熟悉的部分提供链接至相关文档如ft components,htmx,pico.css作为上下文信息。在FastHTML的帮助下,整个应用程序可以在一个main.py
和一个小的style.css
中实现(代码)。
以下是该应用的样子。
初版完成后,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的应用程序:
Next.JS
为了学习 Next.js,我完成了React Foundations和[Next.js](https://nextjs.org/learn-dashboard-ap