资讯动态

用 PostgREST 与 htmx 构建无前端框架的 To-Do 应用:自定义 text/html 媒体类型实战

发布时间:2026/9/11 13:40:30 来源:尧图企业网站定制
用 PostgREST 与 htmx 构建无前端框架的 To-Do 应用自定义 text/html 媒体类型实战【免费下载链接】postgrestREST API for any Postgres database项目地址: https://gitcode.com/GitHub_Trending/po/postgrest这篇指南演示如何让 PostgREST 返回原始 HTML 内容并结合 htmx 库处理 AJAX 请求在不引入任何前端框架的前提下构建一个可增删改查的 To-Do 单页应用。读完本文你将掌握通过 PostgreSQL domain 注册自定义媒体类型、用 SQL 函数生成 HTML 模板、以及利用 htmx 属性hx-post、hx-get、hx-target等与/rpc端点交互的完整链路并理解 PostgREST 底层内容协商Content Negotiation与 schema cache 重载机制。本指南是 docs/how-tos/providing-html-content-using-htmx.rst 的完整展开示例代码与仓库源码一一对应。htmx 期望服务端返回一段 HTML并用它替换 DOM 中的某个元素——这正是 PostgREST 自定义媒体类型所能交付的响应形态。注意本文是概念验证Proof of Concept用于展示 PostgREST 与 htmx 两种技术结合所能达到的效果。项目团队正在推进 plmustache 相关工作以进一步改进本教程在 HTML 渲染方面的体验。一、前置准备基于 tut0 的环境搭建本教程要构建一个 To-Do 应用因此请先完成 tut0零基础入门教程 再继续。tut0 会为你准备好以下基础设施名为api的 schema以及核心数据表api.todos含id、done、task、due字段匿名请求角色web_anonnologin与连接角色authenticatornoinherit login其中authenticator被授予切换到web_anon的权限最小化配置文件tutorial.confdb-uri postgres://authenticator:mysecretpasswordlocalhost:5432/postgres db-schemas api db-anon-role web_anon完成 tut0 后PostgREST 即可在http://localhost:3000上提供服务。1. 简化权限把 todos 表完全开放给 web_anon为了聚焦 HTML 与 htmx 的主题本教程不使用认证因此需要把所有权限授予web_anon用户grant all on api.todos to web_anon; grant usage, select on sequence api.todos_id_seq to web_anon;第一条语句放开表上的全部 DML 权限第二条语句针对id字段的自增序列授权tut0 中id为generated by default as identity插入时需要序列权限。2. 注册自定义媒体类型create domain text/html as text接下来把text/html注册为自定义媒体类型。这样 PostgREST 就能识别浏览器发来的携带Accept: text/html头的请求并返回一份原始 HTML 文档create domain text/html as text;关于这一机制详见 Media Type Handlers 参考文档PostgREST 允许以 PostgreSQL domain 作为类型别名来表达自定义媒体类型domain 名称必须符合 RFC 6838 的要求。用这些 domain 作为返回类型SQL 函数就变成了媒体类型处理器handler。从源码层面看自定义媒体类型在 src/library/PostgREST/MediaType.hs 中被解析为MTOther Text分支见decodeMediaType的兜底分支_ - MTOther mt。值得注意的是toContentType的实现charset case ct of MTOther _ - mempty _ - ; charsetutf-8也就是说自定义媒体类型如text/html的响应头不会像 JSON 那样追加; charsetutf-8这一点与下文测试用例中Content-Type: text/html的断言完全吻合。二、创建第一个 HTML 响应api.index端点让我们创建一个返回基础 HTML 文件的函数样式使用 Pico CSS图标使用 Ionicons后续列表交互会用到create or replace function api.index() returns text/html as $$ select $html$ !DOCTYPE html html head meta charsetutf-8 meta nameviewport contentwidthdevice-width, initial-scale1 titlePostgREST HTMX To-Do List/title !-- Pico CSS for CSS styling -- link hrefhttps://cdn.jsdelivr.net/npm/picocss/piconext/css/pico.min.css relstylesheet / /head body main classcontainer article h5 styletext-align: center; PostgREST HTMX To-Do List /h5 /article /main !-- Script for Ionicons icons -- script typemodule srchttps://unpkg.com/ionicons7.1.0/dist/ionicons/ionicons.esm.js/script script nomodule srchttps://unpkg.com/ionicons7.1.0/dist/ionicons/ionicons.js/script /body /html $html$; $$ language sql;这里的关键点是函数返回类型是text/htmldomain函数体用 PostgreSQL 的$html$...$html$美元引用包裹整段 HTML 字符串。浏览器访问http://localhost:3000/rpc/index即可看到页面。内容协商如何选中这个处理器请求到达时PostgREST 会根据Accept头与可产生的媒体类型求交集。在 src/library/PostgREST/Plan/Negotiate.hs 的negotiateContent中firstAcceptedPick listToMaybe $ mapMaybe matchMT accepts即按Accept头里出现的顺序取第一个可匹配的媒体类型对于非 vendor 类型走lookupHandler依次尝试「标识符 */*」→「标识符 具体类型」→「anyelement 具体类型」的查找路径。若Accept: text/html匹配到返回该 domain 的函数该函数就被选中并作为处理器执行若没有任何媒体类型可用则返回 406 错误。三、列出与创建 To-Dos1. HTML 清洗与列表模板函数现在展示数据库中已插入的待办事项。首先需要一个函数来清洗任务文本中可能存在的 HTML 内容防止注入与破坏页面结构create or replace function api.sanitize_html(text) returns text as $$ select replace(replace(replace(replace(replace($1, , amp;), , quot;),, gt;),, lt;), , apos;) $$ language sql; create or replace function api.html_todo(api.todos) returns text as $$ select format($html$ div %2$s %3$s /%2$s /div $html$, $1.id, case when $1.done then s else span end, api.sanitize_html($1.task) ); $$ language sql stable; create or replace function api.html_all_todos() returns text as $$ select coalesce( string_agg(api.html_todo(t), hr/ order by t.id), pemThere is nothing else to do./em/p ) from api.todos t; $$ language sql;这两个函数仅用于构建列表模板不直接作为 PostgREST 端点暴露。要点拆解api.html_todo以api.todos表类型作为参数把每条待办格式化为列表元素。它借助 PostgreSQL 的format函数按位置替换占位符例如%1$s会被替换为第一个参数$1.id%2$s依据done状态渲染成s删除线或spanapi.html_all_todos返回所有列表元素的外层包裹。它用聚合函数string_agg把所有待办拼接成单个文本值按id排序条目间用hr/分隔当api.todos表为空时coalesce保证返回替代消息pemThere is nothing else to do./em/p而非空串。2. 注册待办端点并改造首页接下来新增一个向数据库登记待办的端点并相应修改/rpc/index页面create or replace function api.add_todo(_task text) returns text/html as $$ insert into api.todos(task) values (_task); select api.html_all_todos(); $$ language sql; create or replace function api.index() returns text/html as $$ select $html$ !DOCTYPE html html head meta charsetutf-8 meta nameviewport contentwidthdevice-width, initial-scale1 titlePostgREST HTMX To-Do List/title !-- Pico CSS for CSS styling -- link hrefhttps://cdn.jsdelivr.net/npm/picocss/piconext/css/pico.min.css relstylesheet/ !-- htmx for AJAX requests -- script srchttps://unpkg.com/htmx.org/script /head body main classcontainer stylemax-width: 600px hx-headers{Accept: text/html} article h5 styletext-align: center; PostgREST HTMX To-Do List /h5 form hx-post/rpc/add_todo hx-target#todo-list-area hx-triggersubmit hx-onhtmx:afterRequest: this.reset() input typetext name_task placeholderAdd a todo... /form div idtodo-list-area $html$ || api.html_all_todos() || $html$ div /article /main !-- Script for Ionicons icons -- script typemodule srchttps://unpkg.com/ionicons7.1.0/dist/ionicons/ionicons.esm.js/script script nomodule srchttps://unpkg.com/ionicons7.1.0/dist/ionicons/ionicons.js/script /body /html $html$; $$ language sql;/rpc/add_todo端点接收_task参数完成插入并返回数据库中全部待办组成的 HTML新版/rpc/index在main上增加了hx-headers{Accept: text/html}确保 body 内所有 htmx 元素发出的请求都携带该头否则 PostgREST 无法将其识别为 HTML 请求form元素使用了 htmx 库逐项拆解如下属性作用hx-post/rpc/add_todo向/rpc/add_todo端点发送 AJAX POST 请求携带input中_task的值hx-target#todo-list-area请求返回的 HTML 内容会放入div idtodo-list-area/div即待办列表区域内hx-triggersubmit提交表单时在input内按回车才发起请求hx-onhtmx:afterRequest: this.reset()请求完成后执行 JavaScript 清空表单$html$ || api.html_all_todos() || $html$是美元引用拼接语法把api.html_all_todos()的执行结果内联到首页 HTML 中从而在页面加载时即渲染出全部待办。完成后http://localhost:3000/rpc/index页面即可列出全部待办并通过输入框提交新任务。别忘了刷新 schema cache详见第五节。四、编辑与删除 To-Dos1. 让列表项具备交互能力现在修改api.html_todo为每条待办加上「切换完成状态」「编辑」「删除」三个交互create or replace function api.html_todo(api.todos) returns text as $$ select format($html$ div classgrid div idtodo-edit-area-%1$s form idedit-task-state-%1$s hx-post/rpc/change_todo_state hx-vals{_id: %1$s, _done: %4$s} hx-target#todo-list-area hx-triggerclick %2$s stylecursor: pointer %3$s /%2$s /form /div div styletext-align: right button classoutline hx-get/rpc/html_editable_task hx-vals{_id: %1$s} hx-target#todo-edit-area-%1$s hx-triggerclick span ion-icon namecreate/ion-icon /span /button button classoutline contrast hx-post/rpc/delete_todo hx-vals{_id: %1$s} hx-target#todo-list-area hx-triggerclick span ion-icon nametrash stylecolor: #f87171/ion-icon /span /button /div /div $html$, $1.id, case when $1.done then s else span end, api.sanitize_html($1.task), (not $1.done)::text ); $$ language sql stable;新引入的 htmx 特性拆解如下状态切换formhx-post/rpc/change_todo_state向该端点发 AJAX POST 请求用于切换待办的done状态hx-vals{_id: %1$s, _done: %4$s}直接以 JSON 形式附加请求参数这是用隐藏input传参的替代方案%4$s为(not $1.done)::text即「取反后的布尔值转文本」hx-triggerclick点击元素后发起请求。第一个button编辑hx-get/rpc/html_editable_task向该端点发 AJAX GET 请求返回一个带输入框的 HTML 以便编辑任务hx-target#todo-edit-area-%1$s返回的 HTML 替换具有该 id 的元素——注意这里替换的是单条任务todo-edit-area-id而非整个列表hx-vals{_id: %1$s}为 GET 请求附加查询参数。文档中的拆解特别提醒如果需要按表列过滤而非函数参数应使用eq.运算符例如{id: eq.%1$s}。第二个button删除hx-post/rpc/delete_todo发起 POST 请求删除对应待办。2. 可编辑输入框端点点击编辑按钮后需要启用任务编辑因此创建api.html_editable_task端点create or replace function api.html_editable_task(_id int) returns text/html as $$ select format ($html$ form idedit-task-%1$s hx-post/rpc/change_todo_task hx-headers{Accept: text/html} hx-vals{_id: %1$s} hx-target#todo-list-area hx-triggersubmit,focusout input idtask-%1$s typetext name_task value%2$s autofocus /form $html$, id, api.sanitize_html(task) ) from api.todos where id _id; $$ language sql;该函数按_id查询并返回一个form包裹的输入框value中预填当前任务文本经sanitize_html清洗。注意这里的表单显式携带了hx-headers{Accept: text/html}——因为该元素不在首页 body 的hx-headers作用域内必须自行声明否则请求将因缺少Accept: text/html而无法被识别为 HTML。hx-triggersubmit,focusout意味着提交或焦点移出都会触发保存。3. 修改与删除端点最后添加三个真正修改数据库的端点create or replace function api.change_todo_state(_id int, _done boolean) returns text/html as $$ update api.todos set done _done where id _id; select api.html_all_todos(); $$ language sql; create or replace function api.change_todo_task(_id int, _task text) returns text/html as $$ update api.todos set task _task where id _id; select api.html_all_todos(); $$ language sql; create or replace function api.delete_todo(_id int) returns text/html as $$ delete from api.todos where id _id; select api.html_all_todos(); $$ language sql;三个函数的模式完全一致先执行 DML再调用api.html_all_todos()返回全新的完整待办列表 HTML由 htmx 用其替换页面上过期的列表区域api.change_todo_state用请求中的_id与_done更新done列api.delete_todo用请求中的_id删除对应待办api.change_todo_task用请求中的_id与_task修改task列。刷新 schema cache 之后http://localhost:3000/rpc/index页面即可完成对待办的编辑、删除与状态切换整个 To-Do 应用的功能就此闭环。五、别忘了刷新 Schema Cache新增/修改了函数后必须让 PostgREST 重新读取数据库元数据否则新端点不会生效。根据 Schema Cache 参考文档有以下方式发送信号向 PostgREST 进程发送SIGUSR1信号手动重载缓存无需重启服务kill -SIGUSR1 postgrest_pid数据库内 NOTIFY在 psql 中执行NOTIFY pgrst, reload schema;PostgREST 监听该频道后触发重载自动重载通过事件触发器event trigger在 DDL 变更时自动发送NOTIFY pgrst, reload schema实现「忘记缓存」的体验参考文档中的pgrst_watch示例。六、测试与源码验证仓库中的测试用例直接印证了本文所述机制。在 test/spec/Feature/Query/CustomMediaSpec.hs 中函数返回标量 Accept: text/html请求/rpc/welcome.html得到完整 HTML 文档并断言Content-Type恰为text/html注意不带; charsetutf-8后缀与MediaType.hs中MTOther分支不附加 charset 的实现一致媒体类型不可用时的 406 响应请求text/plain而无可用的处理器时返回PGRST107None of these media types are available: text/plain表级聚合处理器为表定义返回 domain 类型的聚合transition/final function后可用对应媒体类型请求表端点这在 Media Type Handlers 参考文档 中有完整示例TWKB、覆盖内建text/csv、*/*任意处理器等。从实现链路看一次 htmx → PostgREST 的 HTML 请求完整经过ApiRequest解析Accept头 →Plan/Negotiate.hs的negotiateContent内容协商选中处理器 → 执行 SQL 函数 →Response.hs的contentTypeHeaders依据媒体类型设置响应头。七、注意事项与边界本文属于概念验证直接以 SQL 函数拼接 HTML 的方式适用于小规模、低复杂度场景PostgREST 团队正在开发的 plmustache 项目旨在进一步改进 HTML 渲染体验text/html这类自定义媒体类型以 domain 表达名称受 PostgreSQL 标识符长度限制超长媒体类型可考虑*/*任意处理器详见 Media Type Handlers 参考文档自定义媒体类型响应不带; charsetutf-8浏览器按 HTML5 规范推断编码因此模板中保留meta charsetutf-8声明是必要的本教程关闭了认证并将api.todos全量开放给web_anon仅适用于本地演示真实部署应结合 Auth 参考文档 配置 JWT 与行级安全策略。【免费下载链接】postgrestREST API for any Postgres database项目地址: https://gitcode.com/GitHub_Trending/po/postgrest创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价