- CRDTチュートリアルのインタラクティブデモを長く残るコンテンツとして扱うには、特定のフレームワークよりもHTML内に直接埋め込めるWeb Componentsのほうが適している
- AstroのMDXコンポーネントはビルド、CSSスコープ、TypeScriptなどの利便性を提供するが、Astroの外へ移すと書き直しコストが発生する可能性がある
- Web Componentsは
<pixelart-demo></pixelart-demo>のようなカスタムタグをMarkdownに入れ、スクリプトを読み込むだけでページのどこでもタグベースで再利用できる
- フレームワークやTypeScriptも依存関係であるため、バージョン・API・互換性の変化が積み重なり、長く放置したコードほど依存関係が多いほど復旧が難しくなる
- Webプラットフォームは古いサイトが現代のブラウザでも動作するほど安定しており、5年・10年・20年後にもアクセス可能な成果物には標準ベースのアプローチが有利である
インタラクティブデモをコンテンツとして保存する
- CRDTブログシリーズには概念を示すインタラクティブデモが必要で、例として共同編集のピクセルアートエディタが使われた
- 技術選定の基準は、デモがHTML、CSS、JSで作られていても、ブログ記事の中に含まれるコンテンツに近いという点だった
- 画像や動画のように記事の中へ差し込む対象に近い
- HTMLをレンダリングできる場所ならどこへでも移せるべきである
- サイトは2023年時点でAstroで作られていたが、それ以前には自作の静的サイトジェネレーター、Hugo、PHP製カスタムCMS、Tumblr、Movable Type、WordPressなどを経てきた
- いまAstroを気に入っていても、Webサイトが永遠にAstro上で動くと仮定するのは難しい
Markdownが移行コストを下げた方法
- 最近の移行が容易だった理由は、コンテンツをMarkdownのプレーンテキストファイルとして維持していたからである
- システムを移行するときによく必要になる作業が減る
- あるシステムからエクスポートし、別のシステムへインポートする
- 互換性の問題を修正する
- 移行方法が見つからなかった要素を削除する
- Markdownファイルを新しいWebサイトに入れれば、たいていそのまま動作する
- MarkdownにはHTMLを直接書けるため、複雑なインタラクティブ図でも通常のHTMLタグで表現できるなら、他のコンテンツと同じように移植できる
Astro MDXの利便性と移植性の限界
- AstroのMDX統合により、Markdown内でAstroコンポーネントをレンダリングできる
- Astroコンポーネントはビルドシステムのさまざまな利便性を活用する
- 1つのファイル内にHTML、CSS、JSを書く
- CSSセレクタのスコープ処理
- TypeScriptのコンパイル
- 条件付きマークアップのレンダリング
- その他の最適化
- 問題は、これらの利便性がすべてAstro内部でのみ安定して動作する点にある
- 別のサイトジェネレーターへ移行すると、コンポーネントを書き直さなければならない可能性がある
- HTML、CSS、JSを分離する必要があるかもしれない
- 新しいビルドシステムの構成が必要になるかもしれない
- スタイルのスコープ方法を新たに探す必要があるかもしれない
- そのためAstro専用機能は便利でも、長期保存が必要なデモには向かない選択となる
Web Componentsが解決した点
- Web Componentsは、再利用可能なHTML要素を作るためのWeb標準技術の集合である
- 使い方は、カスタム要素クラスを書き、タグ名を登録し、そのタグをマークアップ内で使うという形になる
- ピクセルアートデモはMarkdown内に次のHTMLだけを書いて埋め込まれている
<pixelart-demo></pixelart-demo>
- 別途特別な設定や、特定の要素を先に配置してから関数を呼び出す手順は不要である
- JSファイルは保持して
<script>タグで読み込む必要があるが、テキストコンテンツの中からメディアを参照する方式が必要という点では他のメディアと同じである
- スクリプトが読み込まれるとタグ名が登録され、JavaScript実行前にマークアップがすでに存在していてもページのどこでも動作する
単一ファイル構成とShadow DOM
- Web ComponentsはHTML、CSS、JSを1つのファイルにカプセル化でき、別個のビルドシステムを必要としない
- コンポーネントコードが1か所にまとまっていると認知負荷が下がり、単一ファイルコンポーネントは開発体験の面でも利点がある
- 例の
<pixelart-demo>コンポーネントはHTMLElementを継承し、attachShadow({ mode: "closed" })でShadow DOMを付与したうえで内部マークアップとスタイルをレンダリングする
- ファイル先頭の
import PixelEditor from "./PixelEditor.js";はES module importであり、別個のビルドシステムに依存しない
- 関連ファイルを一緒に保持しておけば、ブラウザがモジュールを処理する
- Shadow DOMはコンポーネントを周囲のページから隔離する
- アプリ全体とスタイルを共有したい場合には不自然なことがある
- 完全に隔離される必要がある場合には適している
- 画像や動画のように、どこで使っても同じ見た目と動作を保てる
属性で外部設定を受け取る
- Web Componentsは外部から設定できる属性(attribute) を公開できる
- 属性はネイティブ属性と同じように使える
- CSS変数はShadow DOMの中へ入れられる数少ない値の1つなので、例のrange sliderは次のようにアクセント色を設定する
<range-slider style="--accent: #0085F2"></range-slider>
- ピクセルアートデモは属性で解像度を20に変更し、各ピクセルのデバッグ情報を表示する
<pixelart-demo debug resolution="20"></pixelart-demo>
- コンポーネントクラスの
getAttributeとhasAttribute呼び出しは、こうした属性値を読むために使われる
- 同じコンポーネントをチュートリアルの複数段階で再利用しつつ、進行段階に応じて特定機能を有効にするのに役立った
フレームワークとTypeScriptも依存関係である
- Lit、Stencil、SvelteなどWeb Componentsへコンパイルされるツールもあるが、フレームワークは結局依存関係である
- ここで最も懸念していたトレードオフは長期的なメンテナンスコストである
- TypeScriptもWebのネイティブ基盤ではないため、依存関係に含まれる
- 直近15回のTypeScriptバージョンにはbreaking changeがあり、その多くは有用な新機能だったがコード修正が必要だった
- 依存関係を使うと、新バージョンのリリース、API変更、互換性確認のコストが伴う
- このコードは継続的に手を入れる予定がないため、後になってどうしても修正が必要になったときに、何年分もの更新をまとめて処理したくはないという状況である
古いWebサイト復旧の経験とWebプラットフォームの安定性
- オンライン博物館を作る中で、10年間電源を入れていなかったノートPCに保存されていた古いコードを復旧したが、依存関係の多いWebサイトほど蘇らせるのが難しかった
- 約20年間Webを作る中で、jQueryの誕生、成長、衰退を見てきており、Node.jsの誕生とio.jsへのフォーク、その後の再統合も経験した
- BackboneはAngularJSへ、AngularJSはReactへと急速に置き換えられ、React自身もその半分ほどの期間で複数のコンポーネント記述スタイルを経てきた
- エコシステムが変わる一方で、Webプラットフォーム自体は既存のWebサイトを壊さないよう慎重に変更され、かなり安定した状態を保ってきた
- 古いサイトも現代のブラウザでいまなお動作する
- 5年後、10年後、20年後にもアクセス可能な成果物を望むなら、制御できない依存関係を減らし、壊れにくいWeb標準に寄せるほうが適している
- Webには欠点があっても、中間レイヤーなしにその特性を踏まえて作ることで、回復力があり、移植性が高く、将来志向のコンピューティングプラットフォームになる
1件のコメント
Hacker News のコメント
htmx はフレームワークではなく ライブラリであり、記事でも言及されていたので、いくつか答えたい
まず、この記事はサーバーと状態を同期する問題を扱っていないが、htmx はハイパーメディア交換を通じてまさにその問題に焦点を当てている。なので、この観点では htmx と Web Components は互いに直交する関係にある
依存関係には新バージョン、API 変更、互換性維持コストが伴うため、htmx には依存関係がなく、後方互換性に非常に注力している。たとえば IE11 と互換性があり、htmx 1.0 に相当する intercooler.js は jQuery への依存が1つある形で2013年に登場したが、今でもサポート中だ
多くの JavaScript ライブラリには API の書き直しや難しいアップグレードの文化があるが、htmx は明示的にそうではなく、そうした一般化でひとくくりにされるべきではない。htmx 2.0 の唯一の破壊的変更は、おそらく IE サポートの打ち切り程度だろう
Web Components が htmx より長く残るかどうかは、ブラウザの一部なのでおそらくそうだろうが、可能な限り htmx は今後も非常に安定し、変化の少ない状態を保つつもりだ。API は基本的に正しい方向にあり、純粋さや格好よさのためにすべてを書き直す必要はないと思う
何より htmx は Web Components とは別の問い、つまりサーバーと状態をどう同期するかに答えている
WebComponent を使うと、Htmx の morphdom のように外部から変更される DOM 要素のライフサイクルにフックできる。属性が変わったら React コンポーネントに知らせる必要があり、ノードが DOM から削除されたら React にクリーンアップ処理を実行するよう知らせる必要がある。WebComponents なしでは、これをきれいかつ効率的に行うのは難しかったと思う
結果として、ビジネスロジックの状態はすべてサーバーに置き、現在フォーカスされている要素や入力中のテキストのような控えめな一部だけをクライアントに置く、Htmx のやり方に引き続き留まれる。この方向に進むほどバグや競合状態が減り、ユーザー体験がはるかに良くなるので、努力する価値がある
https://x.com/jlazaroff/status/1717176726248108469
https://htmx.org/essays/hypermedia-friendly-scripting/
POST できるのがフォームだけで、レスポンスを処理する唯一の方法がページ全体を書き換えることだけ、という制約は恣意的に制限的に見える。htmx が HTML6 のようなものになれば、あらゆる JavaScript フレームワークより長く残るだろう
主にバックエンドに集中しているエンジニアなので、持ってきて使うほとんどの JS ライブラリはその後の保守負担が大きく、主要 API の変更も頻繁に起こる。htmx は個人プロジェクトで一度試してみるつもりだ
サーバー側で HTML 断片を生成するテンプレートも、あまり複雑にならずに済みそうだ
htmx 自体は本当に説得力のあるコンセプトだが、もっと大きなコミュニティがあればいいと思う。鶏と卵の問題ではあるが、他の Web フレームワークに比べるとブログ記事や動画のような資料が多くない。たとえば Auth0 認証を htmx アプリに統合する基本的な内容も見つけにくい
non.io 全体を純粋な Web Components で書いており、完全な SPA です。単純な入れ子のブログコンポーネントを超えていく中で、いくつか学んだことがあります
状態管理には依然として答えがありません。ラッパーフレームワークを使わないなら、多くの部分を自分でやる必要があります。React の良い点の一つは、状態管理が概ね解決されていて、既存のパターンや期待値があることです。Web Components では西部開拓時代のようで、JSX のように複雑なオブジェクトを属性として渡せないため、属性、イベント、クラス関数の直接呼び出しを混ぜることになります
パフォーマンスも期待したほど大きな利点ではありませんでした。小さな原子的要素、たとえばアイコンのようなものは、ページ上に数百個ある場合、インスタンス生成コストが価値を上回るため Web Components から戻しました。静的要素はコードの可読性を高めるとしても Web Components として使う価値がなく、かなり残念でした
それでも本当に気に入っている点もあります。筆者が述べているように、使用しているフレームワークに関係なく、新しいページにコンポーネントを追加するのが非常に簡単です。Web Components で作られていれば、どこでも本当に使えます。nonio の API ドキュメントのような補助サイトを作るとき、スタックが違っても欲しいコンポーネントを選んで使いやすいのが良かったです。コンポーネントを埋め込む必要があれば、JS を読み込み、HTML を追加すれば終わりです
shadow root の スタイルカプセル化もかなり良いです。ページ作業中にどこかのコンポーネントのスタイルを壊すのではないかと心配しなくてよい点が気に入っています。他の要素のスタイルを上書きしてしまうかどうか推測して確認する作業が減り、それを保証するためのビジュアル差分検査フレームワークの必要性も減ります
パッケージサイズが小さい点も良いです。ライブラリや追加ロードがなく、すべてが組み込みです。必要なコンポーネントを簡単に選んで使えるので、パッケージサイズを低く保つのも非常に簡単です
nonio のソースコンポーネントはこちらにあります: https://github.com/jjcm/nonio-frontend/tree/master/component...
状態管理についても、望むどんな状態管理ソリューションでも接続することを妨げるものはまったくありません。依存関係を最小限にすることが目標なら、ブラウザ組み込みのイベントモデルを使う解法もかなり見かけました
最後に、純粋な Web Components でアプリ全体を書くのは、個人的にはかなり無理のある発想だと思います。5KB で Lit を使えば、はるかに良い開発者体験が得られます: https://lit.dev
こうしたフレームワークは、ページ全体にわたる DOM 操作を一括処理することでパフォーマンスを得ることが多いのですが、Web Components ではそれができません
この理由だけでも、Web Components はほぼ行き止まりだと思います。完全にクライアントレンダリングされるページではなく、小さな機能の島だけが必要な場合には有用かもしれません
私も似たような パフォーマンスの限界 を見ましたが、常に shadow DOM が重いからだと思っていました。今では、インスタンス生成そのものが重いのか気になっています
状態管理にも同じ問題を感じており、デスクトップ開発からアイデアを借りて、ウィンドウレベルのメッセージバスを置き、すべてのコンポーネントが購読・フィルタリングし、すべての状態変更をそのメッセージバスに発行する方式を試してみようかと考えています
複雑なオブジェクトを属性として渡す例は、こちらで zx-listeditor を見れば分かります:
https://github.com/wisercoder/uibuilder/blob/master/WebCompo...
Web Component 本体はこちらです:
https://github.com/wisercoder/uibuilder/blob/master/WebCompo...
これを可能にする小さな 500 行のライブラリはこちらです:
https://github.com/wisercoder/uibuilder/#jsx-for-web-compone...
LiveState の基本パターンは「イベントをディスパッチし、状態を購読する」です。イベントと状態更新は WebSocket 接続で送信され、フロントエンドとバックエンドのライブラリは Phoenix Channels 上の薄いレイヤーです
現在、イベントハンドラ関数は Elixir で書きますが、WebAssembly にコンパイルされる任意の言語で書けるようにする作業が進行中です
好みの JS フレームワークがずっと同じであり続けるとは期待していない。寿命が短いことは分かっているし、その点が良いと思っている
前に進み、進歩し、改善することが React、Vue、Svelte などを使う理由だ。今、静的な何かを求めているわけではない。Web Components を含め、現在使えるフレームワークのどれも、この問題を特別にエレガントには解決できていないからだ
Web Components で作ったサイトが JS フレームワークで作ったサイトより長持ちするという主張なら、賭けてみたい。率直に言って、今後 20 年のうちに Web Components の技術スタックの一部が Chrome、Safari、Firefox から削除されてアプリを壊す可能性のほうが、2 年前に書いた React 16 アプリが JavaScript の変化によって壊れる可能性より高いと思う
React 16 アプリのすべての依存関係のバージョンを完全に固定すれば壊れないかもしれないが、それは「React 16 アプリで作業やハックを続けられる」というより、古いプログラムを VM やエミュレータで動かすことに近い
より意味のある賭けは、「React ZZZ アプリで開発を続けられるか vs Web Components で作ったアプリで開発を続けられるか」だろう
自分に有利な見方をしつつ、より意味のある条件にするなら、「N 年分の依存関係アップデートに追いつくために React アプリを更新するコストは、Web Components アプリより大きいか小さいか」になる。Web Components アプリに妙な依存関係を大量に入れていないなら、React アプリ側の更新コストのほうがずっと大きい可能性が高いと思う
基本的には、ブラウザベンダーのほうが React の作者たちより後方互換性を壊しにくい、という側に賭けることになる
HTML Imports は JS 専用の解法が好まれたことで廃止され、Firefox はすでに実装していた
Custom Elements v0 も廃止されたが、Chrome はこれを実装しており、YouTube は急いでこれで書き直していた。YouTube の移行にそれだけ時間がかかったため、実装削除まで約 4 年待たなければならなかった
コードを安全に保つにはライブラリを更新する必要がある。しかし古いバージョンのライブラリはサポートが終了し、修正できない既知の脆弱性が積み上がり始める。攻撃者は、既知の脆弱性があるウェブサイトをスキャンして自動攻撃するプログラムを持っている可能性が高い
逆に Web Components のように後方互換性を重視するライブラリやプラットフォームを使えば、セキュリティを維持しながら古いコードをより長くサポートしてくれる可能性がある
技術よりも人に焦点を当てるようになる
新しいプロジェクトを始めるなら、そのプロジェクトで働く人たちが何を知っているかをまず見る。フロントエンドで最近の React がそうであるように、共通分母やベストプラクティスがあるなら、単にそれを使う
こうした技術的判断で大きく悩むことはない。チーム構成、求職者市場、StackOverflow の技術ランキングが、その種の問いに簡単に答えてくれるからだ
だが全員がそう考えるなら、ソフトウェアを作るためのより良い答えがあったとしても、私たちは永遠に React を使うことになる。人気のあるものを使うというのは、良い技術的解決策を見つけて人気にしてくれる他の人々に、技術スタックの改善を依存するという意味だ
別の言い方をすれば、この戦略は採用曲線の中でも明らかに中盤から後半の採用者側に位置する。こうした記事や議論のかなりの部分は、もっと早期の採用者を対象にしている
Markdown の中に HTML を書けるので、華やかなインタラクティブな図も通常の HTML タグで表現できるなら Markdown の残りの部分と同じように移植可能だ、という内容は、MDX でもまったく同じことができる
ここで Web Components という技術そのものが役に立つわけではない。筆者が気にしているのは、マークアップとランタイム機能を適用するコード片との間のインターフェースである。Web Components はここでは実装上の詳細にすぎない。山括弧の中に名前を書くとき、それが React コンポーネントを参照していようと、Astro の何かを参照していようと、Web Component を参照していようと重要ではない。肝心なのは、別の何かが呼び出されるという点だ
ここでの Web Components の欠点は、事実上クライアントでしか実行されないことだ。サーバーでレンダリングされた shadow DOM の内容をクライアントに渡す方法がないため、Web Component が完全に非インタラクティブであっても、レンダリングするにはクライアント JS が必要になる。他のフレームワークはそうではない
筆者の要点が間違っているというわけではない。コード片同士のインターフェースを体系化する方法として、Web Components はおそらく最良で長く残る方法かもしれない。しかしサーバーでは Web Components は何の助けにもならない。レンダリングされず、インターフェースは扱いづらく、コードはすでに全体として扱える形で書かれている可能性が高い。サーバーで Astro を使っているなら、そのまま Astro を使えばよい。React でも Vue でも何でも同じだ
Web Components が真価を発揮するのはクライアントだ。選んだフレームワークで書かれていないかもしれないサードパーティ製コンポーネントを使うとき、ページに載せるコンポーネントがどのフレームワークで作られたのかを知る必要がない。HTML タグを書けば動く。インターフェースの仕組みがブラウザレベルに引き上げられるので、2つのフレームワークが互いの実装詳細を知る必要がなくなる
その意味では、何らかのツールが必要だという点では正しい。ただしそのツールは、初期 HTML を埋めてブートストラップしてくれる任意のサーバーサイドのテンプレートエンジンでよい
しかし、サーバーでレンダリングされた shadow DOM はクライアントに渡せる。これは 宣言的 shadow DOM と呼ばれ、Firefox を除く主要ブラウザすべてで動作する。Firefox でも現在作業中で、ポリフィルもある
https://developer.chrome.com/articles/declarative-shadow-dom...
ちなみにこのポリフィルはかなり簡単なので、今ブラウザが対応していなくても使える可能性がある
サーバーでレンダリング可能な Web Components が登場すれば、マルチページアプリのユーザー体験をより良くする可能性が大きく開けるはずだ
JS の世界はいまだに、すべての制御権を持つ厚いクライアントにこだわりすぎていて、サーバーの世界で起きている興味深いことを見逃している
Rails Hotwire、Phoenix LiveView、Laravel Livewire のような解決策は、最小限の JS とほとんどない開発者体験上の複雑さで、フロントエンドの 80% を解決できる
サーバーなしでデータベースとオブジェクトストレージを読み書きする、機能豊富なフロントエンドアプリも作ったことがある。移植性が非常に高く、インストール手順は数個のファイルをアップロードするだけだ。単純な CDN からそのまま実行でき、とても速い
私の知るフロントエンド開発者の大半も Hotwire 系を知っているが、彼らにとってはそのようなスタックのほうが開発者体験上の複雑さを増やす。人は時に「複雑さ」と言うが、実際には「不慣れさ」を意味している場合がある。フロントエンドもバックエンドも過度に複雑になり得るが、必ずそうである必要はない
物事がうまくいかなくなったときには、より多くの制御権があるほうがよいと思う。特に、クライアントが今すぐ欲しいと求めるようなスピード感のある環境ではなおさらだ
個人的には、HTML の上に構文糖衣を振りかけるより、すべてを JavaScript/TypeScript で書くほうがよい
Livewire のようにサーバー側に置いたからといって、レイテンシのような強い欠点が消えるわけではない
タイトルはおそらく正しいのだろうが、それが重要かどうかはわからない。ほとんどのアプリケーションはそこまで長く生きない
最近 Web Components を使ってみたが、どのフレームワークでもごく単純な機能の設定が本当にわかりにくく複雑だった。コンポーネントが単純なものなら問題ないかもしれないが、JS でアプリケーションを作るなら、スロットは合成可能なコンポーネントを作れるようにする中核機能だ
開発者ツールで shadow DOM をデバッグするのも本当に混乱した
React の renderProp や Vue の scoped slot は、親コンポーネントから子コンポーネントへデータを注入するために非常によく使われる。しかし Web Component でそのような動作を再現するのは、MutationObserver によるハックなしには事実上不可能だ。子コンポーネントと親コンポーネントが互いの情報を知る必要はないという Web Component の前提は、現実世界とは合っていない
子コンポーネントが親とは無関係に常にロードされ初期化されるという前提も、ほとんどのフレームワークでは間違っている。ほとんどのフレームワークでは、親コンポーネントが子を実際にロードまたは初期化するかを決定する
Web Components は、隔離され再利用可能なコンポーネントを書くにはより適しているかもしれないが、アプリケーションを動かすためにあれこれつなぎ合わせるのに使う DOM API は、単にひどい
XML 文書を編集するために作られた名残が見える
隔離され再利用可能なコンポーネントにも Web Components を作らない理由がもう一つある。どのエディタも属性やイベント名の自動補完、タイプミス検出のようなものをきちんと拾ってくれない
役に立つものにするには複数の依存関係をつなぎ合わせる必要があり、その依存関係はおそらく React ほどよく保守されないだろう
こういう哲学に従う会社で働いたことがある。すべての UI コンポーネントが、それぞれインストールしなければならない npm パッケージを持つ Web Component だった。ちゃんと動きはしたが、利点はまったくなかった。6年たった今でも利点はない。おそらく来ない未来のために最適化しているようなものだ
HTML/CSS サイトは何よりも長持ちするだろう
テキスト、画像、レイアウトが実際に静的なものの中に入っていて、手を加えなくても永遠に残り、複雑なコード実行や曖昧で急速に変わる準標準の維持を必要としないからだ
長持ちするサイトを作り、長持ちする技術を手にしたいなら、段階的に強化された HTML Web サイトの作り方を学べばよい。Web Components のように役に立たないハイフン付きの未定義 HTML 要素を作っておいて、JavaScript を大量に実行したあとで定義するやり方や、HTML を使うふりすらしない JavaScript フレームワークは、数年しか持たないだろう
少なくともブラウザから HTTP/1.1 のサポートが削除されるまではそうだろう