🗓️

HTMX + Hyperscriptでサーバレスアプリケーションをつくってみた

2024/02/25に公開

日程調整サービス 「イツスル?」をつくった

ちょうど今日MVPをリリースしました。

イベントを作成して、予定を入力すると皆の予定表ができる

画面数は3画面しかない小さいツールをつくったお話です。

URLをシェアすると動的なOGPが生成される

みんなに入力をお願いする時に、URLだけだと素っ気ないので

調整さんの代わりにどうぞ!

利用はこちらから(無料、ログイン不要)
https://itsusuru.com

きっかけは、日程調整ツールの老舗「◯◯さん」

普段、ゲームやオフ会の日程調整をする際に、◯◯さんを良く使っていました。
ログイン不要でプラットフォームに依存しないでブラウザだけで完結するミニマルな機能をかなり気に入っていたからです。

というのも、個人間でやり取りするケースでは、有料のリッチな調整サービスはそもそも不要だし、
ディスコのBotや、LINE日程調整ツールは、フレンドになるか鯖に招待しないと使えなかったりして個人的には使いづらく、◯◯さんはその辺りとても便利でした

しかし、いくつか操作性に大きな課題があった・・・

  • 不要な情報や、不要な画面遷移が多い
  • 候補日を決めた後、日程を削除したり時間を変更するのが極めて面倒(テキストエリアの編集をモバイルでするのはツライ)

特に2点目の問題が著者にとっては、非常にストレスであり
決め打ちで19時とされ、その後は日ごとにテキストを編集しなければならない。
無料で使えて本来何の文句もないのだが便利が故にストレスを感じていました。

ならば、自分でつくってしまおう

ストレスに関しては誰もが思っていただろうし、
これくらいサービスなら誰かがパッとつくれるレベルではあるだろうが、
儲かるわけでもないので誰もやらないのは納得です。

ただ、長らく調子を崩していた筆者のリハビリには丁度良いサイズなので
つくっちゃお〜〜〜〜〜〜〜〜〜と決めました。

どうせなら、話題のHTMX + Hyperscriptで。

https://htmx.org/

活動をほぼしていなかったので、技術のキャッチアップもろくに出来ていなかったわけだが、同僚からその存在を耳にした。コンセプトを聞くと「なにそれ、めっちゃ賛同できる」という感想だった。正直ワクワクしました。

そして、これがトップページのソースコードですが、APIへのリクエストやレスポンス処理に関して
一切JavaScriptを書いていない。楽しい、最高すぎる。

※テンプレートエンジンにはnunjucksを採用

  <form id="create-form" class="inherit-main" hx-post="{{API_BASE_PATH}}{{ ACTION_PATH }}"
          hx-swap="outerHTML" hx-select=".wrapper" hx-target="closest .wrapper">
    <div class="form-field">
      <div>
        <input type="text" id="event-name" name="name" 
                {% if event %}value="{{ event.name }}"{% endif %}
                placeholder="イベント名を入力してね" required="required"/>
      </div>
    </div>
    <div class="form-field">
      <label>候補日を選んでね</label>
      <div class="section-flatpickr">
        <input id="datepicker" name="candidateDates" required
        {% if event %} value="{{ event.candidateDates }}"{% endif %}/>
      </div>
    </div>
    <div class="form-field">
      <div class="event-time-header">
        <label>時間を選んでね</label>
      </div>
      <div class="event-time">
        <div class="base-time-selector">
          <div>
            <input type="time" id="base-time" name="baseTime" value="{{ event.baseTime }}" 
                {% if event.timeByDay %} disabled{% endif %} required/>
          </div>
          <div>
            <input type="checkbox" name="timeByDay" id="time-by-day"{% if event.timeByDay %} checked{% endif %}/>
            <label for="time-by-day">←を基準に日毎に設定</label>
          </div>

        </div>
        <div id="event-time"></div>
      </div>
    </div>
    {% if event %}
      <input type="hidden" name="eventId" value="{{ event.id }}"/>
    {% endif %}
  </form>

  <template id="event-time-template">
    {% raw %}
      <div>
        <ul class="candidate-dates">
          {% for c in candidateDates %}
            <li class="flex-justify-between">
              <time datetime="{{c.key}}">
                {{ c.date }}
              </time>
              <div>
                <input type="time" value="{{c.time}}" name="candidateTimes[{{c.key}}]" required/>
              </div>
            </li>
          {% endfor %}
        </ul>
      </div>
    {% endraw %}
  </template>

{% endblock %}

{% block footer %}
  <button form="create-form" type="submit">
    {%- if event -%}イベント更新{%- else -%}イベント作成{%- endif -%}
    <img src="https://htmx.org/img/bars.svg" class="htmx-indicator" alt="loading..."/>
  </button>
{% endblock %}

HTMXのつらみ

インタラクティブなUIを実装するとなると多少なりともDOM操作せざるを得ず、かなり悩みました。やはりNunjucksじゃなくて、JSXが良かったなぁという反省。

