都2023年了,再说说使用HTMX的感受

王福强

2023-07-13


快糙猛的写一个简单的单页应用是可以的,只不过得转换思维,而且,再简单也需要些变通,比如, 最简单的一个点,它生成自己是正宗的hypermedia,但对于响应的status code的处理实在不敢说有多方便,即使是它提供了extension的情况下。 反正我是嫌它:

  1. 繁琐不好用(引入js,至少两个地方声明指令);
  2. 不能用(可能是我没搞明白,反正我是没实验成功);

很简单的处理非200状态码的需求,要么你就集成到核心库,否则,extension的版本兼容性之类肯定不会好,对用户也多了一层认知负担。 所以,最后我只能在服务器端舍弃状态码,直接都用200,反正通过返回的错误消息片段来区分反馈。1

    //    ctx.response().setStatusCode(400)
    val template =
      s"""
         |<div class="flex flex-wrap ${if (disappearAfter) "disappear" else ""}">
         |  <div class="mx-auto w-full p-4 md:w-1/2">
         |    <div class="h-full rounded shadow-md p-8">
         |      <svg width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
         |  <path fill-rule="evenodd" clip-rule="evenodd" d="M49.0242 14.4358C43.3597 12.5214 37.2232 12.5214 31.5588 14.4358L30.5226 14.786C25.2023 16.5841 20.6721 20.1749 17.7068 24.944L17.5034 25.2712C14.4253 30.2217 13.3205 36.1484 14.408 41.8755C15.4616 47.4241 18.4999 52.399 22.955 55.8702L23.72 56.4662C24.5986 57.1508 25.0473 58.2719 24.784 59.3541L24.5935 60.1374C24.3954 60.9518 24.4167 61.8042 24.6553 62.6078C25.2527 64.62 27.102 66 29.201 66H35.2624H45.3196H51.3893C53.4834 66 55.3284 64.6233 55.9244 62.6158C56.1644 61.8073 56.184 60.9493 55.981 60.1306L55.7953 59.3821C55.5247 58.2907 55.9759 57.1573 56.8629 56.4662L57.628 55.8702C62.083 52.399 65.1213 47.4241 66.1749 41.8755C67.2624 36.1484 66.1576 30.2217 63.0795 25.2712L62.8761 24.944C59.9108 20.1749 55.3806 16.5841 50.0604 14.786L49.0242 14.4358Z" fill="#9B51E0" />
         |  <path d="M31.5588 14.4358L32.1991 16.3305V16.3305L31.5588 14.4358ZM49.0242 14.4358L48.3838 16.3305L48.3838 16.3305L49.0242 14.4358ZM30.5226 14.786L29.8822 12.8913L29.8822 12.8913L30.5226 14.786ZM17.7068 24.944L16.0083 23.888L17.7068 24.944ZM17.5034 25.2712L19.2019 26.3272L17.5034 25.2712ZM14.408 41.8755L16.3729 41.5024L14.408 41.8755ZM22.955 55.8702L24.1842 54.2925L24.1842 54.2925L22.955 55.8702ZM24.5935 60.1374L26.5368 60.6101L24.5935 60.1374ZM24.6553 62.6078L22.738 63.177L24.6553 62.6078ZM55.9244 62.6158L54.0071 62.0465L55.9244 62.6158ZM55.981 60.1306L57.9222 59.6493L55.981 60.1306ZM57.628 55.8702L58.8572 57.4478H58.8572L57.628 55.8702ZM66.1749 41.8755L64.21 41.5024L66.1749 41.8755ZM63.0795 25.2712L61.381 26.3272L61.3811 26.3272L63.0795 25.2712ZM62.8761 24.944L64.5746 23.888L64.5746 23.888L62.8761 24.944ZM50.0604 14.786L50.7007 12.8913L50.7007 12.8913L50.0604 14.786ZM55.7953 59.3821L53.8541 59.8635L55.7953 59.3821ZM24.784 59.3541L22.8407 58.8814L24.784 59.3541ZM32.1991 16.3305C37.4482 14.5565 43.1347 14.5565 48.3838 16.3305L49.6645 12.5411C43.5847 10.4863 36.9982 10.4863 30.9184 12.5411L32.1991 16.3305ZM31.1629 16.6807L32.1991 16.3305L30.9184 12.5411L29.8822 12.8913L31.1629 16.6807ZM19.4053 26.0001C22.1257 21.6247 26.2819 18.3304 31.1629 16.6807L29.8822 12.8913C24.1227 14.8379 19.2185 18.7251 16.0083 23.888L19.4053 26.0001ZM19.2019 26.3272L19.4053 26.0001L16.0083 23.888L15.8049 24.2151L19.2019 26.3272ZM16.3729 41.5024C15.379 36.2682 16.3887 30.8517 19.2019 26.3272L15.8049 24.2151C12.4619 29.5918 11.262 36.0286 12.4431 42.2486L16.3729 41.5024ZM24.1842 54.2925C20.1126 51.1201 17.3358 46.5734 16.3729 41.5024L12.4431 42.2486C13.5874 48.2747 16.8873 53.6778 21.7257 57.4478L24.1842 54.2925ZM24.9492 54.8886L24.1842 54.2925L21.7257 57.4478L22.4908 58.0439L24.9492 54.8886ZM22.8407 58.8814L22.6502 59.6647L26.5368 60.6101L26.7274 59.8268L22.8407 58.8814ZM22.6502 59.6647C22.3685 60.8226 22.3988 62.0346 22.738 63.177L26.5726 62.0385C26.4346 61.5739 26.4223 61.081 26.5368 60.6101L22.6502 59.6647ZM22.738 63.177C23.5874 66.0379 26.2167 68 29.201 68V64C27.9873 64 26.918 63.202 26.5726 62.0385L22.738 63.177ZM29.201 68H35.2624V64H29.201V68ZM35.2624 68H45.3196V64H35.2624V68ZM45.3196 68H51.3893V64H45.3196V68ZM51.3893 68C54.3688 68 56.9936 66.0412 57.8417 63.185L54.0071 62.0465C53.6631 63.2053 52.5981 64 51.3893 64V68ZM57.8417 63.185C58.1832 62.0347 58.211 60.8139 57.9222 59.6493L54.0398 60.612C54.1569 61.0846 54.1457 61.5798 54.0071 62.0465L57.8417 63.185ZM57.9222 59.6493L57.7365 58.9007L53.8541 59.8635L54.0398 60.612L57.9222 59.6493ZM56.3987 54.2925L55.6337 54.8886L58.0922 58.0439L58.8572 57.4478L56.3987 54.2925ZM64.21 41.5024C63.2471 46.5734 60.4703 51.1201 56.3987 54.2925L58.8572 57.4478C63.6957 53.6778 66.9955 48.2747 68.1398 42.2486L64.21 41.5024ZM61.3811 26.3272C64.1942 30.8517 65.2039 36.2682 64.21 41.5024L68.1398 42.2486C69.3209 36.0286 68.121 29.5918 64.778 24.2151L61.3811 26.3272ZM61.1777 26.0001L61.381 26.3272L64.778 24.2151L64.5746 23.888L61.1777 26.0001ZM49.42 16.6807C54.301 18.3304 58.4572 21.6247 61.1777 26.0001L64.5746 23.888C61.3645 18.7251 56.4602 14.8379 50.7007 12.8913L49.42 16.6807ZM48.3838 16.3305L49.42 16.6807L50.7007 12.8913L49.6645 12.5411L48.3838 16.3305ZM57.7365 58.9007C57.6755 58.6545 57.7619 58.3012 58.0922 58.0439L55.6337 54.8886C54.19 56.0135 53.3739 57.9269 53.8541 59.8635L57.7365 58.9007ZM22.4908 58.0439C22.8144 58.296 22.8991 58.6412 22.8407 58.8814L26.7274 59.8268C27.1954 57.9025 26.3828 56.0055 24.9492 54.8886L22.4908 58.0439Z" fill="#56CCF2" />
         |  <path d="M25.8269 33C27.9705 31.7624 30.6115 31.7624 32.7551 33C34.8987 34.2376 36.2192 36.5248 36.2192 39C36.2192 41.4752 34.8987 43.7624 32.7551 45C30.6115 46.2376 27.9705 46.2376 25.8269 45C23.6833 43.7624 22.3628 41.4752 22.3628 39C22.3628 36.5248 23.6833 34.2376 25.8269 33Z" fill="#56CCF2" />
         |  <path d="M47.8269 33C49.9705 31.7624 52.6115 31.7624 54.7551 33C56.8987 34.2376 58.2192 36.5248 58.2192 39C58.2192 41.4752 56.8987 43.7624 54.7551 45C52.6115 46.2376 49.9705 46.2376 47.8269 45C45.6833 43.7624 44.3628 41.4752 44.3628 39C44.3628 36.5248 45.6833 34.2376 47.8269 33Z" fill="#56CCF2" />
         |  <path d="M35.291 66V58" stroke="#56CCF2" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
         |  <path d="M45.291 66V58" stroke="#56CCF2" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
         |</svg>
         |      <p class="mb-6 leading-relaxed">
         |      Oops... $message
         |      </p>
         |    </div>
         |  </div>
         |</div>
         |""".stripMargin
    ctx.response().end(template)

