序文vue.js フレームワークについて学習したフロントエンド開発者は、単一ファイル コンポーネントについて知っているかもしれません。 Vue.js の単一ファイル コンポーネントを使用すると、コンポーネントのすべてのコンテンツを 1 つのファイルで定義できます。これは非常に便利なソリューションであり、ブラウザの Web ページではすでに推奨されています。しかし残念なことに、このコンセプトは2017年8月に提案されて以来、これまで進展がなく、消滅しつつあるようです。ただし、このトピックをさらに深く掘り下げて、既存のテクノロジを使用して単一ファイル コンポーネントを実装してみることは興味深く、価値があります。 単一ファイルコンポーネント「プログレッシブエンハンスメント」の概念を知っているフロントエンド開発者は、「レイヤリング」の概念も聞いたことがあるはずです。コンポーネントにもそのような概念があります。実際、すべてのコンポーネントには、コンテンツ/テンプレート、プレゼンテーション、動作という少なくとも 3 つのレイヤー、あるいはそれ以上のレイヤーがあります。あるいは、保守的に考えると、各コンポーネントは少なくとも 3 つのファイルに分割されます。たとえば、ボタン コンポーネントのファイル構造は次のようになります。
このように階層化することは、テクノロジの分離(コンテンツ/テンプレート: HTML を使用、プレゼンテーション: CSS を使用、動作: JavaScript を使用)と同等です。バンドルにビルド ツールが使用されない場合は、ブラウザーがこれらの 3 つのファイルを取得する必要があることを意味します。したがって、この問題を解決するには、テクノロジ(ファイル)を分離せずにコンポーネントコードを分離するテクノロジが緊急に必要です。この記事で取り上げるのは、単一ファイル コンポーネントです。 一般的に、私は「テクノロジーの階層化」には懐疑的です。これは、完全に独立した「テクノロジーの階層化」を回避する方法として、コンポーネントの階層化が放棄されることが多いという事実に由来しています。 話題に戻ると、単一ファイル コンポーネントを使用してボタンを実装すると、次のようになります。 <テンプレート> <!-- Button.html の内容をここに記述します。 --> </テンプレート> <スタイル> /* Button.css の内容をここに記述します。 */ </スタイル> <スクリプト> // Button.js の内容をここに記述します。 </スクリプト> この単一ファイル コンポーネントは、初期のフロントエンド開発の HTML ドキュメントと非常によく似ていることがわかります。独自のスタイル タグとスクリプト タグがありますが、プレゼンテーション層ではテンプレート タグが使用されます。シンプルなアプローチのおかげで、3 つの個別のファイルを使用しなくても、強力な階層化コンポーネント (コンテンツ/テンプレート: <template>、プレゼンテーション: <style>、動作: <script>) を取得できます。 基本概念まず、コンポーネントをロードするためのグローバル関数 loadComponent() を作成します。 window.loadComponent = (関数() { 関数 loadComponent( URL ) {} loadComponent を返します。 }()); ここでは JavaScript モジュール パターンが使用されます。必要なヘルパー関数をすべて定義できますが、外部に公開されるのは loadComponent() 関数のみです。もちろん、この関数は現時点では空です。 後で、次のコンテンツを表示する <hello-world> コンポーネントを作成します。
さらに、このコンポーネントをクリックすると、次のメッセージがポップアップ表示されます。
コンポーネント コードは、HelloWorld.wc ファイルとして保存されます (ここで、.wc は Web コンポーネントを表します)。初期コードは次のとおりです。 <テンプレート> <div class="hello"> <p>こんにちは、世界!私の名前は <slot></slot> です。</p> </div> </テンプレート> <スタイル> div { 背景: 赤; 境界線の半径: 30px; パディング: 20px; フォントサイズ: 20px; テキスト配置: 中央; 幅: 300ピクセル; マージン: 0 自動; } </スタイル> <スクリプト></スクリプト> 現在、コンポーネントには動作は追加されておらず、テンプレートとスタイルのみが定義されています。テンプレートでは、<div> などの一般的な HTML タグを使用できます。また、テンプレートには、コンポーネントが Shadow DOM を実装することを示す <slot> 要素が表示されます。また、デフォルトでは、この DOM 自体のすべてのスタイルとテンプレートはこの DOM 内に隠されています。 Web ページでコンポーネントを使用する方法は非常に簡単です。 <hello-world>コマンダー</hello-world> <script src="loader.js"></script> <スクリプト> コンポーネントをロードします( 'HelloWorld.wc' ); </スクリプト> コンポーネントは標準のカスタム要素のように使用できます。唯一の違いは、loadComponent() メソッドを使用する前にロードする必要があることです (このメソッドは loader.js に配置されます)。 loadComponent() メソッドは、コンポーネントを取得して customElements.define() に登録するなど、面倒な処理をすべて実行します。 すべての概念を理解したので、実際にやってみることにしましょう。 シンプルなローダー外部ファイルからファイルを読み込む場合は、ユニバーサル Ajax を使用する必要があります。しかし、今は 2020 年であり、ほとんどのブラウザでは Fetch API を安全に使用できます。 関数loadComponent(URL){ fetch(URL)を返します。 } ただし、これはファイルを取得するだけで、処理は何も行いません。次に行うことは、次のように、Ajax の戻りコンテンツをテキストに変換することです。 関数loadComponent(URL){ return fetch( URL ).then( ( レスポンス ) => { 応答.text() を返します。 } ); } loadComponent() 関数は fetch 関数の実行結果を返すため、Promise オブジェクトになります。 then メソッドで、ファイル (HelloWorld.wc) が実際に読み込まれ、テキストに変換されているかどうかを確認できます。 結果は次のとおりです。 Chrome ブラウザで console() メソッドを使用すると、HelloWorld.wc の内容がテキストに変換されて出力されていることがわかります。つまり、動作しているようです。 コンポーネントコンテンツの解析しかし、単にテキストを出力するだけでは目的は達成されません。最終的には、表示のために DOM に変換され、実際にユーザーと対話できるようになります。 ブラウザ環境には、DOM パーサーを作成するために使用できる非常に実用的なクラス DOMParser があります。 DOMParser クラスをインスタンス化してオブジェクトを取得します。このオブジェクトを使用して、コンポーネント テキストを DOM に変換できます。 window.loadComponent = (関数() { 関数loadComponent(URL) { 戻り値 fetch(URL).then((レスポンス) => { 応答.text() を返します。 }).then((html) => { const パーサー = new DOMParser(); // 1 parser.parseFromString(html, 'text/html'); を返します。 // 2 }); } loadComponent を返します。 }()); まず、DOMParserインスタンスパーサーが作成され(1)、次にこのインスタンスを使用してコンポーネントのコンテンツをDOMに変換します(2)。ここでは HTML モード ('text/html') が使用されていることに注意してください。コードを JSX 標準または元の Vue.js コンポーネントにより準拠させたい場合は、XML モード ('text/XML') を使用できます。ただし、この場合、コンポーネント自体の構造を変更する必要があります (たとえば、他の要素を保持できるメイン要素を追加するなど)。 これは loadComponent() 関数の戻り結果であり、DOM ツリーです。 Chrome ブラウザでは、console.log() は解析された HelloWorld.wc ファイル (DOM ツリー) を出力します。 parser.parseFromString メソッドは、<html>、<head>、<body> タグ要素をコンポーネントに自動的に追加することに注意してください。これは HTML パーサーの動作によるものです。 DOM ツリーを構築するためのアルゴリズムは、HTML LS 仕様で詳しく説明されています。これは長い記事なので、読むのに時間がかかりますが、簡単に理解すると、パーサーは、<body> タグ内にのみ配置できる DOM 要素に遭遇するまで、デフォルトですべてのコンテンツを <head> 要素に配置するということです。したがって、コンポーネント コード内のすべての要素 (<element>、<style>、<script>) を <head> 内に配置できます。 <p> 要素を <template> 内にラップすると、パーサーはそれを <body> 内に配置します。 もう一つ問題があります。コンポーネントを解析した後、<!DOCTYPE html> 宣言がないので、これは異常な HTML ドキュメントとなり、ブラウザは Quirks モードと呼ばれる方法を使用してこの HTML ドキュメントをレンダリングします。幸いなことに、DOM パーサーはコンポーネントを適切な部分に分割するためにのみ使用されるため、ここでは悪影響はありません。 DOM ツリーを取得したら、必要な部分だけを抽出できます。 戻り値 fetch(URL).then((レスポンス) => { 応答.text() を返します。 }).then((html) => { 定数パーサー = 新しい DOMParser(); 定数ドキュメント = parser.parseFromString(html, 'text/html'); ドキュメントのhead要素をコピーします。 const テンプレート = head.querySelector('テンプレート'); 定数スタイル = head.querySelector('style'); 定数script = head.querySelector('script'); 戻る { テンプレート、 スタイル、 スクリプト }; }); 最後にコードを整理してみましょう。loadComponent メソッドは以下のとおりです。 window.loadComponent = (関数() { 関数 fetchAndParse(URL) { 戻り値 fetch(URL).then((レスポンス) => { 応答.text() を返します。 }).then((html) => { 定数パーサー = 新しい DOMParser(); 定数ドキュメント = parser.parseFromString(html, 'text/html'); ドキュメントのhead要素をコピーします。 const テンプレート = head.querySelector('テンプレート'); 定数スタイル = head.querySelector('style'); 定数script = head.querySelector('script'); 戻る { テンプレート、 スタイル、 スクリプト }; }); } 関数loadComponent(URL) { fetchAndParse(URL)を返します。 } loadComponent を返します。 }()); Fetch API は、外部ファイルからコンポーネント コードを取得する唯一の方法ではありません。XMLHttpRequest には、解析手順全体をスキップできる専用のドキュメント モードがあります。しかし、XMLHttpRequest は Promise を返さないので、自分でラップする必要があります。 コンポーネントの登録コンポーネント レイヤーができたので、新しいカスタム コンポーネントを登録するための registerComponent() メソッドを作成できます。 window.loadComponent = (関数() { 関数 fetchAndParse(URL) { […] } 関数registerComponent() { } 関数loadComponent(URL) { fetchAndParse(URL) を返します。次に、registerComponent を返します。 } loadComponent を返します。 }()); カスタム コンポーネントは HTMLElement から継承するクラスである必要があることに注意してください。さらに、各コンポーネントはスタイルとテンプレート コンテンツを保存するために Shadow DOM を使用します。したがって、このコンポーネントが参照されるたびに、同じスタイルになります。方法は次のとおりです。 関数registerComponent({テンプレート、スタイル、スクリプト}) { クラスUnityComponentはHTMLElementを拡張します{ 接続されたコールバック() { this._upcast(); } _アップキャスト() { const shadow = this.attachShadow({mode: 'open'}); shadow.appendChild(style.cloneNode(true)); shadow.appendChild(document.importNode(template.content, true)); } } } UnityComponent クラスは、registerComponent() に渡されたパラメータを使用するため、registerComponent() メソッド内に作成する必要があります。このクラスは、Shadow DOM に関するこの記事 (ポーランド語) で詳しく説明している、わずかに変更されたメカニズムを使用して Shadow DOM を実装します。 これで、コンポーネントを登録するために残っているのは、単一ファイル コンポーネントに名前を付けて、現在のページの DOM に追加するだけです。 関数registerComponent({テンプレート、スタイル、スクリプト}) { クラスUnityComponentはHTMLElementを拡張します{ [...] } customElements.define( 'hello-world', UnityComponent ) を返します。 } 次のように開いて確認してみましょう。 Chrome では、このボタン コンポーネントには、「Hello, world! My name is Comandeer」というテキストが入った赤い四角形が表示されます。 スクリプトコンテンツを取得するこれで、シンプルなボタン コンポーネントが実装されました。ここで、動作レイヤーを追加し、ボタン内のコンテンツをカスタマイズするという難しい部分がやってきます。上記の手順では、コンポーネント コード内のボタンにテキスト コンテンツをハードコーディングするのではなく、ボタンに渡されたコンテンツを使用する必要があります。同様に、コンポーネント内にバインドされたイベント リスナーも処理する必要があります。ここでは、次のように Vue.js に似た規則を使用します。 <テンプレート> […] </テンプレート> <スタイル> […] </スタイル> <スクリプト> エクスポートデフォルト{ // 1 名前: 'hello-world', // 2 onClick() { // 3 警告(`私に触れないでください!`); } } </スクリプト> コンポーネント内の <script> タグ内のコンテンツは、コンテンツをエクスポートする JavaScript モジュールであると想定できます (1)。モジュールによってエクスポートされたオブジェクトには、コンポーネントの名前 (2) と「on..」で始まるイベント リスナー メソッド (3) が含まれています。 これは見た目がすっきりしており、モジュールの外部には何も公開されません (モジュールは JavaScript のグローバル スコープ内にないため)。ここで問題があります。内部モジュールからエクスポートされたオブジェクト (HTML ドキュメントで直接定義されたオブジェクト) を処理するための標準が存在しないのです。 import ステートメントは、モジュール識別子が取得され、この識別子に従ってインポートされることを前提としています。最も一般的なのは、コードを含むファイルへの URL パスです。コンポーネントは js ファイルではないため、このような識別子はありません。内部モジュールにはこのような識別子はありません。 降伏する前に、使える超汚いハックがあります。ブラウザがテキストをファイルのように扱う方法は、データ URI とオブジェクト URI の少なくとも 2 つあります。 Service Worker を使用するための提案もいくつかあります。しかし、ここでは少しやり過ぎのようです。 データ URI とオブジェクト URIデータ URI は古くて原始的な方法です。これは、ファイルの内容を URL に変換し、不要なスペースを削除し、すべてを Base64 を使用してエンコードすることに基づいています。次の内容の JavaScript ファイルがあるとします。 エクスポートのデフォルトは true です。 次のようにデータ URI に変換されます。 データ:application/javascript;base64,ZXhwb3J0IGRlZmF1bHQgdHJ1ZTs= ファイルをインポートするのと同じように、この URI をインポートできます。 'data:application/javascript;base64,ZXhwb3J0IGRlZmF1bHQgdHJ1ZTs=' からテストをインポートします。 console.log( テスト ); Data URI アプローチの明らかな欠点は、JavaScript ファイルのコンテンツが増加するにつれて、URL の長さが非常に長くなることです。また、バイナリ データを Data URI に配置するのは非常に困難です。 つまり、新しい種類のオブジェクト URI が誕生したのです。これは、File API や HTML5 の <video> タグおよび <audio> タグなど、いくつかの標準から派生したものです。オブジェクト URI の目的は単純です。現在のコンテキストで一意の URI を指定して、指定されたバイナリ データから「疑似ファイル」を作成します。簡単に言えば、メモリ内に一意の名前を持つファイルを作成します。オブジェクト URI には、データ URI (「ファイル」を作成する方法) の利点がすべてありますが、欠点はありません (ファイルが 100 MB でも問題ありません)。 オブジェクト URI は通常、マルチメディア ストリーム (<video> または <audio> コンテキストなど) から、または入力 [type=file] およびドラッグ アンド ドロップ メカニズムを介して送信されたファイルから作成されます。 File クラスと Blob クラスを使用して手動で作成することもできます。この例では、Bolb を使用して最初にコンテンツをモジュールに配置し、次にそれをオブジェクト URI に変換します。 const myJSFile = new Blob( [ 'export default true;' ], { type: 'application/javascript' } ); const myJSURL = URL.createObjectURL( myJSFile ); console.log( myJSURL ); // ブロブ:https://blog.comandeer.pl/8e8fbd73-5505-470d-a797-dfb06ca71333 動的インポートただし、問題が 1 つあります。import ステートメントは、モジュール識別子として変数を受け入れません。つまり、この方法を使用してモジュールを「ファイル」に変換する以外に、それをインポートする方法はありません。まだ解決策はないのでしょうか? 必ずしもそうではありません。この問題はずっと以前から提起されており、動的インポート メカニズムを使用することで解決できます。これは ES2020 標準の一部であり、Firefox、Safari、Node.js 13.x に実装されています。動的にインポートされるモジュールの識別子として変数を使用することは、もはや問題ではありません。 const myJSFile = new Blob( [ 'export default true;' ], { type: 'application/javascript' } ); const myJSURL = URL.createObjectURL( myJSFile ); import( myJSURL ).then( ( モジュール ) => { console.log( module.default ); // true }); 上記のコードからわかるように、import() コマンドはメソッドのように使用できます。Promise オブジェクトを返し、then メソッドでモジュール オブジェクトが取得されます。デフォルト属性には、モジュールで定義されているすべてのエクスポート オブジェクトが含まれます。 成し遂げるアイデアができたので、それを実装し始めることができます。ツール メソッド getSetting() を追加します。スクリプト コードからすべての情報を取得するには、registerComponents() メソッドの前に呼び出します。 関数 getSettings({ テンプレート、スタイル、スクリプト }) { 戻る { テンプレート、 スタイル、 スクリプト }; } [...] 関数loadComponent(URL){ fetchAndParse( URL ).then( getSettings ).then( registerComponent ) を返します。 } このメソッドは渡されたすべてのパラメータを返します。上記のロジックに従って、スクリプト コードをオブジェクト URI に変換します。 const jsFile = new Blob( [ script.textContent ], { type: 'application/javascript' } ); const jsURL = URL.createObjectURL( jsFile ); 次に、import を使用してモジュールをロードし、テンプレート、スタイル、コンポーネントの名前を返します。 import(jsURL).then((モジュール) => { を返します。 戻る { 名前: module.default.name、 テンプレート、 スタイル } } ); このため、registerComponent() は依然として 3 つの引数を取得しますが、スクリプトの代わりに名前を取得するようになりました。正しいコードは次のとおりです。 関数registerComponent({テンプレート、スタイル、名前}) { クラスUnityComponentはHTMLElementを拡張します{ [...] } customElements.define( 名前、 UnityComponent ) を返します。 } 行動層コンポーネントには、イベントを処理するために使用される動作レイヤーという最後のレイヤーが残っています。ここで、getSettings() メソッドでコンポーネントの名前を取得するだけでなく、イベント リスナーも取得する必要があります。 Object.entrie() メソッドを使用して取得できます。 getSettings() メソッドに適切なコードを追加します。 関数 getSettings({ テンプレート、スタイル、スクリプト }) { [...] 関数 getListeners( 設定 ) { // 1 定数リスナー = {}; Object.entries( 設定 ).forEach( ( [ 設定, 値 ] ) => { // 3 if (setting.startsWith('on')) { // 4 リスナー[setting[2].toLowerCase() + setting.substr(3)] = 値; // 5 } } ); リスナーを返します。 } import(jsURL).then((モジュール) => { を返します。 const listeners = getListeners( module.default ); // 2 戻る { 名前: module.default.name、 リスナー、// 6 テンプレート、 スタイル } } ); } ここで、方法は少し複雑になります。モジュールの出力をこのパラメータに渡す新しい関数 getListeners() (1) が追加されました。 次に、Object.entries() (3) メソッドを使用して、エクスポートされたモジュールを反復処理します。現在の属性が「on」で始まる場合(4)、それはリスニング関数であることを意味します。このノード(リスニング関数)の値をリスナーオブジェクトに追加し、setting[2].toLowerCase()+setting.substr(3)(5)を使用してキー値を取得します。 キー値は、先頭の「on」を削除し、次の「Click」の最初の文字を小文字に変換することによって形成されます (つまり、onClick からキー値として click を取得します)。次に、istenersオブジェクト(6)を渡します。 次のように、[].forEach() メソッドの代わりに [].reduce() メソッドを使用すると、リスナー変数を省略できます。 関数 getListeners(設定) { Object.entries(設定).reduce((リスナー、[設定、値])を返す=> { if ( 設定が 'on' の場合 ) { リスナー[setting[2].toLowerCase() + setting.substr(3)] = 値; } リスナーを返します。 }, {} ); } これで、コンポーネント内のクラスにリスナーをバインドできます。 関数registerComponent({テンプレート、スタイル、名前、リスナー}) { // 1 クラスUnityComponentはHTMLElementを拡張します{ 接続されたコールバック() { this._upcast(); this._attachListeners(); // 2 } [...] _attachListeners() { Object.entries( リスナー ).forEach( ( [ イベント, リスナー ] ) => { // 3 this.addEventListener( イベント、リスナー、false ); // 4 } ); } } customElements.define( 名前、 UnityComponent ) を返します。 } リスナーメソッド(1)にパラメータが追加され、クラス(2)に新しいメソッド_attachListeners()が追加されます。ここで、Object.entries() を再度使用してリスナー (3) を反復処理し、それらを要素 (4) にバインドできます。 最後に、コンポーネントをクリックすると、以下に示すように「Don't touch me!」というポップアップが表示されます。 互換性の問題やその他の問題ご覧のとおり、この単一ファイル コンポーネントを実装するには、基本的なフォームをサポートする方法を中心に作業が行われます。多くの部分でダーティ ハックが使用されています (Object URI を使用して ES でモジュールをロードしますが、これはブラウザーのサポートがなければ意味がありません)。幸いなことに、これらのテクニックはすべて、Chrome、Firefox、Safari などの主要なブラウザで問題なく動作します。 それでも、多くのブラウザ テクノロジと最新の Web 標準を扱えるこのようなプロジェクトを作成するのは楽しかったです。 最後に、このプロジェクトのコードはオンラインで入手できます。 要約するこれで、Vue での単一ファイル コンポーネントの実装に関するこの記事は終了です。Vue 単一ファイル コンポーネントに関するより関連性の高いコンテンツについては、123WORDPRESS.COM で以前の記事を検索するか、以下の関連記事を引き続き参照してください。今後とも 123WORDPRESS.COM をよろしくお願いいたします。 以下もご興味があるかもしれません:
|
<<: Pycharm2017はpython3.6とmysqlの接続を実現します
>>: Linux で複数のファイルの名前を一括で変更する方法
目次配列分割代入オブジェクトの分解代入分割割り当ての適用変数の値の交換関数から複数の値を返すマップ構...
序文Vuex を使用すると、ストア内に「ゲッター」を定義できます (これはストアの計算されたプロパテ...
今日は、タブ バーをクリックして切り替えるという目的を実現するために、js と jQuery を使用...
説明 ソリューションVMware 15 仮想マシン ブリッジ モードではインターネットにアクセスでき...
序文ご存知のとおり、JavaScript は本質的にシングルスレッドですが、ブラウザは非同期リクエス...
目次1. ページレイアウト2. 画像のアップロードと表示3. キャンバスを初期化する4. テンプレー...
CentOS 8 が正式にリリースされました! CentOS は Red Hat の再配布ポリシー...
MySQL ショートリンクの設定方法1. mysql 接続番号ステートメントコマンドを確認します。 ...
以下の機能が実装されています。 1. ユーザー名: onfouc は msg ルールを表示します。o...
背景tomcat によって生成された catalina.out ログ ファイルが分割されていない場合...
オンラインショッピングモールデータベース - 商品カテゴリデータ操作(I)プロジェクトの説明電子商取...
目次序文1. 準備2. インストール3. 環境変数を設定する1. 「新規」をクリックすると、ポップア...
1. PPTP VPNを構築するには、ポート1723とGREプロトコルを開く必要があります。 1. ...
CSS は div にスクロールを追加し、スクロール バーを非表示にします。具体的なコードは次のとお...
訪問者があなたのウェブサイトを覚えておくのに役立つ3つの便利なコード。お気に入りに追加するためのヒン...