[翻訳]Web Componentsに祝杯を?
Any Holy Grail for Web Components?の翻訳。
Web Componentsの歴史は輝かしいものではなかった:
- それらの宣言的な性質は静的なHTMLの世界に限定されている
- スロットとShadow DOMを介したJSカウンターの宣言はぎこちない
- Shadow DOMは重く、確実にポリフィルするのが困難
- Shadow DOMはすべての問題は解決できず、必須の設定が必要であり、SSR(そうすべきではないとわかっているが…)を介して配信できない
- HTML Modulesはまだどこにも実装がないが、Vue.jsや他のフレームワークが何年も前から提供しているものとあまり変わらない
Shadow DOMを必要としない、または使用しない場合、これまでに成功してきたライブラリやプロジェクトで採用されてきたように、コンポーネントが広く採用される可能性が高くなる。しかしまだ他の弱みが残る:
- 宣言はまだ静的なHTMLの世界に限定されている
- CSSはあらゆる場所にも存在し、Shadow DOM内のものより脆弱である
- 冗長なノードの数があまりにも多くなるため、複雑なページではダウンロードと処理の両方に負荷がかかる
- 古き良き標準DOMとテーマの統合はまだぎこちないだろう
DOMの冗長性について
ホスティングしているページからテーマを継承する可能性のあるParagraphを表現したいとしよう。そして、JSを経由して魔法のようなことができる「Special paragraph」として配信したいとすれば、おそらく次のようにするだろう:
// the JS side class SpecialP extends HTMLElement { constructor() { /* setup it once */ } } customElements.define('special-p', SpecialP); // the HTML side <special-p> <p> The actual content </p> </special-p>
代償として、どのような形であれShadow DOMを使用すると、ホスティングしているサイトのテーマを反映できる<p>
タグをつけなくてもロジックとレイアウトの両方が肥大化する。
グレースフルエンハンスメントについて言うと、カスタム要素をサポートしていないブラウザには<spacial-p>
タグをどう処理すればいいのかさえわからない。
私たちが望んでいたのは、Paragraphを簡単に設定する方法だけだった。
標準ベースのアプローチ
2KBの小さなWicked Elementsライブラリは、すでに説明したような問題を解決する素晴らしいソリューションになっている。それはひとつのポリフィルも必要とせずにIE9までサポートする。カスタム要素をまったく使用せずに、DOMに到達するあらゆる要素に、カスタム要素のようなイベントを別のAPIを介して公開する。
// the JS side wickedElements.define('p.special', { init() { /* setup it once */ } }); // the HTML side <p class="special"> The actual content </p>
頭を使う必要がないほど簡単なので、ぜひこの小さなライブラリを試してみてほしい。しかしまだいくつかの問題点が残っている:
- wickedElementsライブラリはそのスタックをより理解している人にはうまく機能するが、初心者には少し混乱を招くかもしれない(普通のパラダイムとはまったく異なるので)
- 要素はライブになるとその場でアップグレードされるが、DOMはアップグレードされるまでこれらの要素について何も知らない。これはカスタム要素にも当てはまるが、内部または将来のブラウザパフォーマンス最適化に関しては、カスタム要素などのネイティブのプリミティブを使用する方が常に安全である
- HTMLコンテンツの宣言はデフォルトではまだHTMLの世界に限定されている。これはSSRとグレースフルエンハンスメントについては素晴らしいことだが、Reactエコシステムは多くの開発者がロジックをひとつの場所——つまりJSファイルとJSX(または似たような解決策)だけに限定したがることを示している
簡単なまとめ:React開発のエクスペリエンス
Web標準についてさらに説明する前に、私が考えるReactの人気を高めるために役立ったことを要約したい:
- 素晴らしいドキュメント
- great tooling around
- 依存関係をインポートし、1つのファイルでコンポーネントの意図を宣言する
Web標準もこのリストの最初の2つの点を、より良くはないにしても確実にカバーしている。しかし3つ目の点を超えることはできない:
// the React JS side class Special extends React.Component { render() { return <p>{this.props.text}</p>; } } // still the React JS side ... const el = <Special text="The actual content" />;
そしてstyled-componentsを追加する……。標準技術を通して私たちがそのような経験に近づき、楽になる可能性はあるだろうか?
カスタム要素に組み込まれているアプローチ
残念ながらまだすべてのブラウザで利用できるわけではないが、すでにカスタム要素をサポートしている場合(例:Safari)ならたった1Kのポリフィルを使用すれば、このプリミティブはカスタム要素の標準的な表現力だけでwickedElementの能力を得るための最善の妥協策になる。
// the JS side class SpecialP extends HTMLParagraphElement { constructor() { /* setup it once */ } } customElements.define('special-p', SpecialP, {extends: 'p'}); // the HTML side <p is="special-p"> The actual content </p>
結果として、これまでに説明したいくつかの課題が解決された:
- DOMはまったく膨張しない
- ページはグレースフルエンハンスメントされる
- ページは今すぐSSRできる
- カスタム要素のすべてのライフサイクルを利用できる
したがって、不足しているのは次の通り:
- JSファイル内でも内容を宣言する方法
- JSファイル内でもコンポーネントのスタイルを定義する方法
- 素晴らしいアプリを簡単に開発するために役立ついつもの特別な魔法
簡単に言うと、これまで押し戻されてきた組み込みの拡張とユーザーランドの標準ベースのソリューションを組み合わせてJSで宣言的レイアウトを作成することは、標準的な世界でReactエクスペリエンスをシミュレートするための完璧な組み合わせかもしれないということだ。
Heresyとは何か
このライブラリ名(和訳すると異教、異端、異説)は、一般に受け入れられている概念とは大きく異なる概念を組み合わせているため、これ以上適切なものはないと思う。
Special P
コンポーネントの最も基本的な例を次に示す:
// the JS side import {render, html, define} from 'heresy'; define(class Special extends HTMLParagraphElement { static tagName = 'p'; }); // the HTML side (SSR ready) <p is="special-heresy"> The actual content </p> // or ... the JS side render(document.body, () => html` <Special> The actual content </Special> `);
🤯
待って……。何が起こっている?
- 静的クラス
.name
を使用してレジストリ名を定義し、末尾に-heresy
を付けて、カスタム要素レジストリに対して常に有効な名前にする - 静的クラス
.tagName
を使用して、表すべき実際のDOM要素の種類を指定する。組み込みのカスタム要素であればどんな要素でも構わない(option
、tr
、td
、select
、li
、input
、label
……) - もしDOMに
is=
属性が検出されればすぐにアップグレードされる - lighterhtmlを使用してクラス
.name
を記述した場合、テンプレートリテラルでレイアウトを宣言するために使用されるエンジンは、<p is="...">
として自動的に変換されて膨張しない。宣言されたノードは実際にはDOM上に存在し(これらを参照することもできる)、仮想的なものは存在しない。つまりカスタム要素はユーザーがハンドリングできるインスタンスである
その結果、JSXと非常によく似た方法で宣言することができる。
the best part of `heresy` is that layout can look exactly like in JSX, but there's no indirection whatsoever to what you write and what you get/target 🎉 pic.twitter.com/tKZpLrvsKE
— Andrea Giammarchi (@WebReflection) April 27, 2019
CSSについてはどうか?
import {render, html, define} from 'heresy'; define(class Special extends HTMLParagraphElement { static style = selector => ` ${selector} { transition: background 300ms; } ${selector}:hover { background-color: silver; } `; static tagName = 'p'; });
このCSSは、クラスの静的style
メソッドを介してクラス宣言ごとに1回だけ注入され、スタイル設定に使用される名前を含む文字列であるセレクタを引数として渡す。
この例では、引数はp[is="special-heresy"]
になる。
style
は単なる静的メソッドであるため、実行時にSASSやLESSのようなトランスフォームを介して変換された文字列を返すこともできる。
propsについてはどうか?
import {render, html, define} from 'heresy'; define(class Special extends HTMLParagraphElement { static tagName = 'p'; #props = {}; get props() { return this.#props; } set props(props) { this.#props = props; this.render(); } render() { this.html`${this.props.text}`; } }); render(document.body, () => html` <Special props=${{text: 'The actual content'}}/> `);
lighterhtmlのおかげで、カスタム要素には任意のゲッターまたはセッターを指定できるので、任意の種類の値を即座に渡すことができる。
互換性についてはどうか?
ポリフィルとツールを適切に組み合わせることで、HeresyはIE9でも動く。
その方法を理解するには、基本的なライブデモまたはそのソースコードを参考にできる。
では…?
私に休憩させてください😂
このプロジェクトはまだ初期の段階で実験的なものだが、すでにいくつか有望な成果が出ている。たとえばこの古典的なTodoデモ(ソース)は現在Chrome Canaryでしか動作しないが、これはトランスパイルを行わずに最新のJSの機能をベースにしているからだ。
次のようなもの:
// ... render() { this.html` <input placeholder="type item" onkeydown=${this}> <Hide onchange=${this}/> <ul>${this.items.map( data => html`<Item data=${data}/>` )}</ul>`; }
このカスタム要素ベースのアプローチを使うのはとても楽しいが、私はまだこれを使って具体的なアプリを作っていないことを覚えておくように。そしてlightterhtmlよりもhyperHTMLの方が、その.bind
メカニズムのおかげでより適切なのではないかと考えている。
しかしプロトタイプ作成にはlighterhtmlが最も簡単なツールだ。当面私が理解したいのは、コミュニティがこのようなソリューションを気に入って、興味を持っているかどうか。そうすればおそらくいつかSafariの開発者もネイティブの組み込み機能を提供して、誰もがポリフィルをドロップできるようになるだろう。
特定のコンテンツのためのコンポーネントとそうでないコンポーネント
CSS Utility Classes and "Separation of Concerns"(日本語訳)というTailwind CSS作者の記事を読んだ。そこでは「セマンティック」なコンポーネントと「コンテンツに依存しない」コンポーネントについて言及されていた。
セマンティックなコンポーネントは、
author-bio
article-preview
のように取り扱うコンテンツの種類をそのまま命名に反映させたものだ。コンポーネントの用途は特定のコンテンツのためだけに限定される。これを特定のコンテンツのためのコンポーネントと呼ぼう。
コンテンツに依存しないコンポーネントは、
media-card
card-list
button
のようにUIパターンを区別するだけの命名がされる。コンポーネントの用途として取り扱うコンテンツの種類を制限しない。これはUIパターンとしてのコンポーネントと呼ぼう。
以下私見。
普通は特定のコンテンツのためのコンポーネントの方が専用的だ。同じ種類のコンテンツの表示を別々のコンポーネントで行うことはあまりない。というかそうなるように分ける。たとえば商品詳細と商品属性概要だと、product
とproduct-teaser
のようにそれぞれ別コンポーネントにする。
デザインの個別性が高いサイトほど特定のコンテンツのためのコンポーネントが増える。それは何かしらの理由により共通化が進められていない状態とも言える。だから影響範囲が予測しやすく捨てやすい。反面新しいページで再利用できるコンポーネントは少なくなる。言うまでもなく、実情を見ずにUIパターンとしてのコンポーネントを増やしてしまったりすると、結果的には名前通りの使い方ができない嘘の設計になってしまう。
一方でUIパターンとしてのコンポーネントは汎用的だ。media-card
を使えば、商品でも著者でも記事でも好きなコンテンツを表示できる。
デザインの共通化が進むとUIパターンとしてのコンポーネントが増える。すると再利用できるコンポーネントが増える一方で、個別の違いを持たせるのが難しくなる。汎用性を目的としたUIフレームワークはUIパターンとしてのコンポーネントを中心に構成されていることが多く、スケールさせやすい。逆に特定のプロダクトに特化したPrimerのようなUIフレームワークは特定のコンテンツのためのコンポーネントも多く含む(ただしできるだけ避けるべきと言っている)。
両者には優劣があるわけではなく、必要に応じて併有するものだ。そうしてコンポーネントの役割を明らかにする。やがてサイトが成長するにつれて(あるいは実装が進むにつれて)、それぞれのコンポーネントが本当のところはどちら側なのかが浮き彫りになってくる。その段階で改めて命名し直す。この作業によりガイドが適切に引き直される。最初に戻る。
多くの場合、早すぎる共通化は失敗する。つまり特定のコンポーネントが、UIパターンとしてのコンポーネントであるかは慎重に判断すべきだ。
間違った共通化は意図しない再利用を生む。意図しない再利用は非合理的な拡張のきっかけになり、不必要に複雑性を高める。さらには使用箇所を把握しにくくなる。
共通化はすぐにできても、個別化は適切な影響範囲の理解と構成要素の分解を要する。それなら安直な共通化は避けるべきだ。
だから僕は必要と思えるタイミングまで、UIパターンとしてのコンポーネントの作成を見送ることが多い。
SPAじゃないプロジェクトのための控えめなJavaScriptフレームワーク「Stimulus」
ReactやAngularのように、アプリ内のすべてのHTMLをJavaScript側で管理して描画するアプローチは大げさすぎる。ほとんどの場合において。
もちろん一定以上の複雑さがあればその辺のJavaScriptフレームワークを使った方が良い。が、それがやり過ぎになるプロジェクトは多い。というかその方が多い。それでも、それらのアプローチではやり過ぎになってしまう場合でも、秩序のあるコードを書き続けられないと当然苦しい状態へ向かう。
この微妙な(そして多数派であるはずの)環境を適度に管理できる枠組みが必要だ。これまでちょうどいい解決策を見つけられずに苦労してきた。Web Componentsは課題を解決するかもしれないがまだ来ない。そこで出会ったのがStimulusだ。
いわく、Stimulusは既存のHTMLのための控えめなJavaScriptフレームワーク。サーバーサイドレンダリングされたHTML(これはIsomorphicとかじゃなくて普通の)に対して、特定の属性が付与されていれば対応するコントローラを初期化する。
まずHTMLにはdata-controller
属性、必要に応じてdata-target
、data-action
属性を追加する。
<!-- HTML片は好きなところに --> <div data-controller="hello"> <input data-target="hello.name" type="text"> <button data-action="click->hello#greet"> Greet </button> <span data-target="hello.output"> </span> </div>
対応するコントローラを作成すると、Stimulusはその振る舞いを登録する。
// hello_controller.js import { Controller } from "stimulus" export default class extends Controller { static targets = [ "name", "output" ] greet() { this.outputTarget.textContent = `Hello, ${this.nameTarget.value}!` } }
これのよくできてるところは、単に読み込み時にだけコントローラの初期化が行われるわけでなく、ドキュメントへコントローラに対応するDOMが挿入されるたびに自動的に初期化される。削除されたら勝手に破棄される。さらにそのためのライフサイクルフックがある(initialize/connect/disconnect)。
// 好きなタイミングでDOMを挿入すれば勝手に初期化される document.body.innerHTML = `<div data-controller="hello"> <input data-target="hello.name" type="text"> <button data-action="click->hello#greet"> Greet </button> <span data-target="hello.output"> </span> </div>` // 好きなタイミングでDOMを削除すれば勝手に破棄される document.body.innerHTML = ''
これは、StimulusはそもそもTurbolinksと協調するために作られたからだ。だからTurbolinksと組み合わせて使うこともできる。
これだけ書けば求める人にはグッと来るものがあるはずだ。詳しい使い方は公式ハンドブックのサンプルコードを見れば大体わかる。中途半端にまとめてもすぐに腐るので触らない(APIの変更が多いわけではないが)。思想的な序文だけ訳しておくことにする。
次からStimulusの起源(The Origin of Stimulus)の翻訳。
私たちはBasecampでたくさんのJavaScriptを書いているが、現代的な意味での「JavaScriptアプリケーション」は作成していない。すべてのアプリケーションはサーバーサイドレンダリングされたHTMLをベースにして、それらに付与する形でJavaScriptを追加している。
これが雄大なモノリスのやり方である。Basecampは、Ruby on Railsを使って作成されたコントローラ、ビュー、モデルの単一セットを持ち、ネイティブモバイルアプリを含むいくつかのプラットフォームで動作する。多くのプラットフォームがあるにも関わらず、小さなチームでやっていくためには、1つの共有インターフェイスを1箇所で更新できることが重要だ。
すると昔のように生産性の高いパーティーができる。1人のプログラマーが間接的なシステムや分散システムのレイヤーに縛られることなく、貪欲に進歩できる時代に逆戻りする。誰もにとっての究極の目標は、サーバーサイドアプリケーションがクライアントサイドアプリケーションのためにJSONを生成するだけのものになる前に戻ることだ。
そのようなアプローチに価値がないというわけではない。しかしそれは多くのアプリケーションにとって、そしてBasecampにとって、複雑性と生産性の低下をもたらす。
シングルページのJavaScriptアプリケーションにメリットはある。ページ全体を更新せずにより高速で流動的なインターフェースを実装できることだ。
Basecampもそのようだと感じて欲しかった。まるで流れに乗って、レンダリングをクライアントサイドで行うようにすべて書き直したか、完全なネイティブアプリになったかのように。
この要求が私たちをTurbolinksとStimulusという強力な組み合わせに導いた。
Turbolinkは高く、Stimulusは低く
私たちのこの新しい控えめなJavaScriptフレームワークであるStimulusを紹介する前に、これを使ってTurbolinksの命題を要約しておこう。
TurbolinksはGitHubで開発されたpjaxと呼ばれるアプローチから派生している。基本的な概念は同じままだ。
ページ全体のリフレッシュに時間がかかるのは、ブラウザがサーバーから送信される大量のHTMLを処理しなければならないからではない。それに関してブラウザは非常に優れており、非常に高速だ。そしてほとんどの場合、HTMLペイロードがJSONペイロードよりも大きくなる傾向がある事実も問題にはならない。要因は、CSSとJavaScriptを再初期化して再度ページに適用する必要があることだ。ファイル自体がキャッシュされているかどうかに関係なく。CSSとJavaScriptのサイズによってはかなり遅くなる。
この再初期化を回避するためにTurbolinksは、シングルページアプリケーションと同じように永続的なプロセスを維持する。リンクのクリックイベントを乗っとり、Ajaxによって新しいページをロードする。サーバーは完全な形式のHTMLドキュメントを返す。
この戦略だけでも多くのアプリケーションを非常に速く感じさせられる。Basecampではページからページへの移動が約3倍速くなった。この高速化はアプリケーションに反応性とシングルページアプリケーションの魅力の大部分を占めていた流動性を与える。
しかしTurbolinksだけでは話の半分にしかならない。ページの読み込み以降、1つのページ内にはあらゆる振る舞いが登録される。要素の表示と非表示の切り替え、クリップボードへのコンテンツのコピー、リストへの新しいTodoの追加、および昨今のウェブアプリケーションに関連づけられているその他すべての操作を行うインタラクション。
Stimulus以前、Basecampはこれらをさまざまなスタイルやパターンを使って実装していた。コードの中にはjQueryを使ったちょっとしたものもあれば、同じようなサイズの単純なバニラJavaScriptもあり、また、より大きなオブジェクト指向のサブシステムであるものもあった。これらすべてはいつも、明示的なイベント処理によってdata-behavior
属性を無効にしていた。
(data-behavior
はJavaScriptによる振る舞いをビューに依存せずに管理するための方法論。詳しくはCode Spelunking in the all new Basecamp – Signal v. Noiseを参照)
このような新しいコードを追加するのは簡単だったが、包括的な解決策ではなく、社内のスタイルやパターンが多すぎた。そのためコードの再利用が難しくなり、新しい開発者が一貫したアプローチを習得することが困難だった。
Stimulusの3つの核となるコンセプト
社内の良いパターンから抽出した次の3つが主要な概念となった。
- コントローラ
- アクション
- ターゲット
Stimulusはこれらを控えめで小さなフレームワークとしてまとめたもの。
対応付けられたHTMLを見ると、Stimulusはプログレッシブエンハンスメントとして読み込まれるよう設計されていることがわかる。これにより、どの振る舞いが作用しているのかをテンプレートを見て理解できる。例を示そう。
<div data-controller="clipboard"> PIN: <input data-target="clipboard.source" type="text" value="1234" readonly> <button data-action="clipboard#copy">Copy to Clipboard</button> </div>
例を読むと何が起きているのかよくわかる。Stimulusについて何も知らなくても、あるいはコントローラのコードを見なくても。これは対応するイベントハンドラーが外部JavaScriptファイルに存在するHTML片を読むことと大きく異なる。また、現代の多くのJavaScriptフレームワークで失われている関心の分離も維持している。
ご覧のように、StimulusはわざわざHTMLを作成するのではなく既存のHTMLドキュメントに取り付けられる。ほとんどの場合、HTMLはページ読み込み時(最初の読み込みまたはTurbolinks経由)に、またはDOMに作用する起点になるAjaxリクエストによってサーバー上でレンダリングされる。
Stimulusはその既存のHTMLを操作することに関心がある。例えば、要素を非表示にしたりアニメーションさせたり、ハイライトさせたりするCSSクラスを追加すること。グループ内の要素を再配置させたりすること。UTCを地方時の日時表記に変換するように要素の中身を操作することなど。
Stimulusに新しいDOM要素を作成させたいこともあるが、それも自由に行える。将来的には簡単にできる方法を提供するかもしれない。しかしこれはごく少数のユースケースだ。焦点は要素の作成でなく操作にある。
Stimulusが主流のJavaScriptフレームワークとどのように違うのか
そのため、Stimulusは現代のJavaScriptフレームワークの大部分とはかなり異なっている。ほとんどのフレームワークは何らかのテンプレート言語を介してJSONをDOM要素に変換することに焦点を当てている。それらのフレームワークでは多くの場合、空のページを作成し、そうしてJSONからレンダリングした要素だけをそのページに追加する。
主流のJavaScriptフレームワークとStimulusでは状態の扱いも異なる。ほとんどのフレームワークには、JavaScriptオブジェクト内に保持した状態に基づいてHTMLをレンダリングする機能がある。Stimulusはその正反対だ。状態はHTMLに格納されるため、ページが変更されるたびにコントローラを破棄できる。そしてキャッシュされたHTMLから再び表示されてすぐの状態から再初期化できる。
これはまったく異なるパラダイムだ。現代のフレームワークに慣れているベテランのJavaScript開発者の多くはこれをあざ笑うに違いない。React+Reduxのような巨大な渦巻きの中で、アプリケーションを維持するために必要な複雑さと労力に満足しているなら、Turbolinks+Stimulusには魅力を感じないだろう。
一方で、現代の技術が示唆するような複雑さとアプリケーションの分離が不要だと感じている場合は、このアプローチに退避することになる。
Stimulusや関連するアイデアは現実世界から抽出された
Basecampではこのアーキテクチャをいくつかの異なるバージョンのBasecampや他のアプリケーションで長年使用してきた。GitHubも同様のアプローチを使って大きな効果を得ている。これは「現代」における、ウェブアプリケーションがどのようなものであるかの主流の理解に対する有効な代替手段である。それも非常に説得力のある。
実際、Stimulusは私たちがRuby on Railsを開発したときにBasecampで使った秘伝のタレに似ている。現代の主流のアプローチは不必要に複雑になっている感覚がある。私たちは本当はより多くのことをより速く、より少ないリソースで実現できるように思う。
あなたはそのいずれかだけを選択する必要はない。StimulusやTurbolinksはよりヘビーな他のアプローチと組み合わせて使うと効果的だ。もしあなたのアプリケーションの80%がヘビーな仕組みを必要としないのであれば、私たちのようにStimulus+Turbolinksを使うことを検討して欲しい。そしてアプリケーションの中の、実際に恩恵を受けられる部分でヘビーな仕組みを展開すると良い。
Basecampでは必要に応じていくつかの強力なアプローチを採用している。例えば、私たちのカレンダーはクライアント側のレンダリングを使用する傾向にある。私たちのテキストエディタはTrixという完全な形のテキストプロセッサで、Stimulusコントローラの位置付けとしては意味を成さない。
一連の代替フレームワークはできるだけ重労働を回避することを目的としている。この単純なモデルでうまく機能する多くのインタラクションに対して、リクエスト・レスポンスパラダイムの範囲内に留まる。そして必要性が真に迫っていれば高価なツールに手を伸ばす。
何よりもこれは、より労力がかかる主流のアプローチを使う大きなチームと、忠実さで競争したいと思っている小さなチームのためのツールキットである。
試してみてごらん。
デイヴィッド・ハインマイアー・ハンソン
サーバーサイドのみのテンプレートエンジンとしてのReact
最近の仕事ではJSがあんまりなくてページ数はそこそこあるみたいなサイトを作ってることが多い。作り方として、コンポーネントごとにPugのmixinとかNunjucksのmacroで抽象化してマークアップが壊れないようにしてるんだけど、これらだとコンポーネントを実装するための機能として微妙。具体的には、ノードを挿入できる箇所が1箇所に限定されてることとエディタの補完がない。
mixin Disclosure(params = {}) - const props = Object.assign({ initialExpanded: false, detailsId: ulid(), }, params) .Disclosure(role="group")&attributes(attributes) button.Disclosure__summary(type="button" aria-expanded=String(props.initialExpanded) aria-controls=props.detailsId)!= props.summaryContent .Disclosure__details(id=props.detailsId hidden=!props.initialExpanded) block +Disclosure({ summaryContent: '最高の<em>コンテンツ</em>' }).u-mt5 p 立派なインターネットコンテンツになったなあ。
{% macro Disclosure(params = {}) %} {% set rootClass = params.rootClass %} {% set initialExpanded = params.initialExpanded %} {% set summaryContent = params.summaryContent %} {% set detailsId = params.detailsId | default(ulid()) %} <div class="Disclosure {{ rootClass }}" role="group"> <button class="Disclosure__summary" type="button" aria-expanded="{{ initialExpanded }}" aria-controls="{{ detailsId }}">{{ summaryContent | safe }}</button> <div id="{{ detailsId }}" class="Disclosure__details" {% if not initialExpanded %}hidden{% endif %}> {{ caller() }} </div> </div> {% endmacro %} {% call Disclosure({ rootClass: 'u-mt5', summaryContent: '最高の<em>コンテンツ</em>' }) %} <p>立派なインターネットコンテンツになったなあ。</p> {% endcall %}
例ではsummaryContent
の中身が平坦化されたテキストだと限らないので、仕方なくそこだけ生のHTML書くとかになっちゃう。テンプレートエンジンの機能が使えなくなるのでコンポーネントが入れ子になったりすると詰む。
VS Codeではシンタックスハイライトしてくれるだけで補完とかはなんも出ない。tsxでReact Componentのpropsの型までわかるのと比べるとだいぶ非効率的になる。Pugのmixinとかはそもそもコンポーネント専用の機能じゃないというのもあると思うし、言語のコミュニティの勢い的にも大きな進歩は望めなそう。自分で拡張書くほどのガッツもない。
じゃあもうtsxそのまま使えばいいじゃんって感じになってきたのでサーバーサイドテンプレートエンジンとしてReactを試した。<body>
の中身が空の状態から始めるって話じゃなくて、この場合ではReactはクライアント側に一切介入しない。サーバーでしか動かさない。
似たところだとDocusaurusが同じような発想でReactを使ってる。Facebook製だからだと思うけど。
素直に長いものに巻かれるとNext.jsとかGatsbyJS使えばいいんだけど、あんまりJSを使わないサイトだとやり過ぎになったり、あと納品形態がいろいろなのでいろいろある(ビルド後のHTMLファイルを人間が編集できるようにしておきたい)みたいな理由。
最近Eleventyという静的サイトジェネレータが気に入ってるのでこれを使う。便利なのがテンプレートエンジンの選択肢が豊富なところで、ピュアなJavaScriptでテンプレートを書くこともできる。
import React from 'react' import ReactDOMServer from 'react-dom/server' import { Layout } from './components/Layout' import { Disclosure } from './components/Disclosure' // https://www.11ty.io/docs/data/#eleventy-provided-data-variables type DefaultProvidedData = { pkg: unknown page: { url: string } collections: unknown } type PageData = { title: string } module.exports = class { data(): PageData { return { title: 'Home', } } render({ page, title }: DefaultProvidedData & PageData) { return ( '<!doctype html>' + ReactDOMServer.renderToStaticMarkup( <Layout url={page.url} title={title}> <Disclosure rootClassName="u-mt5" summaryContent={ <> 最高の<em>コンテンツ</em> </> } > <p>立派なインターネットコンテンツになったなあ。</p> </Disclosure> </Layout>, ) ) } }
こういうのをページごとに書いていく。よさそう。
JSXの代わりにJSXっぽい構文をTagged templatesで書けるdevelopit/htmを使えばプリコンパイルもなくせそうと思ってそれも試した。
const htm = require('htm') const vhtml = require('vhtml') const Layout = require('./components/Layout') const Disclosure = require('./components/Disclosure') const html = htm.bind(vhtml) module.exports = class { data() { return { title: 'Home', } } render({ page, title }) { return ( '<!doctype html>' + html` <${Layout} url=${page.url} title=${title}> <${Disclosure} rootClass="u-mt5" summaryContent=${ html` <span>最高の<em>コンテンツ</em></span> ` } ><p>立派なインターネットコンテンツになったなあ。</p><// > <//> ` ) } }
確かにプリコンパイルはなくせたけど、そもそも解決したい補完が弱い。TypeScriptじゃないというのはあるけどもうちょっとがんばって欲しい。lit-htmlの拡張を使ってて進化を期待はできそうではある。
テンプレートの構文の細かい仕様を確認したりするのが地味にめんどい。あとReact.Fragmentみたいなやつが実装されてないとか。そういう風に考えるとしばらくはReactよりいい選択肢はなさそう。
加えて別の視点だとWeb Componentsを使って解決できる線はある気がする。コンポーネントはCustom Elementsで実装して、それをNunjucksとか今あるテンプレートエンジンで普通に使う。クライアントサイドから見ればオーバースペックだけど、JSで実装する部分があればこっちの方が安心できる。
--- layout: base title: Home --- <x-disclosure class="u-mt5"> <span slot="summary">最高の<em>コンテンツ</em></span> <p>立派なインターネットコンテンツになったなあ。</p> </x-disclosure>
Custom Elementsの補完は微妙だけど、開いてるファイルからcustomElements.define(...)
を拾っていい感じにしてくれるようになるのを期待できなくもない。というのを書きながら希望的観測過ぎる気はしてきた。
完全に問題を解決する場所を間違えている、みたいな意識はない。
なゆくんとのおもいで
ナユコロニー2 Advent Calendar 2018の8日目の記事です。
なゆくんはおぼえていますか? ぼくです、しばらくぶりだね。しらないうちになゆくんはとうきょうではたらくようになっていたんだね。きくとそのわかさでほんをかいたというじゃないか。じつはぼくもきになってとりよせてよんでみたんだけど、せんもんがいのないようだからむずかしくてさっぱりだったよ。しらないうちにたくさんべんきょうしていたんだね。プログラマーというんだっけか? やっぱりいいね、てにしょくつけるというのは。ぼくもそういうしごとをえらべばよかったのかなあとすこしこうかいしています。さて、ぜんだんのはなしはこれくらいでいいかな。ほんだいのまえのはなしがすこしながくなってしまうことがぼくのわるいくせです。
さいしょにおぼえていますかときいたけどわざわざきくまでもないよね。なゆくんはむかしからとてもあたまのいいこだったから、ぼくがこれからなにをつたえるかなんてもうしっているよね。というよりきっと、なんねんもまえから、きょうというひにこのしらせがくるということをいちにちたりともわすれられなかったんじゃないかなあ。でもしかたない、ぼくはわるくないよ、ぜんぶなゆくんがかんがえたことなんだからね。またおしゃべりをしてしまったね、はやくほんだいをだったね。
なゆくんはあしたのしはつでじもとにかえります。ひるすぎにはついてるだろうから、ふるさとのくうきをすってかんがいぶかいきもちになったりせずに、すぐにそうこのまえまできてください。もちろんじもとのしりあいにみつかったりしたらだめだよ。ぼくはうらのあきやにかくれておもてのようすをみてるからついたらすぐにわかるとおもいます。さいきんぼくはずっとなゆくんのためにそうこのようすをしらべてあげてるんだけど、ふたつきにいっかいさとしのおばさんがみにくるだけでそれいがいのひとはよりつきもしてないね。なゆくんはほんとうにラッキーなひとだよ。
わかってるってば。なゆくんがしりたいのはそんなことじゃなくて、あのせいまいきのおくにあるたなをだれもさわってないかってことだよね。だれもあんなところにあるたなをつかおうとするはずないじゃないか。そうこはずっとぎゅうぎゅうずめでせいりするおとこでがたりないってずっとさとしのおばさんがなげいてるよ。このしごとがおわったらてつだってあげるといいんじゃないかな。もちろんぼくはごめんだけどね。どっちにしてもまずはれいのごとく、そうこにたくさんあるガラクタとゆかとのすきまをとおったらたなにかくしてあるかぎとごたいめんだ。あのやせぎすだったなゆくんがとうきょうのおいしいごはんでふとってつっかえてしまわないといいけどね。
かぎがてにはいったらどうするかはわかるよね。なゆくんはあたまがいいからちゃんとやりとげてくれるとしんじてます。ぼくだってまたともだちをなくすのはさみしいからね。
[WIP]CSSの命名について
下書き供養 Advent Calendar 2018の9日目の記事です。
CSSの命名規則じゃない命名についての体系的な何かができないかを考えていた。どういう要素に命名するためにどのように言葉を選定するのか、命名という切り口で具体的に説明する文書みたいなものを見たことない気がする。
きっかけはクラス名に使える単語リストみたいなのが流れてきたのを見て感じたなんか違う感。それはそれでわかるんだけど、そもそもどういった場合にどのような種類の言葉を探すのか、要素をどの視点から捉えて説明するのかみたいな話が先にあった方が良さそうに思う。
一旦コンポーネント名の命名に基準を合わせて、UI的な命名とドメイン的な命名に分解できるのではと考えた。有名なところではMCSSのベースとプロジェクト、及びそれに影響を受けたであろうFLOCSSのComponentとProjectのレイヤー分けに近いと思う。ただしこれらはレイヤーとして分離した先にある個々のコンポーネント名についての詳細な説明がない。どちらのレイヤーに属するのかわからないと言われるのもその辺が鍵になってきそう。
- CardとかModalとかはUI的、NewsとかUserとかはドメイン的
- 特定のドメイン知識に依存しないコンポーネント、そのサイトの中では汎用的に使えるルールのコンポーネントはUI的な命名が妥当
- デザインの共通化が進むとUI的な命名が増えてくる
- コンポーネントを小さく砕いて共通化していくとUI的な命名に向かいやすい
- 規模によってはドメイン的である方がネームスペース的には安全である可能性が高い
- 同じ機能が出現する場所はだいたい決まっているし、だいたい同じUIで表現される
- カンプよりワイヤーフレームの方が一貫性があることが多い、表現することの密度の問題
- ドメイン的な命名はスケールしない、反面捨てやすい
- UI的な命名とドメイン的な命名を掛け合わせることでより端的な命名に向かうこともある
- Articleとかどっち側? 記事というUIで記事というデータ型。そのデータ型が特定のドメイン特有のものでないのであればUI的と言っても良い?
- CardというのかTeaserというのかとかはUI側だけどそれぞれ別の側面から見た視点っぽい。Cardはパターンっぽい、Teaserは用法っぽい。対応する両方はイコールじゃなくて全然違いものを指す可能性があるけどイコールなこともある
- Primary buttonとBlue buttonとかも違うものだけど、ドメイン的ではないという意味では同じものとして説明できる、Blueっていう名前の商品を買わせるための専用ボタンなのであればドメイン的かもしれない
- ややこしい用語があったりするとなにがUI的な言葉でなにがドメイン的な言葉なのかプロジェクトごとにはっきりさせとく必要があることもありそう
しばらくはまとまる気配がない。いつか更新するかもしれないし永遠にしないかもしれない。
たまごまぜごはん
TKG Advent Calendar 2018の2日目の記事です。
人が卵かけご飯と言ったとき、僕の頭の中で連想されるのは「卵まぜご飯」と呼んだ方が適切であろうと思われるものだ。幼少期に母親の紹介を経ていわゆる卵かけご飯との出会いを果たしたとき、それは当たり前のように混ぜ合わされた後の状態になっていて、さも混ぜ合わされるという工程を経て初めて卵かけご飯という名を授かるのだと思わされた。
ご飯とおかずは均等な味の比率を保っていなければならない。当時の僕は暗黙的にこのようなルールを遵守していた。特定のおかずにご飯と合う種類の味がどの程度含まれているかによって同時に口に含むべきご飯の量は決まる。これは単なる目安というより、可能な限り守られなければならない規定だった。特定のおかずに対して必要な量のご飯が用意されていないとき、味の総量としてご飯を超えるだけのおかずは食べることができない。そしてそのルールを犯してしまうことに強い罪悪感があった。
チキンライスやチャーハンなどの色付きご飯は例外として扱われた。ルールに則るならば、その味付けの量が対応するご飯の量を超えている場合は均等になるだけの「白いご飯」を増やして中和するというのが筋だ。しかし僕の家庭では色付きご飯の日には炊飯器の釜は「洗い待ち」状態で食事が開始された。これは色付きご飯の日には白いご飯を提供する意思がないということを示す。僕はこの色付きご飯の場合にはルールに当てはまらないとして自分を納得させた。従って色付きご飯の日に白いご飯を食べることができる状態になっていても味を中和させることは必要ではない。
他に少し複雑な対応として、ちょっと茶色いキノコ混ぜ炊き込みご飯のような、色付きご飯ではありながら味の比率としてご飯の方が多いという薄い色付きご飯パターンもあった。これまでのルールに則れば、薄い色付きご飯にはおかずの味の量が不足しているためその分の別のおかずが入り込む余地があるはずだ。つまり特定のおかずを食べるときに、白いご飯の場合に比べれば必要なご飯の量は増えるが、薄い色付きご飯でも味の比率を均等にするルールを守ることができる。しかし当時の僕には別のルールとして、別の種類のおかずの味を混ぜてはならないという決まりがあった。そのため薄い色付きご飯を別のおかずと同時に食べると規定違反になる。
ただそうはいってもこれではその日の食事を食べることができない。これに対する対策として、僕は薄い色付きご飯は白いご飯と同一であるというように解釈した。このご飯には一切の色などなくて、白いご飯とまったく変わりがないのだと。もちろんこの解釈には無理があった。強く念じようとも薄い色付きご飯はやはりおかずとしての佇まいを残しているのだ。僕はこれに対する打開策を講じることができず、薄い味付けご飯の日は「捨て」であるということにした。
こうした背景を振り返って自分はなぜ卵かけご飯は卵混ぜご飯であると捉えているのかが分析できてきた。まず卵の味がご飯全体に均等に浸透していなければ食べることができない。それに塩や醤油が混ざると色付きご飯のパターンとして取り扱われる。そして色付きご飯であるということはこれまでのあらゆるルールの免罪符となり無限の進化の可能性を獲得するのだ。