UPDATE@2023-07-15: 对于非200状态码,可以用事件捕捉自定义:

document.body.addEventListener('htmx:beforeSwap', function(evt) {
    if(evt.detail.xhr.status === 404){
        // alert the user when a 404 occurs (maybe use a nicer mechanism than alert())
        alert("Error: Could Not Find Resource");
    } else if(evt.detail.xhr.status === 422){
        // allow 422 responses to swap as we are using this as a signal that
        // a form was submitted with bad data and want to rerender with the
        // errors
        //
        // set isError to false to avoid error logging in console
        evt.detail.shouldSwap = true;
        evt.detail.isError = false;
    } else if(evt.detail.xhr.status === 418){
        // if the response code 418 (I'm a teapot) is returned, retarget the
        // content of the response to the element with the id `teapot`
        evt.detail.shouldSwap = true;
        evt.detail.target = htmx.find("#teapot");
    }
});

原来对于html片段的管理还是有抵触的,但既然是做小东西简单的东西,配合Scala的String interpolation特性以及tailwind的play所见即所得,也还算过得去,直接拷贝粘贴也差不多了。

所以,最后就变成了,htmx发送最简单的表单提交请求,服务器端提取参数获得数据库状态后,通过string interpolation插入html片段作为字符串响应返回就行了。

