ぱいぱいにっき

Pythonが好きすぎるけれど、今からPerlを好きになりますにっき

#japanpm Japan.pm 2021でhotwireをMojoliciousから使うLTをしてきました

久しぶりに勉強会発表して緊張したなということで。

yapcjapan.connpass.com

これに参加&LTしてきました。

参加の感想

  • 型や(静的|動的)解析に関連する話が多く、Perl型マニアとしてはとても満足するトークばかりでした。

scrapbox.io

Perlでも関数の型をチェックしたい - Speaker Deck

scrapbox.io

  • データ解析ツールを作った話やAWS CDKの解説、InnoDBクラスタの仕組みなど、実世界に寄った話も盛り上がり、裏トークも合わせて聞くと深く知見を得られた気がしてよかったです

  • オフラインにあって、オンラインカンファレンスではなかなか成立しにくいものとして、セッションの間の時間での廊下での交流や、会の後の懇親会がありますが、それに似たような体験を得るために会の後に、交流会と称してDiscord内にボイスチャットグループを話題別にたくさんつくって解決していて、かなり面白かったです

  • 交流会に「型」と名付けられたルームがあったので思い切って飛び込んでみたのですが、型や静的解析関連の登壇をされた方や興味がある人でPerlの型チェックにまつわる悩みトークが出来てよかったです。

blog.sushi.money

型チャンネルの話題は参加されていたid:hitode909さんの記事のも言及があります。

発表

speakerdeck.com

Mojolicioushotwireを使うトークをしました。というかhotwireっていうのがあって、それがこう言う事ができて、しかもフレームワーク依存はないですみたいなのを伝えたかった感じです。

スライド中のコードはこちらに上げています。動かす手順なども書いたので、興味がある方はぜひ。

sandbox/turbo/example1 at master · mackee/sandbox · GitHub

個人的な反省

  • 速く終わりすぎた。多分速く終わりすぎていたと思う(計測していない)
    • 手元にストップウォッチを置くの忘れていた
    • 通しの練習が不足していた
    • このへんは終わらないので事前に説明を飛ばすと決めていたところを予定通り飛ばしたが、時間を手元に用意しておけば飛ばさない判断もできていたと思う
  • hotwire/turboがなんであるか(そもそもJSのライブラリであるよとか)そういう前提の話をすっ飛ばしてしまったなと、スライド見返していて思った
    • 20分40分のトークだと、この辺の前提条件の共有みたいなのを、僕はかなり念入りにやるのだが。というのもWebエンジニアのカンファレンスでハードウェアの話をするなど聴者の専門分野と違う話をすることが多いので。ただ久しぶり&LTというのもありすっぽ抜けていた
  • 今回は高度な話題はなくして中級者レベルの話題を目指したが、そういう人たちにとって重要な「これをやって何が嬉しいか」みたいな部分の説明が不足していた
  • HTMLぶん投げて書き換えているみたいな当たりの面白さの説明は、「面白さが分かる人向け」なので果たして入れるべきかどうか、しかし演者が面白いと思うことをいうのが重要だなとは思う

hotwireについての感想

主にフロントエンドエンジニアやサーバ含めて全部やるエンジニアからhotwireという技術が広まることについての懸念がインターネット上で話されている。というのも、turboやstimulusという技術は現在のWebフロントエンド技術スタックからは連続していない技術であり、どちらかといえばRails/Django/Laravel的なHTTPリクエストを受けるとレスポンスとしてHTMLを返す技術の延長線上に存在しているものだからだと思う。これらは技術の相互運用が難しい。turboの前身であるturbolinksでも同種のハレーションが起こっていた。その時の記憶を思い出す人も多いのだと思う。

昨今のいわゆる"モダン"と呼ばれるWeb開発の環境は、JavaScriptによって駆動するブラウザ上で動くGUIアプリケーションに対して、サーバはHTMLではなくJSONやその他シリアライズフォーマットで返すものが指すものが多い。僕も仕事ではそういう環境でAPIサーバを書いている。もっというとBFFと呼ばれるアーキテクチャを採用すると、フロントエンドリソースの配信やキャッシュ、複数のAPIサーバからのレスポンスを集約するなど、フロントエンドのためにサーバサイドでしかかつて出来なかったことを、フロントエンド技術の延長線上で行うことが提唱されている。ここまでくると、サーバサイドとしてはWebアプリケーションを書いていると言うより、domain specificなデータベースのプログラムを書いている感覚に近くなってくる。そして、APIをしゃべる汎用的なデータベースでよいのであれば、そもそもmBaaSでよいということになり、FirebaseやAppSyncといったサービス、GraphQLとhasuraのようなAPIサーバを構築してくれる技術などが発達していく。

