turbolinksで実現したかったことって、こういうことだったのか

Turbo: The speed of a single-page web application without having to write any JavaScript.

Stimulus ほどあれこれ考察はしていないけど、Turbo を軽く触ってみて結構よさげという印象を受けた。Google Analytics などの 3rd party のツールとの組み合わせの部分には注意が必要になるが、特に初期実装ではかなりの威力になりそうだ。

※ 今回は Turbo Stream は試していない。あくまで Turbo Drive と Turbo Frame の話。

Stimulusはturbolinksに欠けていたもの

まず あーありがち - Stimulus、悪くない の方を読んでほしいんだけど、Turbo は turbolinks が洗練されて戻ってきたものであることから始めると導入しやすいと思う。

細かく引用はしないが、turbolinks 登場時、最も困ったのは当時全盛と言ってよい jQuery を利用したコードと極めて相性が悪かった点であった。なんなら Rails 標準の JavaScript のライブラリが jQuery なのにどうなってんだこれは、という話である。

どういうことかというと、turbolinks を有効にしてしまうと jQuery を利用した以下のような典型的なコード

jQuery(function() {
  ..
})

が、初回のページロード時には動くが、ページ遷移やブラウザの「戻る」ボタンを押した際には動作しない、という問題があったのだ。というか今もこれはある。jQuery をやめて

document.AddEventListener('DOMContentLoaded', () => {
  ..
})

にしても問題は解決しない。なにせ turblinks も Turbo の Turbo Drive も

ページ遷移自体はせず、DOMの一部とHistoryへの変更であたかも遷移したフリをする

ものだからだ。

turbolinks や Turbo のために監視するイベントを増やしたりする方法もあるかもしれないが、それなら今なら潔く VDOM 系のツールで SPA を組む方が賛同を得られやすいだろう。ただし開発コストは大きく膨らむが。

しかし、Stimulus を使うことでこれは解消される。Stimulus ではこの部分は Application.start() の中に隠蔽されていて、DOM がゼロベースで構築完了したかバックグラウンドで必要な HTML を取得して一部の書き換えを行ったかに関わらず Controller の connect() メソッドは呼ばれる。

つまり、Stimulus を使うことでページの読み込みを監視する処理をそもそも自分で書く必要がなくなり、それによって Turbo を利用しているかいないかに関わらず安定して同じように動作するのだ。

これは非常に助かる。これまで turbolinks を off にするのは割とよくある話だったと思うが、フロントエンドエンジニアを多く確保しなくても Turbo と Stimulus を組み合わせることで、よくある多くのパターンの動的な DOM 構築に対応でき、明らかに体感速度は向上する。

実際にブラウザの Developer Tools で見てみると HTML を取得するたった一つの connection で、JavaScript も Stylesheet も読み込み直すことなくシュッと遷移するのは実に快感だ。

方法は簡単。例えば Webpack や Vite などの bundle ツールを利用していたら import するだけである。それだけで有効になり、ページ遷移は爆速になる。具体的には

import * as Turbo from '@hotwired/turbo'

かな。これだけでよい。使っていなくても大丈夫。CDN から script タグで読み込むだけで動く。

Turbo Frameはiframeを使わないiframeのような何か

これも非常によくあると思うのだが、HTMLの中の一部のコンテンツを遅延ロードしたいという場合、以下のように書くと実現できる。

読み込む側 /importer

<turbo-frame id="export" src="/exporter">
</turbo-frame>

読み込まれる側 /exporter

<turbo-frame id="export">
  ..
</turbo-frame>

これで importer 側の <turbo-frame> の中身が exporter 側の <turbo-frame> の中身に挿し変わる。

id でコンテンツを紐付けて対応する DOM を入れ替えてしまうという方法である。

同じことを JavaScript で実現する側として正解を挙げると、従来なら

  • サーバサイドで JSON を出力
  • クライアントサイドで JSON を取得
  • クライアントサイドで(サーバサイドとは異なる記法で)DOM を構築

という3ステップを要する。

しかし Turbo Frame を利用すれば全部端折ってサーバサイドで普通に HTML を書いて別な entry point を与えればよい。bot 避けに meta でも書いておけば変なお漏らしもないだろう。これだけで済む。