基本上就是一个index.html静态文件 + n多的route返回html片段,多了肯定不好管理,我就一个handler,多写几个方法放一个类里管理也够了。

好处就是不用配置jte的编译之类,坏处嘛,失去了开发期间实时预览,频繁重启,哈(热加载就算了,也懒得配置)

Tips

提交表单前确认

    hx-confirm="Are you sure?"

运行期间状态标识

最主要的是,需要定义预先的css:

.htmx-indicator{
    opacity:0;
    transition: opacity 500ms ease-in;
}
.htmx-request .htmx-indicator{
    opacity:1
}
.htmx-request.htmx-indicator{
    opacity:1
}
<button hx-post="/example">
   <img  class="htmx-indicator" src="/img/bars.svg"/>
   Post It!
</button>

通知类消息

我是为返回的响应元素加了disappear这个class:

        .disappear {
            animation: disappear 0s linear 3s forwards;
        }

        @keyframes disappear {
            to {
                opacity: 0;
            }
        }

原则上这类应该是客户端处理比较好,但htmx既然都是server端控制,所以,也就暂时这样了。反正这种跟正常响应用不同的方法渲染就好了,正常的响应不加disappear这个class就可以了。

history

对于顶层的link,最好加入浏览器的history,便于直接访问和回溯,这个效果通过在事件触发的时候添加hx-push-url="true"获得:

        <nav class="md:ml-auto md:mr-auto flex flex-wrap items-center text-base justify-center">
            <a class="mr-5 hover:text-gray-900" href="/">Permission Mgmt</a>
            <a class="mr-5 hover:text-gray-900" hx-get="/product" hx-target="#mainZone" hx-swap="outerHTML" hx-push-url="true">Product Mgmt</a>
            <a class="mr-5 hover:text-gray-900" hx-get="/console" hx-target="#mainZone" hx-swap="outerHTML" hx-push-url="true">SQL Console</a>
        </nav>

附上HTMX相关的实操案例视频:500行HTMX和Scala代码打造一个快糙猛的Web后台程序


  1. 不过这里也可能是思路没有转过来,还在用基于json的rpc思路看待它,如果从html segment的角度看,其实即使返回的是错误信息,也应该是以正常状态码返回的↩︎


>>>>>> 更多阅读 <<<<<<


「福强私学」来一个?

「福强私学」, 一部沉淀了个人成长、技术与架构、组织与管理以及商业上的方法与心法的百科全书。


开天窗,拉认知,订阅「福报」,即刻拥有自己的全模态人工智能。

订阅「福报」