おそらく、僕の予測では、この流れは一般的な消費者がアクセスするようなWebサービスでは止まらないと思う。一方で、そこまでみんなGUI書くのが好きかと言われたらそうではないし、限られた組織向けのWebアプリケーションに、ユーザ体験が良くすることが得意なフロントエンドエンジニアを充てられるかどうかと言えば、今はノーであるという現場が多いのではないのだろうか。ここで指している"限られた組織向けのWebアプリケーション"というのは、社内向け管理画面だとか、勤怠管理システムだとか、在庫管理システムだとか、一般のお客さんからは見えないところで動いているサービスのことを指している。

とはいえ、体験が悪い勤怠管理システムに悩まされている業界の人は結構いるようで、僕もかつてはその一人であったけれども、やはり優れたGUIというのは充てられる人がいるいないに限らず、必要なものだと思う。しかし、優れたデータベースを作れる人が必ずしも優れたGUIを作れるかと言われたらそうではないし、いても超人の類なので、片手間でも優れたGUIを作れる機構というのは、選択肢として悪くないと思う。優れたGUIを作るのには技術だけではなく、作ろうとするシステムに対する深い理解も必要だし、他の例を引用するなどの労力も必要である。現代のフロントエンドスタックは 、それらとデータベースの操作をきちんとやる作業の片手間にやれるほど、お手軽ではないと僕は感じている。なので、hotwireを管理画面に採用するのを同僚に提案してみようと思う。フロントエンドの人には、僕ら以外の一般ユーザの体験を向上するのに注力したほうがビジネス的には正解だと思うからだ。

ただ、hotwireを剥がして一般的なフロントエンドスタックに載せ替えますといったときに、フロントエンドエンジニアの呪詛が激しいと思われるので、採用するなら採用したときのメンバーで閉じて作り続けていく覚悟が必要なんだと思う。ただ、turboを剥がしても普通のMPAとして作動するように作れとドキュメントにも書いてあるので、turbo特有の挙動に頼ったUIを作らなければ、そこまで移行は難しくない、そもそもMPAのアプリケーションをSPAにするのと同じぐらいの労力にとどまるのでは、とは思う。ただ、MPA -> SPAも難しく、というか静的なドキュメントをGUIアプリケーションという別物に作り変えるようなものなので、そもそもページの再設計が必要。

turboをMojoで使うときに困ったときのtips集

onchange=this.form.submit()でfetch requestが発火しない

this.form.submit()をすると、普通のHTMLであれば、submit buttonが押されたのと同じ動作、つまりフォーム送信が行われる。しかし、turboが導入された環境だとsubmit buttonが押されたらページ遷移せずにfetchが使われてリクエストし、レスポンスに応じてturbo driveやturbo framesによるDOM要素の書き換えが行われる。

ただ、今回のデモアプリで示した、トグル状態が変更したら送信するようなときに、素のJSだとタグ内にonchange="this.form.submit()"と書いてしまうのがお手軽だが、これだとturboの環境でもフォーム送信&ページ遷移が行われてしまう。

ググったところ以下のフォーラムのスレッドが見つかり、このスレッドではstimulusでrequestSubmit()というメソッドを呼び出していたが、普通に書いても使えそうだったので、onchange="this.form.requestSubmit()"と書いたところ、ちゃんとfetchのほうが発火したので、そのようにした。しかし今書いてて思ったのだが、turbo無し環境だとこれ動くのか?と思った。

Triggering Turbo Frame with JS - #23 by walterdavis - Hotwire Discussion

turbo streamsでメッセージを投げつけるときの形式

どうやるんやろと思って、turbo-rails gemを呼んでみたのだが、railsにはそんなに詳しくなくて、ぼんやりしかわからなかったので、とりあえず分かる言語でまず調べるか思ったところ、以下のGoでturbo streamsを使う記事が出てきた。

Turbo Streams powered by Go WebSockets - DEV Community

この記事で上げているリポジトリには、

{ 
  "identifier": 
     "{\"channel\":\"Turbo::StreamsChannel\",  \"signed_stream_name\":\"**mysignature**\"}",
  "message":
    "<turbo-stream action='append' target='board'>
    <template>
        <p>My new Message</p>
    </template>
     </turbo-stream>"
}

という形でWebSocketにメッセージを流しているっぽいので、これをそのまま採用したところちゃんと動作したのでそのまま採用している。しかし、messageキーの部分はわかるが、identifierの部分は何なのかわかってない。ActionCableとかそっち方面由来なんですかね?消しても動くのではと密かに思っている。

...

思ってるんなら試せばええやんと思って試したところ、普通に動いたので、message部だけで良さそう。

件のリポジトリのHTMLにはこういう記述もあって、

<!--
  <turbo-cable-stream-source 
    channel="Turbo::StreamsChannel" 
    signed-stream-name="**mysignature**"
    >
  </turbo-cable-stream-source>
-->

