[翻訳]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の開発者もネイティブの組み込み機能を提供して、誰もがポリフィルをドロップできるようになるだろう。