例えば、今回トップページで日付を選ぶと時間を選ぶためのフィールドが動的に生成されるのですが、このようなJavaScriptを書きました。※リファクタは皆無なので、その辺りはご承知おきを

  function parseCandidateDates(dateStr) {
    const isTimeByDay = document.getElementById('time-by-day').checked
    if (!isTimeByDay || !dateStr) {
      document.getElementById('event-time').innerHTML = ''
      return
    }

    const template = document.getElementById('event-time-template').innerHTML

    const data = htmx.values(htmx.find('#create-form'))
    const baseTime = document.getElementById('base-time').value

    const candidateDates = dateStr
      .split(', ')
      .sort((a, b) => new Date(a).getTime() - new Date(b).getTime())
      .map((date) => {
        const time = isTimeByDay
          ? data[`candidateTimes[${date}]`] || baseTime
          : baseTime
        return {
          key: date,
          date: dateFns.format(new Date(date), 'yyyy/M/d'),
          time: time,
        }
      })
    const renderedTemplate = nunjucks.renderString(template, {
      candidateDates,
    })
    document.getElementById('event-time').innerHTML = renderedTemplate
  }

この辺り、HTMXが単体で解決していくのかどうかウォッチしたいと思います。
(リッチなUIやインタラクションを実装する際に、DOM操作地獄になってしまうのは時代の逆戻りなので)

Hyperscriptの感動体験

https://hyperscript.org/

プログラミング的ではなく文章的にフロントエンドの実装ができるライブラリです。
HTMXの開発者(社?)と同じ開発元のようで、ついでに以下のUIを実装してみました。

要件をそのまま文章的に書くように実装が完了した

  • タップしたらクリップボードにフィールドの値が入り、コピーしましたの要素を表示して3秒経ったら非表示にする

これは以下のような記述で実現できました。

<div class="form-field">
    <div class="form-field-header">
      <label>タップしてURLを共有してね</label>
      <div id="copyMessage" class="copy-message">コピーしました</div>
    </div>
    <input type="text" readonly
      _="on load put location.href into me.value
        on click call navigator.clipboard.writeText(me.value) then add .copy-message-show to #copyMessage
        wait 3s then remove .copy-message-show from #copyMessage"/>
  </div>

これにはWOWな体験がありました、フロントエンドはやはり楽しいですね。
他にも色々なことができるようで、今後も使っていきたい

GCP(Firebase)が最高で最悪で最高だった

実は当初サーバーサイドは、SQLに触れる目的でNode.js(Express)+SQLiteの構成をとっていた。
しかし、せっかくのHTMXならサーバレスにしたいなぁみたいな事をぼんやり思っていた。
そこで相談に乗ってくれた優しい知人がアイデアをくれ、以下のサービスを使うことにした。

結論、とても良い開発体験ができたので、知人にはウルトラ莫大感謝です。

良かったこと

無料枠もあって、お金もかからずに、サーバ側のほとんどの事をまかせて、UX設計や、デザイン、フロントの開発に集中することができた。

悪かったこと

ドキュメントがわかりづらい。特にサンプルコードで混乱した。
Node.js版はes6 module版で書かれてたり書かれてなかったり、
それよりも第1世代、第2世代の両方がアクティブで古い情報が残っている?などで
サンプルコードが間違ってるじゃん・・・と思ってしまうことが多々あった。
Perplexity AIなどを使って楽しようと思ったが最新の情報を取ってくれるばかりではないので、
ある程度大量の情報を読まなければならなかったし、沼る時は盛大に沼って苦しいことも多々あった。
が、これだけ巨大なサービス群であるし、これは分かりやすく維持することは不可能だろうな。。。

Cloud Functions

こんな感じに関数をexportしてデプロイするだけで、
ホスティング先のエンドポイントを叩くとレスポンスが返ってくるようになる魔法のようなサービス

イベント作成の部分だけコードが見えるが、HTMXのヘッダが見えると思う。
これはリダイレクト指示の記述で、クライアント側でHTMXライブラリがこのヘッダを検知してリダイレクトを勝手にしてくれるというもの。

最初は概念の理解に苦しんだものの、かなり気持ちいい開発体験ができた。

Firestore Database

今回はログイン機能もなくミニマルな構成を目指し、リレーショナルな構造は一切持たないツリー構造とした

テーブル設計

デザインとCSSの話

Figma Dev Modeが良かった

今回サクッと自分でFigmaでつくったのだが、DevModeで生成されたコードを貼り付けて、CSSを書いていったのだが、思いの外スムーズで良い開発体験だった。

CSSで感じた課題

CSSに関しては、筆者がもっとも思い入れのある言語で
久しぶりにNoライブラリでスクラッチで書いたのだが、色々と課題を感じた。(というか思い出した)

  • Class名考えるのだるい
  • CSSのValidationって未だむずい
  • 手軽にScopedなCSSを書きたい

奇跡的にアイデアが浮かんでHyperstyleを開発するかもしれません

ロゴの制作について

今回突貫だけど、きゃしがロゴをつくってくれました。大感謝、ありがとう!

UX設計について

小さいプロダクトだからロクにドキュメントはつくっておらず、全て脳内シミュレーションで設計した。
ので、なにかフィードバックがあったら気軽にリクエストしてほしいです

今後のロードマップ

  • 動的なOGP生成
    • 追記:実装しました
  • FCMを使ったWeb通知
    • 日程決まった時の通知、入力し忘れリマインダーなど
    • PWAなくなるとだいぶきつい(Apple様なくさないで、、)

この辺り、暇つぶしにやろうかな〜という感じです。

おわりに

せっかくなのでたくさんの人に使ってもらえたら嬉しいです
フィードバックやリクエストは歓迎です、気軽にコメントしてください

https://itsusuru.com

あまりにも使われたらサーバー代がアレだけど、
そんな広まることはないと高を括っていますw

ではまた!

Discussion