再利用可能なコンポーネントの聖杯:カスタム要素、Shadow DOM、NPM
公開: 2022-03-10最も単純なコンポーネントでさえ、人的労力のコストはかなりのものだったかもしれません。 UXチームはユーザビリティテストを行います。 多数の利害関係者が設計を承認する必要があります。
開発者は、ABテスト、アクセシビリティ監査、単体テスト、およびクロスブラウザチェックを実施します。 問題を解決したら、その努力を繰り返したくありません。 (すべてを最初から構築するのではなく)再利用可能なコンポーネントライブラリを構築することで、過去の取り組みを継続的に活用し、すでに解決された設計および開発の課題を再検討することを回避できます。
コンポーネントの武器を構築することは、すべてが共通のブランドを共有するWebサイトのかなりのポートフォリオを所有しているGoogleなどの企業にとって特に役立ちます。 UIを構成可能なウィジェットにコード化することで、大企業は開発時間を短縮し、プロジェクト全体で視覚的デザインとユーザーインタラクションデザインの両方の一貫性を実現できます。 過去数年間、スタイルガイドとパターンライブラリへの関心が高まっています。 複数の開発者と設計者が複数のチームに分散していることを考えると、大企業は一貫性を実現しようとしています。 単純な色見本よりもうまくいくことができます。 必要なのは、簡単に配布できるコードです。
コードの共有と再利用
コードを手動でコピーして貼り付けるのは簡単です。 ただし、そのコードを最新の状態に保つことは、メンテナンスの悪夢です。 したがって、多くの開発者は、プロジェクト間でコードを再利用するためにパッケージマネージャーに依存しています。 その名前にもかかわらず、ノードパッケージマネージャーはフロントエンドパッケージ管理のための比類のないプラットフォームになりました。 現在、NPMレジストリには700,000を超えるパッケージがあり、毎月数十億のパッケージがダウンロードされています。 package.jsonファイルを含むすべてのフォルダーは、共有可能なパッケージとしてNPMにアップロードできます。 NPMは主にJavaScriptに関連付けられていますが、パッケージにはCSSとマークアップを含めることができます。 NPMを使用すると、コードを簡単に再利用でき、重要なことに更新できます。 無数の場所でコードを修正する必要はなく、パッケージ内のコードのみを変更します。
マークアップの問題
SassとJavascriptは、importステートメントを使用して簡単に移植できます。 テンプレート言語はHTMLに同じ機能を提供します—テンプレートはHTMLの他のフラグメントをパーシャルの形でインポートできます。 たとえば、フッターのマークアップを1回だけ記述してから、他のテンプレートに含めることができます。 テンプレート言語が多数存在すると言うのは控えめな表現です。 1つだけに縛られると、コードの潜在的な再利用性が大幅に制限されます。 別の方法は、マークアップをコピーして貼り付け、スタイルとjavascriptにのみNPMを使用することです。
これは、FinancialTimesがOrigamiコンポーネントライブラリを使用して採用したアプローチです。 彼女の講演では、「ブートストラップのようにすることはできませんか?」 Alice Bartlettは、「人々がプロジェクトにテンプレートを含めることができるようにする良い方法はありません」と結論付けました。 ロンリープラネットでコンポーネントライブラリを保守した経験について、IanFeatherはこのアプローチの問題を繰り返しました。
「彼らがそのコードをコピーすると、彼らは本質的に無期限に維持される必要があるバージョンをカットしています。 彼らが動作中のコンポーネントのマークアップをコピーしたとき、その時点でのCSSのスナップショットへの暗黙のリンクがありました。 その後、テンプレートを更新するか、CSSをリファクタリングする場合は、サイトに散在するテンプレートのすべてのバージョンを更新する必要があります。」
解決策:Webコンポーネント
Webコンポーネントは、JavaScriptでマークアップを定義することにより、この問題を解決します。 コンポーネントの作成者は、マークアップ、CSS、およびJavascriptを自由に変更できます。 コンポーネントの利用者は、コードを手動で変更するプロジェクトをトロールする必要なしに、これらのアップグレードの恩恵を受けることができます。 プロジェクト全体の最新の変更との同期は、ターミナルを介した簡潔なnpm update
で実現できます。 コンポーネントの名前とそのAPIのみが一貫性を保つ必要があります。
Webコンポーネントのインストールは、ターミナルにnpm install component-name
と入力するのと同じくらい簡単です。 Javascriptはimportステートメントに含めることができます。
<script type="module"> import './node_modules/component-name/index.js'; </script>
次に、マークアップのどこでもコンポーネントを使用できます。 これは、テキストをクリップボードにコピーする簡単なサンプルコンポーネントです。
フロントエンド開発へのコンポーネント中心のアプローチは、FacebookのReactフレームワークによって導入され、ユビキタスになりました。 必然的に、最新のフロントエンドワークフローにおけるフレームワークの普及を考えると、多くの企業が、選択したフレームワークを使用してコンポーネントライブラリを構築しています。 これらのコンポーネントは、その特定のフレームワーク内でのみ再利用できます。
大企業が統一されたフロントエンドを持っていることはまれであり、あるフレームワークから別のフレームワークへの複製は珍しいことではありません。 フレームワークは行き来します。 プロジェクト全体で最大限の潜在的な再利用を可能にするには、フレームワークに依存しないコンポーネントが必要です。
「私は何年にもわたって、Dojo、Mootools、Prototype、jQuery、Backbone、Thorax、Reactを使用してWebアプリケーションを構築してきました...私と一緒に奴隷にしたキラーDojoコンポーネントをReactに持ち込めたらいいのにと思います。今日のアプリ。」
— Google、エンジニアリングディレクター、Dion Almaer
Webコンポーネントについて話すときは、カスタム要素とシャドウDOMの組み合わせについて話します。 カスタム要素とシャドウDOMは、W3CDOM仕様とWHATWGDOM標準の両方の一部です。つまり、WebコンポーネントはWeb標準です。 カスタム要素とシャドウDOMは、今年、クロスブラウザーサポートを実現するためにようやく設定されました。 ネイティブWebプラットフォームの標準部分を使用することで、コンポーネントがフロントエンドの再構築とアーキテクチャの再考という急速に変化するサイクルに耐えられるようにします。 Webコンポーネントは、任意のテンプレート言語および任意のフロントエンドフレームワークで使用できます。これらは、真に相互互換性があり、相互運用可能です。 Wordpressブログからシングルページアプリケーションまで、どこでも使用できます。
Webコンポーネントの作成
カスタム要素の定義
タグ名を作成し、そのコンテンツをページに表示することは常に可能でした。
<made-up-tag>Hello World!</made-up-tag>
HTMLは、フォールトトレラントになるように設計されています。 上記は、有効なHTML要素ではありませんが、レンダリングされます。 これを行う正当な理由はありませんでした—標準化されたタグから逸脱することは伝統的に悪い習慣でした。 ただし、カスタム要素APIを使用して新しいタグを定義することにより、組み込み機能を備えた再利用可能な要素でHTMLを拡張できます。 カスタム要素の作成は、Reactでコンポーネントを作成するのとよく似ていますが、ここではHTMLElement
を拡張していました。
class ExpandableBox extends HTMLElement { constructor() { super() } }
パラメーターなしのsuper()
の呼び出しは、コンストラクターの最初のステートメントである必要があります。 コンストラクターを使用して、初期状態とデフォルト値を設定し、イベントリスナーを設定する必要があります。 新しいカスタム要素は、そのHTMLタグの名前と対応するクラスの要素で定義する必要があります。
customElements.define('expandable-box', ExpandableBox)
クラス名を大文字にするのは慣例です。 ただし、HTMLタグの構文は、単なる慣例ではありません。 ブラウザが新しいHTML要素を実装し、それをexpandable-boxと呼びたい場合はどうなりますか? 名前の衝突を防ぐために、新しい標準化されたHTMLタグにダッシュが含まれることはありません。 対照的に、カスタム要素の名前にはダッシュを含める必要があります。
customElements.define('whatever', Whatever) // invalid customElements.define('what-ever', Whatever) // valid
カスタム要素のライフサイクル
APIは、4つのカスタム要素リアクションを提供します。カスタム要素のライフサイクル内の特定のイベントに応答して自動的に呼び出されるクラス内で定義できる関数です。
connectedCallbackは、カスタム要素がDOMに追加されたときに実行されます。
connectedCallback() { console.log("custom element is on the page!") }
これには、Javascriptを使用した要素の追加が含まれます。
document.body.appendChild(document.createElement("expandable-box")) //“custom element is on the page”
また、HTMLタグを使用してページ内の要素を単純に含めることもできます。
<expandable-box></expandable-box> // "custom element is on the page"
リソースのフェッチやレンダリングを伴う作業はすべてここにあるはずです。
カスタム要素がDOMから削除されると、 disconnectedCallbackが実行されます。
disconnectedCallback() { console.log("element has been removed") } document.querySelector("expandable-box").remove() //"element has been removed"
adoptedCallback
は、カスタム要素が新しいドキュメントに採用されたときに実行されます。 あなたはおそらくこれについてあまり頻繁に心配する必要はありません。
attributeChangedCallback
は、属性が追加、変更、または削除されたときに実行されます。 これを使用して、 disabledやsrcなどの標準化されたネイティブ属性と、作成したカスタム属性の両方に対する変更をリッスンできます。 これは、ユーザーフレンドリーなAPIの作成を可能にするため、カスタム要素の最も強力な側面の1つです。
カスタム要素属性
非常に多くのHTML属性があります。 属性が変更されたときにブラウザがattributeChangedCallback
を呼び出す時間を無駄にしないように、リッスンする属性の変更のリストを提供する必要があります。 この例では、1つだけに関心があります。
static get observedAttributes() { return ['expanded'] }
これで、 attributeChangedCallback
は、カスタム要素の展開された属性の値を変更したときにのみ呼び出されます。これは、リストした唯一の属性であるためです。
HTML属性は対応する値( href、src、alt、valueなど)を持つことができますが、他の属性はtrueまたはfalse(たとえば、無効、選択、必須)のいずれかです。 対応する値を持つ属性の場合、カスタム要素のクラス定義に以下を含めます。
get yourCustomAttributeName() { return this.getAttribute('yourCustomAttributeName'); } set yourCustomAttributeName(newValue) { this.setAttribute('yourCustomAttributeName', newValue); }
この例の要素では、属性はtrueまたはfalseのいずれかになるため、getterとsetterの定義は少し異なります。
get expanded() { return this.hasAttribute('expanded') } // the second argument for setAttribute is mandatory, so we'll use an empty string set expanded(val) { if (val) { this.setAttribute('expanded', ''); } else { this.removeAttribute('expanded') } }
ボイラープレートが処理されたので、 attributeChangedCallback
を利用できます。
attributeChangedCallback(name, oldval, newval) { console.log(`the ${name} attribute has changed from ${oldval} to ${newval}!!`); // do something every time the attribute changes }
従来、Javascriptコンポーネントの構成には、 init
関数への引数の受け渡しが含まれていました。 attributeChangedCallback
を利用することにより、マークアップだけで構成可能なカスタム要素を作成することができます。
Shadow DOMとカスタム要素は別々に使用でき、カスタム要素はすべてそれ自体で役立つ場合があります。 Shadow DOMとは異なり、それらはポリフィルすることができます。 ただし、2つの仕様は連携して機能します。
ShadowDOMを使用したマークアップとスタイルの添付
これまで、カスタム要素の動作を処理してきました。 ただし、マークアップとスタイルに関しては、カスタム要素は空のスタイルなしの<span>
と同等です。 HTMLとCSSをコンポーネントの一部としてカプセル化するには、シャドウDOMをアタッチする必要があります。 これは、コンストラクター関数内で行うのが最善です。
class FancyComponent extends HTMLElement { constructor() { super() var shadowRoot = this.attachShadow({mode: 'open'}) shadowRoot.innerHTML = `<h2>hello world!</h2>` }
モードの意味を理解する必要はありません。含める必要のある定型文ですが、ほとんどの場合、 open
。 この単純なサンプルコンポーネントは、テキスト「helloworld」をレンダリングするだけです。 他のほとんどのHTML要素と同様に、カスタム要素には子を含めることができますが、デフォルトではできません。 これまでに定義した上記のカスタム要素は、画面に子をレンダリングしません。 タグの間にコンテンツを表示するには、 slot
要素を使用する必要があります。
shadowRoot.innerHTML = ` <h2>hello world!</h2> <slot></slot> `
スタイルタグを使用して、CSSをコンポーネントに適用できます。
shadowRoot.innerHTML = `<style> p { color: red; } </style> <h2>hello world!</h2> <slot>some default content</slot>`
これらのスタイルはコンポーネントにのみ適用されるため、スタイルがページの他の部分に影響を与えることなく、要素セレクターを自由に使用できます。 これにより、CSSの記述が簡素化され、BEMのような命名規則が不要になります。
NPMでのコンポーネントの公開
NPMパッケージは、コマンドラインを介して公開されます。 ターミナルウィンドウを開き、再利用可能なパッケージに変換するディレクトリに移動します。 次に、ターミナルに次のコマンドを入力します。
- プロジェクトにまだpackage.jsonがない場合は、
npm init
がパッケージの生成をガイドします。 -
npm adduser
は、マシンをNPMアカウントにリンクします。 既存のアカウントがない場合は、新しいアカウントが作成されます。 -
npm publish
すべてがうまくいけば、NPMレジストリにコンポーネントがあり、自分のプロジェクトにインストールして使用する準備ができており、世界中で共有できます。
WebコンポーネントAPIは完璧ではありません。 カスタム要素は現在、フォーム送信にデータを含めることができません。 プログレッシブエンハンスメントストーリーは素晴らしいものではありません。 アクセシビリティへの対処は、本来あるべきほど簡単ではありません。
当初は2011年に発表されましたが、ブラウザのサポートはまだ普遍的ではありません。 Firefoxのサポートは今年後半に予定されています。 それにもかかわらず、いくつかの有名なWebサイト(Youtubeなど)はすでにそれらを利用しています。 現在の欠点にもかかわらず、普遍的に共有可能なコンポーネントの場合、それらは唯一のオプションであり、将来的には、それらが提供するものにエキサイティングな追加が期待できます。