たぶんチャンネルとかシグネチャを設定できるんだと思う。これに対応するturbo-rails gemの実装はここだと思う。

turbo-rails/cable_stream_source_element.js at main · hotwired/turbo-rails · GitHub

これ、turboじゃなくてturbo-railsの方なので、turbo単体で動くのか?という気持ちがある。過去のバージョンだと動いていたのかもしれない。

turbo-streamsのmessageの文字化けだとかタグがエスケープされる

どちらかというとPerlあるあるなんだけれども、utf8フラグがあべこべの状態でながすとどうこうというやつである。最終的には、

my $rendered = $c->render_to_string(template => "append_messages");
$connection->send(decode_utf8(encode_json({
    identifier => encode_json({ channel => "Turbo::StreamsChannel", signed_stream_name => "**mysignature**" }),
    message    => $rendered->to_string =~ s/\n//gr,
})));

こんな感じのへんてこりんになってしまった。この中には様々なトラブルシューティングの跡が詰まっている。

まず、MojoのWebSocketの接続オブジェクト(ここでいう$connection)は、Mojo::Transaction::WebSocketなのだが、これにもちゃんとJSONエンコード機能はついている。それを使うと上のコードはこのようになる。

$connection->send({ json => {
    identifier => encode_json({ channel => "Turbo::StreamsChannel", signed_stream_name => "**mysignature**" }),
    message    => $rendered,
}});

render_to_string使ってレンダリングしたやつも、to_stringと言いながら実はMojo::ByteStreamというオブジェクトなので、そのままJSON::XS::encode_jsonに突っ込んでも、シリアライズ出来ませんと怒ってくる。しかし、$connection->sendの機能でJSONエンコードした場合は、Mojo::JSONが使われるので、こいつはMojo::ByteStreamを食べられるので、そのままエンコード可能だ。

しかし、Mojo::JSONには普段は有用だがこの場合にはおせっかいな機能があり、

f:id:mackee_w:20210220143554p:plain

ドキュメントの記述なのだが、つまりXSS対策のためにHTMLタグをエスケープする。しかし、今回は実際にレンダリングするためのHTMLを送っているので、エスケープはされてほしくない。というわけで、Mojo::JSONを使わずにJSON::XSを使っている。

それから、s/\n//grとしているのは、改行コードが入っているとそれがそのまま\nと表示されてしまうからである。これ思ったが、turbo-rails側はどうしているか見に行けばよかったなと思った。この記事も長くなってきたので、今は調べないが、ちゃんとした解決方法を教えてほしい。

あとは、全体をdecode_utf8することである。JSON::XSにもutf8フラグを立てたり立てなかったりする機能があるのだが、それだとうまくいかなかった。なんでこれでうまくいって、JSON::XSでうまくいかないかは分かっていない。こんなのでPerlで仕事している。仕事でもあまり深く考えずにdecode_utf8 encode_utf8を試して文字化けしなかったら採用みたいなことをやっている。utf8フラグがどうのこうのあるが、こういうのは変数を受ける側がどう言う状態を想定しているかで挙動が違うので、知識よりはライブラリ側のコード読むか実際に試したほうが良いみたいな悪い学習をしている。

WebSocketのコネクション維持と再接続

turbo側が切っているのか、それともMojo側が切っているのかは知らないが、Webインスペクタを眺めていると素のnew WebSocketだと無通信が30秒で切れるような挙動を起こしていた。このへんは昔にWebSocket接続管理ミドルウェアを作ったときの知識から引用して、5秒おきにping messageを投げるようにした。そうすると切れなくなった。人生そういうものである。

my $id;
$id = Mojo::IOLoop->recurring(5 => sub ($loop) {
    if (!defined $c->tx || $c->tx->is_finished) {
        $loop->remove($id);
        return;
    }
    $c->send([1, 0, 0, 0, WS_PING, 'Hello World!']);
});

このとき、接続が切れている(is_finished)などのケースでは、繰り返しのタイマーを削除するなどしている。ちなみにMojoはイベントドリブンで動くので、こう言う芸当が可能なのである。ノリもJavaScriptに近い。

しかし、それでもアプリケーション再起動とかのときに接続が切れて切れっぱなしで不便というのもあったので、ReconnectiongWebSocketを使っている。これと同名でおそらく機能が同じのライブラリもあり、こちらはメンテされている雰囲気でなんだかモダンだし、普通ならこっちを選ぶんだが、cdnjsから配信されているからという理由で、前者を選択した。とにかくJavaScriptのビルド環境を用意したくないという気持ちが全面に現れている。

まとめ

Japan.pmまたやってほしい。というか** Weekly Talksみたいな感じで、週1とにかく集まるみたいなのもありでは無いか。それほど技術的な雑談に飢えている。特に会社外の人の意見を吸いたい。

あと技術は触ってみると印象が変わることがあるぞい