こんにちは フロントエンド開発ではWebコンポーネントを使用します。 この記事では、Webコンポーネントのすべての機能と、現在の使用方法について、まだ高度なサポートを考慮して説明します。
Webコンポーネントについて簡単に説明します。これは、ライブラリや花火を接続せずに、Web上のスタイルとスクリプトをネイティブにカプセル化するコンポーネントアプローチを使用できるテクノロジのセットです。 標準が通常のReactまたはAngularの代わりに提供するものに興味があり、古いブラウザ用に開発するときにそれを使用する方法に興味がある場合は、catを求めます。
詳細な研究のための資料のリストは、記事の最後にあります。
内容:
エントリー
私は主要な国際キャンペーンの1つでフロントエンドサービス開発者として働いており、現在、プロジェクトのフロントエンドを2度目に書き換えています。
1C-Bitrixの規範に従って書かれた最初のバージョンでは、多くのテンプレートのスクリプトとスタイルからの肉が私を待っていました。 もちろん、スクリプトはjQueryであり、スタイルは構造や順序がなく、完全に混chaとしたものです。 新しいプラットフォームへの移行に関連して、プロジェクトを完全に書き直す機会がありました。 秩序を回復するために、BEM手法を使用して独自のコンポーネントシステムを開発しました。 マークアップ内のBEMブロックのインスタンスごとに、ページの読み込み後に、対応するクラスのオブジェクトが1つ作成され、ロジックの制御が開始されます。 したがって、すべてが非常に厳密にシステム化されています-ブロック(ロジックとスタイル)は再利用可能で、互いに分離されています。
プロジェクトのサポートとファイナライズの1年後、このようなシステムの多くの欠点が明らかになりました。 私の擬似コンポーネントが機能するマークアップは、開発者の注意と「正直な言葉」に基づいています。JSは、レイアウト設計者が必要なすべての要素を正しく配置し、それらにクラスを割り当てることを望みます。 コンポーネントが他のBEMブロックを含むDOMを変更するか、マークアップがajaxを介してロードされる場合、このマークアップのコンポーネントは手動で初期化する必要があります。 二人目がプロジェクトに登場するまで、それはすべて毎日の作業で簡単に見えました。 残念ながら、ドキュメントは非常に膨大でしたが、基本的な原則のみをカバーしていました(この場合のボリュームはマイナスでした)。 人生では、左へのステップまたは右へのステップが「原理」を打ち破り、完成したコンポーネントを読むことは混乱するだけでした。
このすべてと、SPA / PWAへの計画的な移行中の開発上の問題の数の潜在的な増加に加えて、前線の別の再設計が促されました。 とりわけ私のコンポーネントシステムである自転車は、何か(私の場合はJS)を学習する際に非常に役立ちますが、複数の開発者がいる高品質のプロジェクトでは、より信頼性が高く構造化されたものが必要です。 現在(ただし、長い間)Webには多くのフレームワークがありますが、その中から選択するものがあります:Preactは、既におなじみのReactと開発においてサイズが小さく、最大の類似性を提供します。角を曲がったところから、そのシンプルさなどを誇っています。 標準は際立っています:既に記述されたマークアップに人為的に(BEMおよび追加のJSによって)リンクする必要のないワイヤードロジックとスタイルを使用して、再利用可能なWebコンポーネントを作成できることがわかります。 そして、これらはすべて箱から出して動作するはずです。 奇跡ですね。
私の標準に対する強い愛情と明るい未来への信念、および既存のコンポーネントシステムとWebコンポーネント(孤立したロジックを持つ1つの「コンポーネント」に対する1つのJSクラス)の類似性により、Webコンポーネントを使用することにしました。 最も厄介なことは、ブラウザーでのこのビジネスのサポートでした.Webコンポーネントは若すぎて、幅広いブラウザーをカバーできず、市販製品(Android 4.4ストックブラウザーやInternet Explorer 11など)でサポートする必要があるブラウザーをカバーできません。 私に期待されるある程度の痛みと制限、および開発に適合することに同意するフレームワークは精神的に受け入れられ、私は理論と実践的実験の研究に突入しました:ウェブコンポーネントのフロントを書き、それを生産にロールする方法うまくいきました。
記事を読むために必要な理論上の最小:DOMツリーでの基本的な操作のレベルでの純粋なJavaScript、ES2015のクラスの構文の理解、さらにReact.js / Angular / Vue.jsカテゴリーのフレームワークのいずれかを熟知していること
理論
復習
Webコンポーネントは、宣言的に記述された再利用可能な「ウィジェット」を、独自のタグの形式で分離されたスタイルとスクリプトで作成できる一連の標準です。 標準は独立して開発され、条件付きではなくWebコンポーネントにリンクされます-原則として、個別に使用される各テクノロジーを使用できます。 しかし、正確に組み合わせると最も効果的です。
通常、4つの標準(カスタム要素、シャドウDOM、HTMLテンプレート、およびHTMLインポート)はすべて個別に考慮され、結合されます。 個別に有用性が低いため、標準のすべての可能性を累積的に検討し、以前に研究したものに追加します。
Webコンポーネントはかなり新しい技術であり、標準には多くの変更が加えられていることを思い出してください。 私たちにとって、これは主にカスタム要素とシャドウDOM標準のいくつかのバージョンv0
およびv1
表現されています。 現在、 v0
は関係ありません。 v1
のみを検討します。 追加の資料を検索するときは注意してください! v1
バージョンは2016年にのみ形成されました。つまり、2016年までのすべての記事とビデオは、仕様の古いバージョンについて話すことが保証されています。
カスタム要素
カスタム要素は、たとえば<youtube-player src=""></youtube-player>
または<yandex-map lat="34.86974" lon="-111.76099" zoom="7"></yandex-map>
ように、任意の名前と動作を持つ新しいHTMLタグを作成する機能<yandex-map lat="34.86974" lon="-111.76099" zoom="7"></yandex-map>
。
カスタムアイテムを登録する
もちろん、独自のタグを「作成」できます(たとえば、ブラウザーは<noname></noname>
を正しく処理します)が、同時に要素はDOMツリーのHTMLUnknownElement
クラスのオブジェクトとして登録され、デフォルトの動作はありません。 そのような各要素を手動で「復活」させる必要があります。
カスタム要素の仕様により、ライフサイクルに従って新しいタグを登録し、その動作を設定できます-作成、DOMへの挿入、属性の変更、DOMからの削除。 新しいHTML標準タグとカスタムタグの競合を防ぐため、後者の名前には少なくとも1つのハイフンが含まれている必要があります。たとえば、 <custom-tag></custom-tag>
または<my-awesome-tag></my-awesome-tag>
。 また、ユーザータグは現在、自己終了することはできません。コンテンツのないタグであってもペアにする必要があります。
学習するための最良の方法は実践であるため、機能が<summary>
要素に似た要素を記述します。 <x-spoiler>
と呼びましょう。
そのような要素がDOMツリーに追加されると、その要素はHTMLUnknownElement
クラスのオブジェクトにもなります。 要素をカスタム要素として登録し、 customElements
動作を追加するには、グローバルcustomElements
オブジェクトのdefine
メソッドを使用する必要があります。 最初の引数はタグ名で、2番目は動作を説明するクラスです。 この場合、クラスはHTMLElement
クラスを拡張して、要素が他のHTML要素のすべての品質と機能を持つHTMLElement
します。 合計:
class XSpoiler extends HTMLElement {} customElements.define("x-spoiler", XSpoiler);
その後、ブラウザはマークアップ内のすべてのx-spoiler
タグをXSpoiler
ではなくHTMLUnknownElement
クラスのオブジェクトとして再作成します。 innerHTML
、 insertAdjacentHTML
、 append
またはHTMLを操作するための他のメソッドを介してドキュメントに追加されるすべての新しいx-spoiler
タグは、 XSpoiler
クラスに基づいてすぐに作成されます。 document.createElement
使用して、このようなDOM要素を作成することもできdocument.createElement
。
登録済みの名前で、または登録済みのクラスに基づいて要素を登録しようとすると、例外が発生します。
タグ名とクラス名はまったく一致する必要はありません。 したがって、必要に応じて、異なるタグの下に同じクラス名を持つ2つのユーザー要素を登録できます。
これで、ユーザー要素はもちろん登録されましたが、有用なことは何もありません。 カスタム要素を本当に活気付けるために、そのライフサイクルを考慮してください。
ユーザー要素のライフサイクル
コールバックメソッドを追加して要素を作成し、DOMに追加し、属性を変更し、DOMから要素を削除して、親ドキュメントを変更できます。 これを使用して、スポイラーのロジックを実装します。コンポーネントには、テキスト「Collapse」/「Expand」を含むボタンと、タグの元のコンテンツを含むセクションが含まれます。 セクションの可視性は、ボタンまたは属性値をクリックすることで制御されます。 ボタンテキストは、属性によってカスタマイズすることもできます。
要素を作成するためのコールバックは、クラスのコンストラクターです。 正しく動作させるためには、最初にsuper
経由で親コンストラクターを呼び出す必要があります。 コンストラクターでは、マークアップの設定、イベントハンドラーのハング、その他の準備作業を行うことができます。 コンストラクターでは、他のメソッドと同様に、 this
はDOM要素自体を参照し、カスタム要素がHTMLElement
拡張するという事実により、 this
はquerySelector
などのメソッドとquerySelector
などのプロパティがあります。
ボタンテキストの値、コンストラクターのコンポーネントマークアップを追加し、 opened
属性の存在を変更するハンドラーをボタンにアタッチopened
ます。
class XSpoiler extends HTMLElement { constructor() { super(); this.text = { "when-close": "", "when-open": "", } this.innerHTML = ` <button type="button">${this.text["when-close"]}</button> <section style="display: none;">${this.innerHTML}</section> `; this.querySelector("button").addEventListener("click", () => { const opened = (this.getAttribute("opened") !== null); if (opened) { this.removeAttribute("opened"); } else { this.setAttribute("opened", ""); } }); } }
コンストラクターの各部分を詳細に分析します。
super()
は、 HTMLElement
クラスのコンストラクターを呼び出しHTMLElement
。 この場合、要素コンストラクターが必要な場合、これは必須アクションです。
this.text
this
はオブジェクトなので、独自のプロパティを追加できます。 この場合、ボタンに表示されるtext
オブジェクトに補助テキストを保存します。
this.innerHTML
、DOM要素のマークアップthis.innerHTML
設定します。 そうする際に、少し高いテキストを使用します。
this.querySelector("button").addEventListener
は、 opened
属性を設定またはクリアするボタンのclick
イベントハンドラーを追加opened
ます。 論理値として使用します-スポイラーは開いているか閉じているため、属性は存在するかしないかのどちらかです。 ハンドラーでは、 null
との比較を通じて属性の存在を確認し、属性を設定または削除します。
作成されたボタンをクリックすると、 opened
属性が変更されます。 これまでのところ、属性を変更しても何も起こりません。 このトピックに進む前に、コードを少し変更します。
<button>
は、ボタンを無効にするdisabled
属性があることに注意してください。 マークアップに書き込むと、ボタンはアクティブでなくなり、削除すると再びクリック可能になります。 getAttribute
、 setAttribute
、およびremoveAttribute
メソッドを使用して、JavaScriptコードの属性を操作することもできます。 しかし、これはあまり便利ではありません。属性を操作するには3つのメソッドが必要です。それらは長く、さらに文字列のみで機能します(属性の値は常に文字列です)。 したがって、DOM要素は、多くの場合、同じ名前のプロパティの属性の「リフレクション」を使用します。 そのため、 button.disabled
プロパティは属性の有無を返します。 次に、属性を使用した直接作業とプロパティを使用した2つのアプローチを比較します。
// : // : const isDisabled = button.getAttribute("disabled") !== null; // : const isDisabled = button.disabled; // : // : button.setAttribute("disabled", ""); // : button.disabled = true; // : // : button.removeAttribute("disabled"); // : button.disabled = false; // : // : if (button.getAttribute("disabled") !== null) { button.removeAttribute("disabled"); } else { button.setAttribute("disabled", ""); } // : button.disabled = !button.disabled;
同意して、プロパティを操作する方がはるかに便利ですか? 属性をopened
た状態で同じメカニズムを実装し、その値を簡単に取得して設定できるようにします。 これを行うために、クラス内のプロパティのゲッターとセッターの機能を使用します。
class XSpoiler extends HTMLElement { constructor() { super(); this.text = { "when-close": "", "when-open": "", } this.innerHTML = ` <button type="button">${this.text["when-close"]}</button> <section style="display: none;">${this.innerHTML}</section> `; this.querySelector("button").addEventListener("click", () => { this.opened = !this.opened; }); } get opened() { return (this.getAttribute("opened") !== null); } set opened(state) { if (!!state) { this.setAttribute("opened", ""); } else { this.removeAttribute("opened"); } } }
文字列プロパティの場合(要素のインラインid
やリンクのhref
など)、ゲッターとセッターは少しシンプルに見えますが、アイデアは残ります。
また、このような「リフレクション」がパフォーマンスの点で必ずしも有用ではない場合があることも追加できます。 たとえば、フォーム要素では、 value
属性の動作が異なります。
これで属性ができました。ボタンをクリックするとその値を変更できますが、それ以上の有用なアクションは発生しません。 要素を非表示にしてクリックハンドラに直接表示するための便利なコードを追加できますが、たとえば、別の外部JSコードで要素の可視性を変更するのは非常に困難です。
代わりに、 attributeChangedCallback
メソッドを使用して、ハンドラーをアタッチして属性値を変更できattributeChangedCallback
。 これは、属性が変更されるたびに呼び出されるため、属性を使用して、内部と外部の両方からコンポーネントを制御できます。
このメソッドは、属性名、古い値、新しい値の3つのパラメーターを使用します。 このメソッドを呼び出して絶対にすべての属性を変更することはパフォーマンスの観点からは非合理的であるため、現在のクラスのobservedAttributes
静的配列にリストされているプロパティを変更する場合にのみ機能します。
このコンポーネントは、3つの属性( opened
、 text-when-open
、 text-when-close
の変更に応答する必要がありtext-when-close
。 最初はスポイラーの表示に影響し、他の2つはボタンのテキストを制御します。 まず、これらの属性の名前をobservedAttributes
静的配列に追加します。
static get observedAttributes() { return [ "opened", "text-when-open", "text-when-close", ] }
次に、 attributeChangedCallback
メソッド自体を追加しattributeChangedCallback
。これは、変更された属性に応じて、コンテンツの可視性を変更してボタンテキストを表示するか、必要に応じてボタンテキストを変更して表示します。 これを行うには、メソッドの最初の引数でswitch
を使用します。
attributeChangedCallback(attrName, oldVal, newVal) { switch (attrName) { case "opened": const opened = newVal !== null; const button = this.querySelector("button"); const content = this.querySelector("section"); const display = opened ? "block" : "none"; const text = this.text[opened ? "when-open" : "when-close"]; content.style.display = display; button.textContent = text; break; case "text-when-open": this.text["when-open"] = newVal; if (this.opened) { this.querySelector("button").textContent = newVal; } break; case "text-when-close": this.text["when-close"] = newVal; if (!this.opened) { this.querySelector("button").textContent = newVal; } break; } }
attributeChangedCallback
メソッドは、必要な属性が最初に要素に存在する場合でも機能することに注意してください。 つまり、 opened
属性を使用してコンポーネントをマークアップにすぐに挿入すると、スポイラーが実際に開かれます。 attributeChangedCallback
は、 constructor
直後に機能します。 したがって、コンストラクターで属性の初期値を処理するために追加の作業は必要ありません(もちろん、属性が追跡されない限り)。
これで、コンポーネントが実際に機能します! ボタンをクリックすると、 opened
属性の値が変化し、その後attributeChangedCallback
コールバックがトリガーされ、コンテンツの可視性が制御されます。 属性とattributeChangedCallback
状態管理により、初期状態(オープンスポイラーを表示する場合はマークアップにopenを追加できます)を制御したり、外部から状態を管理したりできます(他のJSコードで要素の属性を設定または削除でき、これは正しく処理されます) 。 ボーナスとして、コントロールボタンのテキストをカスタマイズできます。 結果のデモは、 新鮮なChromeで見てください!
主な機能の準備が整いました。これらは、カスタム要素の最も一般的に使用される機能です。 次に、使用頻度の低いコールバックについて検討します。
connectedCallback
メソッドがトリガーされ、DOMツリーに要素が挿入されます。 登録時に要素がすでにマークアップにあった場合、またはHTML文字列を挿入して作成された場合、 constructor
は必要に応じて順番に機能しattributeChangedCallback
、次にconnectedCallback
。 このコールバックは、たとえば、DOMツリーの親に関する情報を知る必要がある場合、または要素を使用する直前にコンポーネントを最適化し、重いコードを延期する場合に使用できます。 ただし、留意すべき2つの点があります。1つは、 constructor
が1つの要素に対して1回起動する場合、その後、要素がDOMに挿入されるたびにconnectedCallback
起動することです。コンストラクターからconnectedCallback
、これはエラーにつながる可能性があります。 このメソッドは、イベントハンドラーの割り当てや、サーバーへの接続などのその他の負荷の高い操作に使用できます。
DOMでの要素の挿入を追跡できるように、削除を追跡できます。 disconnectedCallback
メソッドがこれを担当します。 たとえば、 remove()
メソッドを使用してDOMから要素が削除される場合に機能します。 注:要素がDOMツリーから削除されているが、その要素へのリンクがある場合は、再びDOMに挿入でき、 connectedCallback
が再び機能します。 削除する場合、たとえば、コンポーネント内のデータの更新を停止したり、タイマーを削除したり、 connectedCallback
に割り当てられたイベントハンドラーを削除したり、サーバーへの接続を閉じたりできます。 disconnectedCallback
はコードの実行を保証しないことに注意してください-たとえば、ユーザーがページを閉じたとき、メソッドは呼び出されません。
最もまれに使用されるコールバックは、 adoptedCallback
メソッドです。 要素がownerDocument
プロパティを変更すると起動します。 これは、たとえば、新しいウィンドウを作成してそこにアイテムを移動した場合に発生します。
ユーザー要素の相互作用
すでに理解したように、コンポーネントは属性の値によって直接またはプロパティを介して制御されます。 ただし、コンポーネントから外部にデータを転送するには、 CustomEvents
を使用できます。 このコンポーネントの場合、状態変更イベントを追加して、リッスンして反応できるようにするのが合理的です。 これを行うには、2つのCustomEvent
オブジェクトを使用して、コンストラクターにevents
プロパティを追加します。
this.events = { "close": new CustomEvent("x-spoiler.changed", { bubbles: true, detail: {opened: false}, }), "open": new CustomEvent("x-spoiler.changed", { bubbles: true, detail: {opened: true}, }), };
また、 opened
が変更されたときにイベントがディスパッチされるようにattributeChangedCallback
編集しattributeChangedCallback
。
this.dispatchEvent(this.events[opened ? "open" : "close"]);
これで、興味のあるイベントを聞いて、スポイラーの状態の変化について知ることができます。
新しいデモ 。
customElementsについてもう少し
要素を登録するときに、グローバルcustomElements
オブジェクトのdefine
メソッドを使用しました。 さらに、彼にはさらに2つの便利な方法があります。
customElements.get(name)
は、name name
(存在する場合)またはundefined
登録されたユーザー要素のコンストラクターを返します。
customElements.whenDefined(name)
は、名前がname
のアイテムが登録された場合、またはアイテムが既に登録されている場合はすぐに成功するプロミスを返します。 これはawait
で特に便利ですが、それは別のトピックです。
カスタムアイテム拡張
ユーザー要素クラスから継承して、既存のものに基づいて新しいユーザー要素を作成できます。 たとえば、スポイラーを拡張し、必要に応じていくつかの機能を追加できます。 必要に応じて、 super.methodName()
を介して親クラスの同様のメソッドを呼び出すことを忘れないことが重要super.methodName()
コンストラクターとsuper()
場合は必要です)。
標準要素の拡張
仕様では、拡張機能と標準のHTMLタグが許可されています。 たとえば、独自のボタン実装が必要ですが、同時に、ブラウザボタンの既存の機能、たとえばdisabled
、 tabindex
、 type
などの属性をdisabled
た正しい操作を保持することをお勧めします。 ただし、多くの機能があります。
まず、クラスを宣言するとき、必要なタグのクラスを展開する必要があります。 ボタンの場合、これはHTMLButtonElement
クラスになります。 クラスの完全なリストは、仕様に記載されています 。
2番目に、3番目のパラメーターで要素を登録する場合、展開するタグを示すオプションオブジェクトを渡す必要があります(同じクラスは複数のタグに対応できます)。
3番目に、そのようなユーザー要素は、展開する必要がある通常のタグとして作成されますが、is属性はユーザー要素の名前と同じです。 要素がdocument.createElement
を介して作成された場合、2番目の引数のプロパティとして渡されます。
次のようになります。
class FancyButton extends HTMLButtonElement { } customElements.define("fancy-button", FancyButton, {extends: "button"});
// document.createElement let button = document.createElement("button", {is: "fancy-button"});
<!-- HTML --> <button is="fancy-button" disabled>Fancy button!</button>
登録前のアイテムのスタイリング
ただし、ユーザーがHTMLマークアップを取得し、JavaScriptコードをダウンロードして実行する方法には時間がかかります。 DOMでレンダリングされるが、まだ登録されておらず、適切に動作しないユーザー定義要素を何らかの方法でスタイル設定するには、 :defined
疑似:defined
使用できます。 最も簡単な使用例は、未登録のユーザー要素をすべて非表示にすることです。
*:not(:defined) { display: none; }
合計
カスタム要素技術に基づいて再利用可能なWebコンポーネントを作成しました。 ただし、多くの欠点があります。 したがって、たとえば、スタイルの分離はありません。 section {display: block !important}
ルールは、コンポーネントのロジックを簡単に壊してしまいます。 とにかく、スタイルをJSに直接ぶら下げるのは悪い音です。 スポイラーのコンテンツを変更することも困難ですinnerHTML
をinnerHTML
して新しいコンテンツをインストールすると、ボタン、セクション、クリックハンドラーが消えます。 コンテンツを実際に変更するには、コンポーネントの構造を知って考慮する必要があります。 また、マークアップはコンストラクターに直接保存されます。 これは明らかに、単純な再利用可能なコンポーネントに必要なものではありません。 これらのすべての欠点を修正するために、他の仕様を使用します。
シャドウDOM(シャドウDOM)
シャドウDOMの仕様は、環境と内部コンテンツからスタイルとレイアウトを分離する問題を解決します。
DOMカプセル化
私たちが使用する標準のDOMモデルは、要素のすべての子孫がchildNodes
を介してアクセス可能であると仮定し、 querySelector()
などを介してquerySelector()
ことができます。 DOM-パススルー。テキストの段落がどこにある場合でも、常にdocument.querySelectorAll("p")
介して検出されdocument.querySelectorAll("p")
。 ただし、DOMツリーにあるものではなく、他のマークアップを表示することが可能であるため、通常のchildNode
およびquerySelector
によって無視されます。 この動作の最も単純な例は、内部に複数の<source>
持つ<video>
です。 <source>
のみをDOMに追加すると、独自の分離マークアップ(ブロック、ボタンなど)を備えた本格的なビデオプレーヤーが表示されます。 画面に表示されるすべてのものは、シャドウDOMに配置されています。 どのように機能しますか?
DOM attachShadow()
. DOM- : Shadow DOM, Light DOM Flattened DOM. .
Light DOM — , DOM- : , innerHTML
, childNodes
querySelectorAll
.
Shadow DOM — DOM-, shadowRoot
. attachShadow
shadowRoot
, - innerHTML
, appendChild
DOM, this.shadowRoot
.
Flattened DOM — Shadow DOM Light DOM. , . , Shadow DOM. Light DOM element.innerHTML
, Shadow DOM element.shadowRoot.innerHTML
, Flattened DOM . Flattened DOM Shadow DOM . , Light DOM . 例:
<x-demo>!</x-demo>
class Demo extends HTMLElement { constructor() { super(); this.attachShadow({mode: "open"}); this.shadowRoot.innerHTML = " ..."; } } customElements.define("x-demo", Demo);
DOM, .. «!». , , « ...».
: Shadow DOM , Shadow DOM . open
. Shadow DOM shadowRoot
.
. <slot>
name
Shadow DOM slot
Light DOM. , Flattened DOM Lignt DOM. : ( 0, 1 ) slot
- Light DOM <slot>
name
Shadow DOM. - <slot>
, Shadow DOM. <slot>
name
, Light DOM slot
.
, Shadow DOM Light DOM Shadow DOM <slot>
, Light DOM <slot>
.
: «» ( ) Shadow DOM, Light DOM <slot>
. :
this.attachShadow({mode: "open"}); this.shadowRoot.innerHTML = ` <button type="button">${this.text["when-close"]}</button> <section style="display: none;"><slot></slot></section> `;
Shadow DOM, this.querySelector("button")
this.shadowRoot.querySelector("button")
. section
.
: , «» DOM- Light DOM. , , : .
, Light DOM slot
, <slot>
name
( ). Light DOM slot
, Shadow DOM <slot>
, name
( ). slot
- , <slot>
( ).
DOM Light DOM / Shadow DOM , .
. Shadow DOM <style>
, Shadow DOM. ,
section { height: 50%; width: 50%; }
section
, Shadow DOM. : section
Shadow DOM . . , .
: :host
. . , . , , .
, , , . Shadow DOM attributeChangedCallback
. .
- . : :host-context(.red)
, .red
. , , .
Light DOM, . ::slotted
, , - Light DOM. , . 例:
<name-badge> <h2>Eric Bidelman</h2> <span class="title"> Digital Jedi, <span class="company">Google</span> </span> </name-badge>
<style> ::slotted(h2) { margin: 0; font-weight: 300; color: red; } ::slotted(.title) { color: orange; } /* ( ). ::slotted(.company), ::slotted(.title .company) { text-transform: uppercase; } */ </style> <slot></slot>
, - , :host
:host-context
.
— :host
. , :host
. .
Shadow DOM CSS , , , , .
DOM
DOM DOM , . , DOM, , <video>
.
DOM JavaScript- , DOM {mode: "open"}
.
?
«» Shadow DOM, mode
closed
. Shadow DOM shadowRoot
, null
. shadowRoot
attachShadow()
, , shadowRoot
- . Shadow DOM, .
<slot>
slotchange
, Light DOM, . , .
<slot>
assignedNodes
, DOM- Light DOM, . DOM- {flatten: true}
, ( ).
Light DOM assignedSlot
, , .
(HTML Templates)
, HTML.
Shadow DOM . - , innerHTML
, - .
<template>
, HTML Templates. , , . , DOM-, , <script>
, <link>
, , querySelector
.
DOM- <template>
content
, DocumentFragment
, . , appendChild
.
, «» . : , . , DOM-, . .
HTML- (HTML Imports)
. , , , , . HTML- : HTML-, (, , ) <link rel="import" href="x-spoiler.html">
.
, html- <template>
<script>
. , , <script>
. html- , , : , , DOM , , document.getElementById
. DOM- html- import
link
. <link>
id, , import
template
, . <script>
const ownerDocument = document.currentScript.ownerDocument
, , , <template>
ownerDocument
, document
. . !
, . , ( ), <link rel="import">
. . , , , .
練習する
. HTML- :
- « ». — .
app.html
, , ? , 20 . , , , , . , . , , . - , . 100 , , n- . , , , .
- , HTTP/2. HTTP/2 , .
-
document.currentScript
. , IE11 . , , id id,<template>
, , , . - Firefox HTML- , . , - , Firefox, , .
, , . , . , HTML-. .
, IE
, Edge 12
Safari 7-
. .
v1 ( Chrome 54+
, Safari 10.1+
) DOM v1 ( Chrome 53+
, Safari 10+
). .
WebComponents/webcomponentsjs . , , webcomponents-sd-ce.js
, DOM, HTML-.
, , : , — ShadyDOM — ShadyCSS DOM. . : <template>
.
— Promise
, CustomEvent
, Object.assign()
, Array.from()
. - , webcomponents-platform es6-promise , polyfill.io .
UPD: . Element.prototype.insertAdjacentHTML
, . , - , issue .
WebReflection , ( Firefox). , , , .
, Babel
, .
, :
,
. . :
- Internet Explorer 11+
- Edge 12+
- Firefox 35+
- Chrome 26+
- Safari 6.1+
- iOS- 8+
- Android browser 4.4+
:
- (HTML-),
<button is="fancy-button">button</button>
.WebComponents
. -
adoptedCallback
. -
:defined
, , . -
:host-context
Safari, . - []
assignedNodes
<slot>
, . issue , .
, , . , .
— . Babel extends
, Chrome , , ES5-. , custom-elements-es5-adapter.js
, , . , . ES5-, - ( ), ( ).
DOM , :
<div id="custom-elements-adapter-test"> <script> (function() { if (isNative(window.customElements.define) === false) { // , Element.prototype.remove() , // , // .parentNode.removeChild() document.getElementById("custom-elements-adapter-test").remove(); } function isNative(fn) { return (/\{\s*\[native code\]\s*\}/).test('' + fn); } })(); </script> <script src="https://cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/1.0.22/custom-elements-es5-adapter.js"></script> </div>
, — . — , . — , .
babel-plugin-transform-custom-element-classes . es5-adapter
, IE11 , Reflect.construct
. , , , . , , — . : , babel-polyfill
. . , .
— babel-plugin-transform-builtin-classes WebReflection
. , , IE11 - , . , WebComponents
. — WebReflection, WebComponents . .
, , — Babel 7, . . , Chrome ( ), IE11, Reflect.construct
.
: DOM ES5-, , . , — .
constructor
: IE Safari HTMLUnknownElementConstructor
, . .
DOM
, . -, ShadyCSS.prepareTemplate()
, , — , . <template id="x-spoiler">
<x-spoiler>
, ShadyCSS.prepareTemplate(document.getElementById("x-spoiler"), "x-spoiler")
.
: ShadyCSS.styleElement(this)
;
:host()
: :host(.zot)
:host(.zot:not(.bar))
, :host(.zot:not(.bar:nth-child(2)))
— .
::slotted
: . , ::slotted(span)
, .header ::slotted(span)
.
— <template>
. , DOMContentLoaded
, , customElements.define()
, DOM-, content
template
. : customElements.define()
DOMContentLoaded
, customElements.define()
. . 次のようになります。
try { HTMLTemplateElement.bootstrap(document); } catch (e) { }
<template>
DOM , : <template>
DOM- . .
<script>
: , <template>
- : IE . . , - .
gulp
, , : html
. <script>
, . ( ) <template>
, DOM HTML. <template>
id
, . DOM, <style>
, CSS. , HTML , , <style>
, css.
, , . これには次のものが必要です。
-
templates.html
-
PostCSS
-
app.js
-
Babel
gulp
, , gulpfile
, . github . :
src
. html
- (, ), scaffolding.js
js- components
-.
index.html
@@templates
, -, -, , es5- , app.js
-, .
scaffolding.js
, DOM ( ).
. : .
scripts
<script>
app.js
:
gulp.task("scripts", () => // gulp.src("src/components/*.html") // .pipe(concat("app.js")) // . , // jsdom, api // <script> .pipe(insert.transform(content => { const document = (new JSDOM(content)).window.document; const scriptsTags = document.querySelectorAll("script"); const scriptsContents = Array.prototype.map.call(scriptsTags, tag => tag.textContent); return scriptsContents.join(""); })) // scaffolding.js .pipe(gap.prependFile("src/scaffolding.js")) // Babel .pipe(babel({ presets: ["env"] })) // .pipe(gulp.dest("dist")) )
templates
, <template>
:
gulp.task("templates", () => // gulp.src("src/components/*.html") // jsdom, <template> // data-, , // . .pipe(insert.transform((content, file) => { const componentName = path.basename(file.path, ".html"); const document = (new JSDOM(content)).window.document; const templatesTags = document.querySelectorAll("template"); templatesTags.forEach(template => template.setAttribute("data-component", componentName)); const templatesHTML = Array.prototype.map.call(templatesTags, tag => tag.outerHTML); return templatesHTML.join(""); })) // .pipe(concat("templates.html")) // gulp-html-postcss, html <style> .pipe(postcss([ autoprefixer() ])) // — html css .pipe(htmlmin({ collapseWhitespace: true, conservativeCollapse: true, minifyCSS: true, })) // .pipe(gulp.dest("dist")) );
html- @@templates
templates.html
, templates
. gulp, . , php include
. gulp .
, , data
-? Gulp . , , ShadyCSS.prepareTemplate()
. , scaffolding.js
:
document.querySelectorAll("template[data-component]").forEach(template => { ShadyCSS.prepareTemplate(template, template.dataset["component"]); });
以上です! -, gulp . .
, . , , HTMLElement
. x-component
, , . ( , ):
$
$$
. querySelector
querySelectorAll
Light DOM. Chrome DevTools, jQuery
.
fireEvent
, . CustomEvent
, , dispatchEvent
. , c .
// this.dispatchEvent(new CustomEvent(`x-timer.ended`, { bubbles: true })); // this.fireEvent(`x-timer.ended`);
is
localName
, — . , , ( ).
getTemplateCopy
- id
.
makeShadowRoot
DOM. , id
( this.is
) getTemplateCopy
. DOM ( ShadyCSS.styleElement(this);
), . $
$$
shadowRoot
.
// ShadyCSS.styleElement(this); this.attachShadow({ mode: "open" }); const template = document.getElementById("x-spoiler"); const templateClone = template.content.cloneNode(true); templateClone.querySelector("button").textContent = this.text["when-close"]; this.shadowRoot.appendChild(templateClone); // this.makeShadowRoot(); this.shadowRoot.$("button").textContent = this.text["when-close"];
. , properties
, , / , ( ), . , :
// , get opened() { return this.getAttribute("opened") !== null; } set opened(value) { if (!!value) { this.setAttribute("opened", ""); } else { this.removeAttribute("opened"); } } // , properties = { opened: { type: Boolean, }, }
, — , , ( ).
,
, , , - IE11, , ? Chrome, , es5- es5- -. .
, JS , - ( — ES), . , — . ? , . , , , 2018 75%. .
-, , ES5 ES6 . , , Babel scaffolding.js . :
gulp.task("scripts-es5", buildJS.bind(null, "es5")); gulp.task("scripts-es6", buildJS.bind(null, "es6")); function buildJS(mode) { return gulp.src("src/components/*.html") .pipe(concat(`app-${mode}.js`)) .pipe(insert.transform(content => { const document = (new JSDOM(content)).window.document; const scriptsTags = document.querySelectorAll("script"); const scriptsContents = Array.prototype.map.call(scriptsTags, tag => tag.textContent); return scriptsContents.join(""); })) .pipe(gulpif(mode === 'es5', gap.prependFile("src/scaffolding.js"))) .pipe(gulpif(mode === 'es5', babel({presets: ["env"]}))) .pipe(uglify()) .pipe(gulp.dest("dist")) }
, app-es5.js
app-es6.js
. , . , ́ , .
. , 150 ! . , , . , . . html:
(function () { var wcReady = ("attachShadow" in document.documentElement) && ('customElements' in window); var scripts; if (wcReady) { scripts = [ "./app-es6.js" ]; } else { scripts = [ "https://cdn.polyfill.io/v2/polyfill.js?features=default", "https://cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/1.0.22/webcomponents-sd-ce.js", "https://cdn.jsdelivr.net/npm/template-mb@2.0.6/template.js", "./app-es5.js" ]; } scripts.forEach(function (script) { insertScript(script); }); function insertScript(src) { var script = document.createElement('script'); script.src = src; script.async = false; document.head.appendChild(script); } })();
. : , - bower es-5 . , , .
ポリマー
-, Polymer . , - - Polymer.Element
. , -, .
, -, Polymer. - webcomponents.org Polymer, . , Polymer.
Polymer , , Polymer, , -. Polymer -, , , jQuery DOM JS.
Polymer HTML-.
Polymer, -, , .
Polymer API . , , , HTML- ( ES6 ), HTML- ( ). , , . , .
Polymer, . , , , Polymer.
, — , , . . . , ( , ) . , , PostCSS
, . — IDE PostCSS- <style>
. .
, node-sass
. , <style></style>
, .
結論
— , , , -. , - , - .
, (, ::part
::theme
DOM ). - — , . - Polymer — , - Polymer, .