数年以上Web開発者である私たちの人々は、おそらく複数のJavaScriptフレームワークを使用してコードを書いたことがあるでしょう。 React、Svelte、Vue、Angular、Solidなど、あらゆる選択肢がありますが、それは避けられないことです。 フレームワーク間で作業するときに対処しなければならない最も苛立たしいことのXNUMXつは、ボタン、タブ、ドロップダウンなどのすべての低レベルUIコンポーネントを再作成することです。特に苛立たしいのは、通常、それらをXNUMXつのフレームワークで定義することです。 、Reactと言いますが、Svelteで何かを構築したい場合は、それらを書き直す必要があります。 またはVue。 またはソリッド。 等々。
これらの低レベルのUIコンポーネントをフレームワークに依存しない方法で一度定義し、フレームワーク間で再利用できればもっと良いのではないでしょうか。 もちろんそうでしょう! そして、私たちはできます。 Webコンポーネントがその方法です。 この投稿では、その方法を説明します。
現在のところ、WebコンポーネントのSSRストーリーは少し欠けています。 宣言型ShadowDOM(DSD)は、Webコンポーネントがサーバー側でレンダリングされる方法ですが、この記事の執筆時点では、Next、Remix、SvelteKitなどのお気に入りのアプリケーションフレームワークと統合されていません。 それが必要な場合は、DSDの最新のステータスを確認してください。 ただし、それ以外の場合、SSRが使用しているものでない場合は、読み進めてください。
まず、いくつかのコンテキスト
Webコンポーネントは、基本的に、自分で定義するHTML要素です。 <yummy-pizza>
または何でも、ゼロから。 これらはCSS-Tricks(含む ケイレブ・ウィリアムズによる広範なシリーズ & JohnRheaによるもの)ただし、プロセスについて簡単に説明します。 基本的に、JavaScriptクラスを定義し、そこから継承します HTMLElement
、次に、Webコンポーネントが持つプロパティ、属性、スタイル、そしてもちろん、最終的にユーザーにレンダリングするマークアップを定義します。
特定のコンポーネントにバインドされていないカスタムHTML要素を定義できることはエキサイティングです。 しかし、この自由も制限です。 JavaScriptフレームワークとは独立して存在するということは、それらのJavaScriptフレームワークと実際に対話できないことを意味します。 いくつかのデータをフェッチしてからいくつかをレンダリングするReactコンポーネントについて考えてみてください 他の コンポーネントを反応させ、データを渡します。 WebコンポーネントはReactコンポーネントをレンダリングする方法を知らないため、これは実際にはWebコンポーネントとしては機能しません。
Webコンポーネントは特に優れています 葉のコンポーネント. 葉のコンポーネント コンポーネントツリーにレンダリングされる最後のものです。 これらは、いくつかの小道具を受け取り、いくつかをレンダリングするコンポーネントです UI。 これらは コンポーネントツリーの中央にあるコンポーネント、データの受け渡し、コンテキストの設定など—純粋な UI どのJavaScriptフレームワークがアプリの残りの部分を強化しているかに関係なく、同じように見えます。
構築しているWebコンポーネント
ボタンのように退屈な(そして一般的な)ものを作成するのではなく、少し異なるものを作成しましょう。 私の中で ポストをロードする ぼやけた画像プレビューを使用してコンテンツのリフローを防ぎ、画像の読み込み中にユーザーに適切なUIを提供することを検討しました。 ぼやけた、劣化したバージョンの画像をエンコードしているbase64を調べ、実際の画像が読み込まれている間にUIでそれを示しました。 また、と呼ばれるツールを使用して、信じられないほどコンパクトでぼやけたプレビューを生成することも検討しました。 ぼかし.
その投稿は、それらのプレビューを生成し、Reactプロジェクトでそれらを使用する方法を示しました。 この投稿では、Webコンポーネントからこれらのプレビューを使用して、 どれか JavaScriptフレームワーク。
ただし、実行する前に歩く必要があるため、最初に些細でばかげたものを調べて、Webコンポーネントがどのように機能するかを正確に確認します。
この投稿のすべては、ツールなしでバニラWebコンポーネントを構築します。 つまり、コードには少し定型文がありますが、比較的簡単に理解できるはずです。 のようなツール リット or ステンシル Webコンポーネントを構築するために設計されており、この定型文の多くを削除するために使用できます。 ぜひチェックしてみてください! しかし、この投稿では、別の依存関係を紹介して教える必要がないことと引き換えに、もう少し定型文を使用することをお勧めします。
シンプルなカウンターコンポーネント
JavaScriptコンポーネントの古典的な「HelloWorld」であるカウンターを作成しましょう。 値と、その値をインクリメントするボタンをレンダリングします。 シンプルで退屈ですが、可能な限りシンプルなWebコンポーネントを見てみましょう。
Webコンポーネントを構築するための最初のステップは、JavaScriptクラスを作成することです。 HTMLElement
:
class Counter extends HTMLElement {}
最後のステップは、Webコンポーネントを登録することですが、まだ登録していない場合に限ります。
if (!customElements.get("counter-wc")) {
customElements.define("counter-wc", Counter);
}
そして、もちろん、それをレンダリングします:
<counter-wc></counter-wc>
そして、その間のすべては、Webコンポーネントに私たちが望むことを何でもさせることです。 一般的なライフサイクル方法のXNUMXつは connectedCallback
、これは、WebコンポーネントがDOMに追加されたときに発生します。 そのメソッドを使用して、必要なコンテンツをレンダリングできます。 これはから継承するJSクラスであることを忘れないでください HTMLElement
、つまり this
valueは、Webコンポーネント要素自体であり、既に知っていて気に入っている通常のDOM操作メソッドがすべて含まれています。
最も簡単な方法として、これを行うことができます。
class Counter extends HTMLElement {
connectedCallback() {
this.innerHTML = "<div style='color: green'>Hey</div>";
}
}
if (!customElements.get("counter-wc")) {
customElements.define("counter-wc", Counter);
}
…これは問題なく機能します。
実際のコンテンツを追加する
便利でインタラクティブなコンテンツをいくつか追加しましょう。 必要です <span>
現在の数値と <button>
カウンターをインクリメントします。 今のところ、コンストラクターでこのコンテンツを作成し、Webコンポーネントが実際にDOMにあるときに追加します。
constructor() {
super();
const container = document.createElement('div');
this.valSpan = document.createElement('span');
const increment = document.createElement('button');
increment.innerText = 'Increment';
increment.addEventListener('click', () => {
this.#value = this.#currentValue + 1;
});
container.appendChild(this.valSpan);
container.appendChild(document.createElement('br'));
container.appendChild(increment);
this.container = container;
}
connectedCallback() {
this.appendChild(this.container);
this.update();
}
手動でDOMを作成することに本当にうんざりしている場合は、設定できることを忘れないでください innerHTML
、またはWebコンポーネントクラスの静的プロパティとしてテンプレート要素をXNUMX回作成し、それを複製して、新しいWebコンポーネントインスタンスのコンテンツを挿入します。 おそらく私が考えていない他のオプションがいくつかあります。または、次のようなWebコンポーネントフレームワークをいつでも使用できます。 リット or ステンシル。 ただし、この投稿では、引き続きシンプルにします。
次に、次の名前の設定可能なJavaScriptクラスプロパティが必要です。 value
#currentValue = 0;
set #value(val) {
this.#currentValue = val;
this.update();
}
これは、値を保持するXNUMX番目のプロパティとともに、セッターを備えた単なる標準クラスのプロパティです。 面白いひねりのXNUMXつは、これらの値にプライベートJavaScriptクラスプロパティ構文を使用していることです。 つまり、Webコンポーネントの外部の誰もこれらの値に触れることはできません。 これは標準のJavaScriptです これはすべての最新のブラウザでサポートされています、それでそれを使うことを恐れないでください。
またはお気軽にお電話ください _value
必要に応じて。 そして最後に、 update
方法:
update() {
this.valSpan.innerText = this.#currentValue;
}
できます!
明らかに、これは大規模に維持したいコードではありません。 これが完全です 実例 詳細を確認したい場合。 私が言ったように、LitやStencilのようなツールはこれをより簡単にするように設計されています。
さらにいくつかの機能を追加する
この投稿は、Webコンポーネントの詳細ではありません。 すべてのAPIとライフサイクルを網羅するわけではありません。 カバーすらしません シャドウルートまたはスロット。 それらのトピックに関する無限のコンテンツがあります。 ここでの私の目標は、実際にいくつかの有用なガイダンスとともに、いくつかの興味を刺激するのに十分な適切な紹介を提供することです あなたがすでに知っていて愛している人気のあるJavaScriptフレームワークを備えたWebコンポーネント。
そのために、カウンターWebコンポーネントを少し強化しましょう。 受け入れてもらいましょう color
属性。表示される値の色を制御します。 そして、それを受け入れさせましょう increment
プロパティであるため、このWebコンポーネントの利用者は、一度に2、3、4ずつインクリメントできます。 そして、これらの状態変化を促進するために、Svelteサンドボックスの新しいカウンターを使用しましょう—少しでReactに到達します。
以前と同じWebコンポーネントから始めて、色属性を追加します。 属性を受け入れて応答するようにWebコンポーネントを構成するには、静的を追加します observedAttributes
Webコンポーネントがリッスンする属性を返すプロパティ。
static observedAttributes = ["color"];
これで、追加できます attributeChangedCallback
ライフサイクルメソッド。以下にリストされている属性のいずれかが実行されるたびに実行されます。 observedAttributes
設定または更新されます。
attributeChangedCallback(name, oldValue, newValue) {
if (name === "color") {
this.update();
}
}
今、私たちは私たちを更新します update
実際に使用する方法:
update() {
this.valSpan.innerText = this._currentValue;
this.valSpan.style.color = this.getAttribute("color") || "black";
}
最後に、 increment
プロパティ:
increment = 1;
シンプルで謙虚。
Svelteのカウンターコンポーネントの使用
作ったものを使ってみましょう。 Svelteアプリコンポーネントに入り、次のようなものを追加します。
<script>
let color = "red";
</script>
<style>
main {
text-align: center;
}
</style>
<main>
<select bind:value={color}>
<option value="red">Red</option>
<option value="green">Green</option>
<option value="blue">Blue</option>
</select>
<counter-wc color={color}></counter-wc>
</main>
そしてそれは動作します! カウンターがレンダリング、増分し、ドロップダウンが色を更新します。 ご覧のとおり、Svelteテンプレートでcolor属性をレンダリングし、値が変更されると、Svelteが呼び出しのレッグワークを処理します setAttribute
基盤となるWebコンポーネントインスタンス。 ここでは特別なことは何もありません。これは、の属性に対してすでに行っていることと同じです。 どれか HTML要素。
物事は少し面白くなります increment
小道具。 これは Webコンポーネントの属性。 これは、Webコンポーネントのクラスの小道具です。 つまり、Webコンポーネントのインスタンスで設定する必要があります。 物事は少し簡単になってしまうので、私と一緒に耐えてください。
まず、Svelteコンポーネントにいくつかの変数を追加します。
let increment = 1;
let wcInstance;
カウンターコンポーネントの強力な機能により、1ずつ、または2ずつインクリメントできます。
<button on:click={() => increment = 1}>Increment 1</button>
<button on:click={() => increment = 2}>Increment 2</button>
しかし、 理論的には、Webコンポーネントの実際のインスタンスを取得する必要があります。 これは、追加するたびに常に行うことと同じです。 ref
Reactで。 Svelteを使えば、それは簡単です bind:this
指令:
<counter-wc bind:this={wcInstance} color={color}></counter-wc>
ここで、Svelteテンプレートで、コンポーネントの増分変数への変更をリッスンし、基になるWebコンポーネントプロパティを設定します。
$: {
if (wcInstance) {
wcInstance.increment = increment;
}
}
あなたはそれをテストすることができます このライブデモで.
管理する必要のあるすべてのWebコンポーネントまたはプロップに対してこれを実行したくないことは明らかです。 素敵じゃないか increment
私たちのWebコンポーネント上で、マークアップで、コンポーネントの小道具に対して通常行うように、それを持っています。 ただ仕事? 言い換えれば、すべての使用法を削除できればいいのですが wcInstance
代わりに、この単純なコードを使用してください。
<counter-wc increment={increment} color={color}></counter-wc>
できることがわかりました。 このコードは機能します。 Svelteは私たちのためにそのすべてのレッグワークを処理します。 このデモでそれをチェックしてください。 これは、ほとんどすべてのJavaScriptフレームワークの標準的な動作です。
では、なぜWebコンポーネントのプロップを手動で設定する方法を示したのでしょうか。 XNUMXつの理由:これらがどのように機能するかを理解することは有用です。少し前に、これは「ほぼ」すべてのJavaScriptフレームワークで機能すると言いました。 しかし、残念ながら、今見たようなWebコンポーネントのプロップ設定をサポートしていないフレームワークがXNUMXつあります。
Reactは別の獣です
反応する。 地球上で最も人気のあるJavaScriptフレームワークは、Webコンポーネントとの基本的な相互運用性をサポートしていません。 これは、Reactに固有のよく知られた問題です。 興味深いことに、これは実際にはReactの実験的なブランチで修正されていますが、何らかの理由でバージョン18にマージされませんでした。 進行状況を追跡する。 そして、あなたはこれを自分で試すことができます ライブデモ.
もちろん、解決策は、 ref
、Webコンポーネントインスタンスを取得し、手動で設定します increment
その値が変わるとき。 次のようになります。
import React, { useState, useRef, useEffect } from 'react';
import './counter-wc';
export default function App() {
const [increment, setIncrement] = useState(1);
const [color, setColor] = useState('red');
const wcRef = useRef(null);
useEffect(() => {
wcRef.current.increment = increment;
}, [increment]);
return (
<div>
<div className="increment-container">
<button onClick={() => setIncrement(1)}>Increment by 1</button>
<button onClick={() => setIncrement(2)}>Increment by 2</button>
</div>
<select value={color} onChange={(e) => setColor(e.target.value)}>
<option value="red">Red</option>
<option value="green">Green</option>
<option value="blue">Blue</option>
</select>
<counter-wc ref={wcRef} increment={increment} color={color}></counter-wc>
</div>
);
}
すでに説明したように、すべてのWebコンポーネントプロパティに対してこれを手動でコーディングすることは、単純にスケーラブルではありません。 しかし、いくつかのオプションがあるため、すべてが失われるわけではありません。
オプション1:どこでも属性を使用する
属性があります。 上記のReactデモをクリックした場合、 increment
小道具は機能していませんでしたが、色が正しく変更されました。 すべてを属性でコーディングすることはできませんか? 悲しいことに、違います。 属性値は文字列のみにすることができます。 ここではそれで十分であり、このアプローチでいくらか遠くまで行くことができます。 のような数字 increment
文字列との間で変換できます。 オブジェクトをJSONで文字列化/解析することもできます。 ただし、最終的には関数をWebコンポーネントに渡す必要があり、その時点でオプションがなくなります。
オプション2:ラップする
間接参照のレベルを追加することで、コンピュータサイエンスの問題を解決できるという古い言い伝えがあります(間接参照のレベルが多すぎるという問題を除く)。 これらの小道具を設定するコードは、かなり予測可能で単純です。 ライブラリに非表示にするとどうなりますか? Litの背後にいる賢い人々 XNUMXつの解決策があります。 このライブラリは、Webコンポーネントを指定した後、新しいReactコンポーネントを作成し、必要なプロパティを一覧表示します。 賢いですが、私はこのアプローチのファンではありません。
Webコンポーネントを手動で作成したReactコンポーネントにXNUMX対XNUMXでマッピングするのではなく、私が好むのは XNUMXつ Webコンポーネントを渡すReactコンポーネント タグ名 に(counter-wc
この場合)—すべての属性とプロパティとともに—このコンポーネントでWebコンポーネントをレンダリングするには、 ref
、次に、小道具とは何か、属性とは何かを理解します。 それが私の意見では理想的な解決策です。 これを行うライブラリはわかりませんが、簡単に作成できるはずです。 やってみよう!
これは 使用 私たちが探しているのは:
<WcWrapper wcTag="counter-wc" increment={increment} color={color} />
wcTag
Webコンポーネントのタグ名です。 残りは、渡したいプロパティと属性です。
私の実装は次のようになります。
import React, { createElement, useRef, useLayoutEffect, memo } from 'react';
const _WcWrapper = (props) => {
const { wcTag, children, ...restProps } = props;
const wcRef = useRef(null);
useLayoutEffect(() => {
const wc = wcRef.current;
for (const [key, value] of Object.entries(restProps)) {
if (key in wc) {
if (wc[key] !== value) {
wc[key] = value;
}
} else {
if (wc.getAttribute(key) !== value) {
wc.setAttribute(key, value);
}
}
}
});
return createElement(wcTag, { ref: wcRef });
};
export const WcWrapper = memo(_WcWrapper);
最も興味深い行は最後にあります:
return createElement(wcTag, { ref: wcRef });
これは、動的な名前でReactに要素を作成する方法です。 実際、これはReactが通常JSXをトランスパイルするものです。 すべてのdivはに変換されます createElement("div")
呼び出します。 通常、このAPIを直接呼び出す必要はありませんが、必要なときにそこにあります。
それを超えて、レイアウトエフェクトを実行し、コンポーネントに渡したすべての小道具をループします。 それらすべてをループして、それがプロパティであるかどうかを確認します in
これは、Webコンポーネントインスタンスオブジェクトとそのプロトタイプチェーンをチェックします。これにより、クラスプロトタイプに到達するゲッター/セッターがキャッチされます。 そのようなプロパティが存在しない場合、それは属性であると見なされます。 いずれの場合も、値が実際に変更された場合にのみ設定します。
なぜ私たちが使用するのか疑問に思っているなら useLayoutEffect
useEffect
、コンテンツがレンダリングされる前にこれらの更新をすぐに実行したいからです。 また、依存関係の配列がないことに注意してください useLayoutEffect
; これは、この更新を実行することを意味します すべてのレンダリング。 Reactは再レンダリングする傾向があるため、これはリスクを伴う可能性があります たくさん。 全体を包むことでこれを改善します React.memo
。 これは本質的に現代版です React.PureComponent
、つまり、コンポーネントは、実際の小道具のいずれかが変更された場合にのみ再レンダリングされます。これは、単純な等価性チェックによって行われたかどうかをチェックします。
ここでの唯一のリスクは、再割り当てせずに直接変更しているオブジェクトプロップを渡すと、更新が表示されないことです。 しかし、これは特にReactコミュニティでは非常に推奨されていないので、心配する必要はありません。
先に進む前に、最後にもうXNUMXつ申し上げたいと思います。 使用法がどのように見えるかに満足できないかもしれません。 この場合も、このコンポーネントは次のように使用されます。
<WcWrapper wcTag="counter-wc" increment={increment} color={color} />
具体的には、Webコンポーネントのタグ名を <WcWrapper>
コンポーネントと代わりに好む @lit-labs/react
上記のパッケージは、Webコンポーネントごとに新しい個別のReactコンポーネントを作成します。 それは完全に公平であり、私はあなたが最も快適なものを使用することをお勧めします。 しかし、私にとって、このアプローチの利点のXNUMXつは、簡単にできることです。 削除。 奇跡によってReactが適切なWebコンポーネント処理を実験ブランチからにマージした場合 main
明日、上記のコードをこれから変更できるようになります。
<WcWrapper wcTag="counter-wc" increment={increment} color={color} />
…これに:
<counter-wc ref={wcRef} increment={increment} color={color} />
おそらく、単一のcodemodを記述して、それをどこでも実行してから、削除することもできます。 <WcWrapper>
完全に。 実際には、それをスクラッチします。グローバル検索と正規表現への置き換えはおそらく機能します。
実装
ここにたどり着くまでに旅がかかったようです。 思い出してください。私たちの当初の目標は、私が見た画像プレビューコードを取得することでした。 ポストをロードする、それをWebコンポーネントに移動して、任意のJavaScriptフレームワークで使用できるようにします。 Reactの適切な相互運用性の欠如は、ミックスに多くの詳細を追加しました。 しかし、Webコンポーネントを作成し、それを使用する方法について適切なハンドルを持っているので、実装はほとんど反気候的です。
ここにWebコンポーネント全体をドロップして、いくつかの興味深い部分を呼び出します。 実際の動作を確認したい場合は、こちらをご覧ください。 作業デモ。 それは私のXNUMXつのお気に入りのプログラミング言語に関する私のXNUMXつのお気に入りの本の間で切り替わります。 各本のURLは毎回一意になるため、プレビューを見ることができますが、実際に行われていることを確認するには、[DevToolsネットワーク]タブで物事を調整することをお勧めします。
コード全体を表示
class BookCover extends HTMLElement {
static observedAttributes = ['url'];
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'url') {
this.createMainImage(newValue);
}
}
set preview(val) {
this.previewEl = this.createPreview(val);
this.render();
}
createPreview(val) {
if (typeof val === 'string') {
return base64Preview(val);
} else {
return blurHashPreview(val);
}
}
createMainImage(url) {
this.loaded = false;
const img = document.createElement('img');
img.alt = 'Book cover';
img.addEventListener('load', () => {
if (img === this.imageEl) {
this.loaded = true;
this.render();
}
});
img.src = url;
this.imageEl = img;
}
connectedCallback() {
this.render();
}
render() {
const elementMaybe = this.loaded ? this.imageEl : this.previewEl;
syncSingleChild(this, elementMaybe);
}
}
まず、関心のある属性を登録し、変更されたときに対応します。
static observedAttributes = ['url'];
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'url') {
this.createMainImage(newValue);
}
}
これにより、画像コンポーネントが作成され、ロードされたときにのみ表示されます。
createMainImage(url) {
this.loaded = false;
const img = document.createElement('img');
img.alt = 'Book cover';
img.addEventListener('load', () => {
if (img === this.imageEl) {
this.loaded = true;
this.render();
}
});
img.src = url;
this.imageEl = img;
}
次に、プレビュープロパティがあります。これは、base64プレビュー文字列または blurhash
パケット:
set preview(val) {
this.previewEl = this.createPreview(val);
this.render();
}
createPreview(val) {
if (typeof val === 'string') {
return base64Preview(val);
} else {
return blurHashPreview(val);
}
}
これは、必要なヘルパー関数に依存します。
function base64Preview(val) {
const img = document.createElement('img');
img.src = val;
return img;
}
function blurHashPreview(preview) {
const canvasEl = document.createElement('canvas');
const { w: width, h: height } = preview;
canvasEl.width = width;
canvasEl.height = height;
const pixels = decode(preview.blurhash, width, height);
const ctx = canvasEl.getContext('2d');
const imageData = ctx.createImageData(width, height);
imageData.data.set(pixels);
ctx.putImageData(imageData, 0, 0);
return canvasEl;
}
そして最後に、 render
方法:
connectedCallback() {
this.render();
}
render() {
const elementMaybe = this.loaded ? this.imageEl : this.previewEl;
syncSingleChild(this, elementMaybe);
}
そして、すべてを結び付けるためのいくつかのヘルパーメソッド:
export function syncSingleChild(container, child) {
const currentChild = container.firstElementChild;
if (currentChild !== child) {
clearContainer(container);
if (child) {
container.appendChild(child);
}
}
}
export function clearContainer(el) {
let child;
while ((child = el.firstElementChild)) {
el.removeChild(child);
}
}
これをフレームワークで構築する場合に必要となるよりも少し定型的ですが、利点は、これを任意のフレームワークで再利用できることです。ただし、前述のように、Reactには今のところラッパーが必要です。 。
オッズとエンド
LitのReactラッパーについてはすでに説明しました。 ただし、Stencilを使用している場合は、実際には React専用の個別の出力パイプライン。 そしてマイクロソフトの善良な人々も Litのラッパーに似たものを作成しました、FastWebコンポーネントライブラリに接続されています。
前述したように、Reactという名前ではないすべてのフレームワークは、Webコンポーネントのプロパティの設定を処理します。 いくつかの特別な構文のフレーバーがあることに注意してください。 たとえば、Solid.jsでは、 <your-wc value={12}>
常に value
はプロパティであり、これをオーバーライドできます。 attr
プレフィックス、 <your-wc attr:value={12}>
.
包み込む
Webコンポーネントは興味深いものであり、Web開発ランドスケープの多くの場合十分に活用されていない部分です。 UIまたは「リーフ」コンポーネントを管理することで、単一のJavaScriptフレームワークへの依存を減らすことができます。 SvelteやReactコンポーネントとは対照的に、これらをWebコンポーネントとして作成することは人間工学的ではありませんが、利点は、広く再利用できることです。