もちろん、そもそもコンテンツをどのように見せるかは import する側の責務なのでは? という指摘はあると思う。自分もそう思う。でもじゃあそのためにフロントエンドのツールでゼロから書きますか、というと「なんだかなぁ」という気がしないだろうか。DOM の書き換えだってせいぜい最初の一回しか起きないのに VirtualDOM とか必要ですか?という気持ちになるし、今書いているバックエンドのコードから完全に頭を切り変えてフロントエンドの海に潜らないといけない。

Turbo を使うとそこはずっとサーバというか HTML のままで実現できるので、スイッチングコストが発生しない。しかも Turbo Drive でのページ遷移と組み合わせると、遅延読み込みしたかどうかも人間にはよく分からない。

まとめ - Stimulus + Turboはよいぞ

以上のような形で

  • サーバ側の routing (あるいは単なる HTML のパス)で
  • サーバ側の HTML の生成で
  • JavaScript をいちいち書かずに

ユーザーの体験を大きく向上させるのが Turbo の狙いだ。

そしてより細かい部分や 3rd party のツールと組み合わせる際には Turbo 固有の event を利用すればよい。

Hotwire という名前や DHH という名前で大袈裟に構えることはない。なんてことのない、だが確実によく利く勘所を押さえたツールと言えそうだ。

フロントエンドのツールの理解、VirtualDOM や reactive という考え方はそれはそれで必要になるし、例えばタイマーのようなより細粒度、高頻度の更新が必要なものは JavaScript で閉じた方がよいと思うが、ほとんどサーバ側が正解の情報(HTMLなど)を持っているのに JavaScript から遠回りして DOM に反映しているケースも実は多い。

そういう場合に .js.erb のように無理やり JavaScript に情報を渡すのではなく、もうそのまま HTML を渡せばいいじゃんという考え方は一見大胆だし、細かくベンチマークを測るとそこまで速くもないかもしれないけど、そこそこ十分に速くて開発コストが大きく抑えられるのもまた事実。混ぜて使う際にはやや注意が必要かもしれないが、少なくとも初手でコストを掛けずにユーザーの体験をよくするためには大きく貢献してくれそうだ。

※ テストが難しそうというかエラーハンドリング周りの情報が全然ない1状態なのは確かに気になるので、そこは追試が必要。あと HTML を「直接」「普通に」取得するのでサーバサイドや昨今のフロントエンドを書く際に忘れていたキャッシュ周りに気をつけないとハマる可能性もある。あと地味にレビューが難しい。Stimulus も Turbo も生々しく自分で実装する量がグッと減るので、テストコードも恐らくかなり少なくなる代わりに、HTML 側の記述ミス防止は恐らく目視チェックになりやすいので、何か工夫があった方がよさそう。

  1. 普通に console にエラーが出るんだけど、その情報が十分に出揃ってない。 

意図通りのURLにユーザーを誘導したい

HTMLでは

<link rel="canonical">

を書いてあげればよい。

JavaScriptでは

location.host

を見て意図通りの URL でなかったら location を書き換えてあげればよい。

サーバ側アプリでは

基本的には HTTP_HOST を見る。ところが構成によってここで取得できる値は変わる。1

例えば Heroku の場合はもともと *.herokuapp.com の名前を持っている。これに DNS の ANAME や CNAME で hostname を設定し、これをユーザーに見せる URL として設定することになるのだが、ここに CDN が加わると設定方法にバリエーションができる。

※ 以下はアプリケーションサーバが Heroku にある前提で書いていくが、別にどんなサーバでも構わない。単にデフォルトの名前が分かりやすく決まっているので参考に挙げやすいだけである。

a) CDN側にだけDNSを設定する

  1. Heroku 側では別な endpoint を定義せず
  2. CDN 側で endpoint を定義する(例えば example.com で Origin を *.herokuapp.com に)

形になっている場合、当然だが Heroku 側で拾える HTTP_HOST はデフォルトのものだけになる。

+-----+
| DNS |
+-----+
   ↓
+-----+    +-----+
| CDN | → | App |
+-----+    +-----+

App で拾えるのはデフォルトの *.herokuapp.com だけ。なぜなから CDN から *.herokuapp.com でアクセスされるから。

これの対処は後述。

b) CDNにもAppにもDNSを設定する

      +-----+
      | DNS |
      +-----+
         ↓
   +----------+
  ↓          ↓
