はじめに
近年注目を集めたHTMXに興味を持って、ついに試す時間が出来ましたので、
その経験や感想を共有させていただきます。
HTMXとは?
HTMXの目的はJSを最低限にして、高度なUXを作ることです。
それはどういう実現できるのを見てみましょう。
前提
PugとExpressを使っていますが、どんなサーバー、バックエンドやテンプレートエンジンを使っても問題ありません。
今回作るTwitterみたいなメッセージ投稿ページです。
実装
レイアウトを定義
doctype html
html(lang="en")
head
title Twitter clone in htmx
link(href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css", rel="stylesheet", crossorigin="anonymous")
link(href="/css/global.css", rel="stylesheet", crossorigin="anonymous")
body
nav.navbar.navbar-dark.bg-dark.shadow-sm.py-0
.container
a.navbar-brand(href="#") htmx-twitter
block navbar
.container
.row.justify-content-center
main.col-10
block content
script(src="https://unpkg.com/htmx.org@1.9.5")
script(src="https://unpkg.com/hyperscript.org@0.9.12")
block script
script(src="/js/main.js")
こちらは特に特別な実装が含まれていない、但し見える通りhtmxをscriptとして定義してます。
hyperscriptはよくHTMXと使われているので、入れてます。
main.jsはクライアント側で使うJSになります、HTMXの仕組みをつかっているので、後から説明説明させていただきます。
ページ
extends layout/main.pug
block navbar
span.navbar-text.text-white #{name}
block content
p.text-center.mt-2 A Twitter clone in <a href="https://htmx.org">htmx</a> and Node
div(hx-ext="ws,redirect-ws" ws-connect="/tweet")
form(hx-ws="send" _="on submit reset() me")
.mb-3.row
label(for="txtMessage") Message:
textarea#txtMessage.form-control(rows="3", name="message", required="true")
.d-grid.gap-2.col-3.mx-auto.mb-3
button.btn.btn-primary.text-center(type="submit") Tweet
div
#timeline(hx-get="/posts" hx-trigger="load, every 60s")
block script
script(src="https://unpkg.com/htmx.org@1.9.5/dist/ext/ws.js")
こちらは面白い点が三つあります:
div(hx-ext="ws,redirect-ws" ws-connect="/tweet")
-
hx-ext
: どんなHTMXのExtensionを利用するための設定です。- ws: WebSocketのExtensionを利用する(関連なjsファイルを含める必要がありますので、
ext/ws.js
のスクリプトを入れてます) -
redirect-ws
: 自作のExtensionです。後から説明します。
- ws: WebSocketのExtensionを利用する(関連なjsファイルを含める必要がありますので、
-
ws-connect
: WebSocketのEndpointの定義
これだけでWebSocketの接続が可能になります。
そのWebSocketを使って、もちろんメッセージの送信・受取は可能ですが、実はDOMの変化も可能です。
form(hx-ws="send" _="on submit reset() me")
-
hx-ws=send
: こちらの設定でFormがsubmitされたら、WebSocketに送られる様にことになります。 -
_="on submit reset() me"
: こちらはHyperScriptなので、HTMXとは別ですが、相性がいいですので、よく一緒につかわれています。今回はただフォームのsubmitイベントの時にreset()ファンクションを実行する様に定義してます。
#timeline(hx-get="/posts" hx-trigger="load, every 60s")
-
hx-get="/posts"
: こちらの設定は/posts
というhtmlファイルを取得する意味です。他の設定(hx-target)がなければ取得されたものは現タグの中身を入替えます。 -
hx-trigger="load, every 60s"
: こちらの設定は/posts
を取得タイミングが指定されてます。-
load
: ページのロード時に取得されることを意味します。 -
every 60s
: 60秒毎に取得されることを意味します。
-
サブページ(フラグメント)
/posts
(posts.pug
)
each t in [...tweets].reverse()
- var replace = true
include post
こちらは特に特別なことを入れていないのですが、replace変数をtrueに指定されていることを覚えてください。
post.pug
div(hx-swap-oob=replace ? "" : "afterbegin:#timeline")
.card.mb-2.shadow-sm(id='tweet-' + t.id)
.card-body
.d-flex
img.me-4(src=t.avatar, width="108")
div
h5.card-title.text-muted
| #{t.username}
small : #{t.time}
.card-text.lead.mb-2
| #{t.message}
include retweets
include likes
こちらはHTMXの設定一つあります:
hx-swap-oob=replace ? "" : "afterbegin:#timeline"
-
hx-swap-oob
: この設定のおかげで親が指定している入替の場所を完全に違う場所に変えることが出来ます。- 今回はreplaceがtrueの場合、指定しないので、そのまま入れ替えられることになります。
- ただし他の場合、afterbegin:#timelineに指定します。後で役に立ちます。
- その意味は#timelineに該当するタグの中の最初のポジション(afterbegin)になります。
HTMXの入替はswapと言います。
retweets.pug
button.btn.btn-link.ps-0.text-decoration-none(id='retweet-' + t.id, type="button", hx-post="/retweet/" + t.id hx-swap="outerHTML") Retweet (#{t.retweets})
こちらもHTMXの設定二つあります:
-
hx-post="/retweet/ + t.id"
: この設定はボタンはクリックされたら、/retweet/:id
のURLをPOSTでコールします。詳細はExpressJSの設定に説明します。 -
hx-swap="outerHTML"
: POSTがHTMLを返却した場合(WebSocket経由でしたら、idで入れ替えるので、この設定は不要です)、タグが入れ替える様に指定します。(デフォルトはinnerHTML
で、タグの中身が入れ替えられます)
ご気づきされたと思いますが、likes.pugも存在します。
ただretweets.pugと似たようなものですので、スキップさせてください。
クライアント側のJS(main.js
)
htmx.defineExtension('redirect-ws', {
onEvent: function(name, event) {
if (name === 'htmx:wsAfterMessage' && event.detail.message?.startsWith('{')) {
const message = JSON.parse(event.detail.message)
const redirect = message?.HEADERS?.['HX-Redirect']
if (redirect) {
alert(`Redirecting to ${redirect}`)
window.location.pathname = redirect
}
}
}
});
漸う唯一のブラウザーJSを書きました。
最初にhx-ext
にredirect-ws
を覚えていますか?今回のコードはそのredirect-ws
を定義するコードになります。
それを登録するためのファンクションはhtmx.defineExtensionになります。
- 最初のパラメーターは見える通りextensionの名称。
- 二つ目はその実装:
- 今回やりたかったのはWebSocketのメッセージがリダイレクトメッセージが届いたら、該当ページに遷移する様に。
- それを実現するためにはhtmx:wsAfterMessageというイベント(WebSocketからのメッセージが届いた後に発行されるイベント)を使います。
サーバー側(ExpressJS)
ExpressJSの記事ではありませんので、一般な設定は説明をスキップさせてください。
コード全体を確認したいのでしたら、リポジトリにあるindex.jsになります。
WebSocket(/tweet)
const tweetChannel = expressWs.getWss("/tweet");
const tweets = [];
const addTweet = (message, username) => {
const tweet = {
id: v4(),
message,
username,
retweets: 0,
likes: 0,
time: dayjs().to(dayjs(new Date().toString())),
avatar:
"https://ui-avatars.com/api/?background=random&rounded=true&name=" +
username,
};
tweets.push(tweet);
const markup = pug.compileFile("views/components/post.pug", {
globals: ["global"],
})({ t: tweet });
tweetChannel.clients.forEach((client) => client.send(markup));
return markup;
};
app.ws("/tweet", (ws, req, res) => {
ws.on("message", function (msg) {
const username = getJWT(req)?.username;
if (username) {
const { message } = JSON.parse(msg);
addTweet(message, username);
} else {
ws.send(JSON.stringify({ HEADERS: { "HX-Redirect": "/login" } }));
}
});
});
先ずはページに使われているWebSocketから説明させていただきます。
- ユーザーネームを取得して、tweetsの配列(普通はデータベース等)に新しいtweetを追加、ユーザー情報がなければリダイレクトメッセージを返却。(
redirect-ws
を覚えてますか?) - 新しく作成されたtweetを基にpost.pugを使って、HTMLは作成してWebSocketで各ユーザーに送信。
- post.pugのreplaceを指定しないので、WebSocketで送信されたHTMLはHTMXの仕組みで自動的に取り入れて、
hx-swap-oob
のおかげただし場所に追加されます。
POST (/retweet/:id)
app.post("/retweet/:id", (req, res) => {
const { id } = req.params;
const tweet = tweets.find((t) => t.id === id);
tweet.retweets += 1;
const retweets = pug.compileFile("views/components/retweets.pug");
const markup = retweets({ t: tweet });
tweetChannel.clients.forEach((client) => client.send(markup));
res.sendStatus(200);
});
単純に該当のtweetを取得して、retweets項目を上げて、HTMLを作成して、WebSocket経由でHTMLを各ユーザーに送信します。
/tweet
と同じく、これで全ユーザーのページにHTMLが入れ替えられます。
PS: WebSocketに送信されなかったらPOSTの返却で同じHTMLを返却していることで、現ユーザーのHTMLが入れ替えられます。(res.sendStatus(200) → res.send(markup))
ボナース
本記事に詳細を含まれていないですが、HTMXのリダイレクト機能を使った、単純なログインロジックもリポジトリに含まれているので、興味があれば
感想
ちょっとしたプロジェクトですが、結果は想像以上に速いです。
今まで、React、Angular等を中心にしてましたので、慣れるまで、時間がかかるかもしれませんが、
複雑さの低いやSEOスコアを重要しているプロジェクトでしたら、十分価値があると思います。
長かったのですが、ここまで読んで頂いて誠にありがとうございます。お疲れ様です。
皆さん、HTMXはどうでしたか?興味になれましたのでしょうか?
既にHTMXを利用しているを
参考