+-----+    +-----+
| CDN | → | App |
+-----+    +-----+

App の endpoint も DNS で設定して

  • example.com ( Heroku )
  • cdn.example.com ( CDN )

のように設定できるなら App 側で Canonical(この場合は *.herokuapp.com ではなく example.com)かどうかを HTTP_HOST で取得することができる。

CDN経由かどうか

ということで今度は CDN 経由かどうかを知りたい場合。上の a) のパターン。

  • Via ヘッダを見れば経由サーバがあるかどうかは分かる
  • Heroku のようなマルチテナント PaaS や Load Balancer 経由のアクセスの場合は常に Via 値があるので、意図したサーバを経由したかどうかは「Viaヘッダがあるかどうか」だけでは判別できない
  • CDN や Load Balancer 経由かどうかは「Via ヘッダに入る文字列」での判別になる
    • 多段の場合は複数の情報が文字列で入る

ということで例えば Rack であれば

env['HTTP_VIA'].include?(特徴的な文字列)

で判別できる。

  1. Rails 4 までだと直接 HTTP_HOST を見ずに request.host を使えばいいと思う。5 以降はこのメソッドなくなったそうなんだけど、使ってないので分からない。 

FasterCSV::Tableでヘッダが取得できるけど空セルに注意

罠っていうかたぶん FasterCSV の理解が足りないだけなんだけど。

FasterCSV::Table なら header を取得できる

Rails の seed data を取り込む際に

FasterCSV.open( PATH, { :headers           => true,
                        :header_converters => :symbol } ) { |csv|
  ...
}

みたいにするコードはよく見かけると思うんだけど、open だと header を取得することができない。

irb(main):001:0> f = FasterCSV.open( '/path/to/csv', :headers => true )
<CSV.open( '/path/to/csv', :headers => true )
=> <#FasterCSV io_type:File io_path:"/path/to/csv" lineno:0 \
col_sep:"," row_sep:"\n" quote_char:"\"" headers:true>
irb(main):002:0> f.headers
f.headers
NoMethodError: undefined method `headers' for #<FasterCSV:0x143f084>
	from (irb):1

リファレンスを眺めていたら FasterCSV::Table だとできるらしい。

irb(main):003:0> g = FasterCSV.table( '/path/to/csv', :headers => true )
<CSV.table( '/path/to/csv, :headers => true )
=> #<FasterCSV::Table mode:col_or_row row_count:8>
irb(main):004:0> g.headers
g.headers
=> [:name, :code]

おぉ、取れた。

FasterCSV::Table のデフォルトでは空セルが 0 になる

変更方法があるのかどうか分からないけど、

FasterCSV.table( '/path/to/csv', :headers => true ) { |csv|
  ...
}

ってそのままやると空セルが 0 になっちゃう。:skip_blanks っていう option が default false で存在してるので true にしてみたけど変わらず。

仕方ないので中身を取得するのに普通に open で開き直すことにした。

なんか納得いかない。

Capistrano founder burnt out

なんかもう書くことない。(実際書いてるのは5月末だし)

同時にいくつも作りすぎてる気もするね。でも github のおかげもあって fork も楽だし、いい時代だ。

Ruby の画像ライブラリはまだ何かと面倒

ruby-gd
ドキュメントがろくすっぽない。なぜか new_from_jpeg 動かず。
RMagick
バージョン2 で ImageMagick 6.3.0 以上を要求するが Debian etch も CentOS 5 もそれ以前のパッケージしか対応してない。
ImageScience
恐らく遠くない将来の本命なんだけど、OS と連動したパッケージが対応するのはまだ先の話。今のところ freeimage 対応の安定感とパッケージへの採用度は今ひとつな感じ。
mini_magick
ImageMagick 実行バイナリを呼ぶだけ。

結局 mini_magick を使うことにした。

なんかどれもちょっとまだ早い印象。

svk って Panther に入らないなぁ

[2006-08-01 追記] いつの間にか svn も svk も、fdclone も mccc も Rails も Fink に入ってる!

  • svn はバイナリがあるんだけど、svk はバイナリがない。
    • Tiger でしかまともに動かないっぽい。
  • Fink でやってもエラーが消えない。
    • 達人はなんとかできるんかもしれんけど、分かりまへん。
  • よく考えたら svk は Perl の svn-mirror モジュールを使ってるだけだし、svn そのものに mirroring と同等に扱える機能があるんじゃないか?

つーことでもうちょっと svn そのものを勉強せないかんちゅーことか。ruby-svn でなんとかならんかと思ったけど、Fink にも gems にもなかったんで結局手作業の予感。

※ Tiger にすれば解決する問題でもあるんだけど、上げられない機械があると思いねぇ。

Putty 0.57 周り

2005-02-25 10:30 確認

PuTTY で ISO 2022 による日本語入力・表示を可能にするパッチ対応済み
PuTTY β 0.57 ごった煮版対応済み
PuTTY 0.56 を INIファイル対応にするパッチ
WinSCP日本語サイト
WinSCP本家

WinSCP のサイトがなんか見にくくなったっつーか、What's New が全然書いてないのは前からこんなもんだっけ。Wiki とか採用してデザインもがらっと変わってるのはいいんだけど、書くもん書いて。頼むし。今回はとりあえず

Part of the code of this software comes from program PuTTY 0.56 (C) 1997-2005 Simon Tatham. License agreements for using PuTTY, are part of WinSCP license agreement.

を信じて「未」と判断。

オープンソースって思想なんだ?

ちょっと見ない間にMatz日記がすごいことになっていた。

個人的にはまつもとさんの書いていることにこれと言って疑問は感じないんだけど、世間では妙な誤解というか偏見があるんだなぁという認識を新たにした次第でして。

なんか一つには「オープンソース」という言葉を取られた、みたいな認識があるみたいに読めますな。

オープンソースという言葉が各種メディアに出始めたときに、「あぁこんなことかな」と思っていたことが、後から出てきた(実際には「あとから知った」のはず)OSD だのなんだの「やたらと一生懸命な人たち」によって「その解釈は定義に合致しない」と指摘され、気軽に使えなくなってしまった、つまり奪われてしまった、という感じ。

しかも知れば知るほど「あのライセンスもこのライセンスもオープンソースです」てなことになってるじゃないか。なんだそのおいしいとこどりって言うか、えぇかっこしいっていうか、単に目立ちたいだけ?みたいな「なんだかこいつらハナにつくぞ」的な感情。

もちろんこれは自分勝手な想像以上の何ものでもないんだけど、なんとなくそういう風潮ってあるだろうなと思う。例えば Linux や Apache は世界的にすごいらしいってのは事前の知識としてあったとしても、逆にその世界的に有名なソフトウェアの名前を利用している妙なやつら、みたいな捉え方って十分あり得ると思う。

それだけ固有のモノに対する名前は強いし、そうでない言葉は弱いものなのではないかと。

「日本の」「オープンソース運動家」はそういう逆風を受けているってことなんじゃないかなぁ。海外の様子は知らないし、オープンソース「開発者」はまた違うと思うけど。「運動」って言葉はよくなかったなぁ〜。やっぱ日本で「運動」って、なんだかうさんくさい感じするもの。なんか中身がなくて題目だけ立派な感じするもの。

FreshPorts

FreshPorts をチェックするようにした。ports がものすごい勢いでどっかどっかアップデートされていく様子がよく分かって楽しい。思わぬ発見もある。FDclone と openssh が今日上がっていた。サイトを確認すると本家の openssh-portable が上がったのが昨日。早い。スラドのセキュリティ部門に頼らずそろそろ自分で情報集めるようにせんとなぁ。

映像と声

松本被告の判決が近づき、各局で特番を組んでいる。

圧巻は NHK だった。手紙を基本とした声と顔写真とほとんど静止画のような「場所」の映像を中心に構成されたものだが、他のどの局よりも事件の内実に迫っていたように思う。そこには局側の市民への煽動的な要素はほとんどなく、だからこそ恐ろしいほど自分の中に入り込んでくる。自分以外の人間がどう感じるかは分からないが、正直、これがテレビの力かと感服した。クオリティの高い映像と声の持つ力。表現がシンプルだからこそ内容が研ぎ澄まされ、受け手との距離を縮める。自分の中の思考の歯車がやたらとスムーズに回り、様々な思いが去来する。

About

例によって個人のなんちゃらです