フランケンシュタインの移行:フレームワークにとらわれないアプローチ(パート2)

公開: 2022-03-10
簡単なまとめ↬最近、「フランケンシュタイン移行」とは何かについて説明し、従来のタイプの移行と比較して、マイクロサービスWebコンポーネントという2つの主要な構成要素について説明しました。 また、このタイプの移行がどのように機能するかについての理論的基礎も得ました。 その議論を読んだり忘れたりしなかった場合は、最初にパート1に戻ってください。これは、この記事のこの2番目のパートで取り上げるすべてを理解するのに役立ちます。

この記事では、前のパートの推奨事項に従って、アプリケーションの段階的な移行を実行することにより、すべての理論をテストします。 物事を簡単にし、不確実性、未知数、および不必要な推測を減らすために、移行の実際的な例として、単純なToDoアプリケーションでの実践を示すことにしました。

理論をテストする時が来ました
理論をテストする時が来ました。 (大プレビュー)

一般に、一般的なToDoアプリケーションがどのように機能するかを十分に理解していると思います。 このタイプのアプリケーションは、私たちのニーズに非常によく適合します。予測可能ですが、フランケンシュタイン移行のさまざまな側面を示すために必要なコンポーネントの実行可能な最小数があります。 ただし、実際のアプリケーションのサイズと複雑さに関係なく、このアプローチはスケーラブルであり、あらゆるサイズのプロジェクトに適していると考えられます。

TodoMVCアプリケーションのデフォルトビュー
TodoMVCアプリケーションのデフォルトビュー(大プレビュー)

この記事では、出発点として、TodoMVCプロジェクトからjQueryアプリケーションを選びました。これはすでに多くの人に馴染みのある例です。 jQueryは十分にレガシーであり、プロジェクトの実際の状況を反映している可能性があり、最も重要なこととして、最新の動的アプリケーションを強化するために大幅なメンテナンスとハックが必要です。 (これは、より柔軟なものへの移行を検討するのに十分なはずです。)

そのとき移行するこの「より柔軟な」ものは何ですか? 実生活で役立つ非常に実用的なケースを示すために、私は最近最も人気のある2つのフレームワーク、ReactとVueから選択する必要がありました。 しかし、どちらを選んでも、他の方向のいくつかの側面を見逃してしまいます。

ジャンプした後もっと! 以下を読み続けてください↓

したがって、このパートでは、次の両方を実行します。

  • jQueryアプリケーションのReactへの移行、および
  • jQueryアプリケーションのVueへの移行。
私たちの目標:ReactとVueへの移行の結果
私たちの目標:ReactとVueへの移行の結果。 (大プレビュー)

コードリポジトリ

ここに記載されているすべてのコードは公開されており、いつでもアクセスできます。 使用できるリポジトリは2つあります。

  • フランケンシュタインTodoMVC
    このリポジトリには、さまざまなフレームワーク/ライブラリのTodoMVCアプリケーションが含まれています。 たとえば、このリポジトリには、 vueangularjsreactjqueryなどのブランチがあります。
  • フランケンシュタインデモ
    これにはいくつかのブランチが含まれており、各ブランチは、最初のリポジトリで使用可能な、アプリケーション間の特定の移行方向を表します。 特に、 migration/jquery-to-reactmigration/jquery-to-vueようなブランチがあり、これについては後で説明します。

どちらのリポジトリも仕掛品であり、新しいアプリケーションと移行の方向性を備えた新しいブランチを定期的に追加する必要があります。 (あなたも自由に貢献できます! )移行ブランチのコミット履歴はよく構成されており、この記事でカバーできるよりもさらに詳細な追加のドキュメントとして役立つ可能性があります。

さあ、手を汚しましょう! まだまだ先は長いので、スムーズな乗り心地を期待しないでください。 この記事に沿ってどのようにフォローするかはあなた次第ですが、次のことを行うことができます。

  • Frankenstein TodoMVCリポジトリからjqueryブランチのクローンを作成し、以下のすべての手順に厳密に従ってください。
  • または、Frankenstein DemoリポジトリからReactへの移行またはVueへの移行専用のブランチを開き、コミット履歴を追跡することもできます。
  • または、ここで最も重要なコードを強調するので、リラックスして読み続けることができます。実際のコードではなく、プロセスの仕組みを理解することがはるかに重要です。

もう一度、記事の理論的な最初の部分に示されている手順に厳密に従うことをお伝えしたいと思います。

さっそく飛び込みましょう!

  1. マイクロサービスを特定する
  2. ホストからエイリアンへのアクセスを許可する
  3. エイリアンのマイクロサービス/コンポーネントを作成する
  4. エイリアンサービスの周りにWebコンポーネントラッパーを書く
  5. ホストサービスをWebコンポーネントに置き換えます
  6. すべてのコンポーネントをすすぎ、繰り返します
  7. エイリアンに切り替える

1.マイクロサービスを特定する

パート1が示唆しているように、このステップでは、アプリケーションを1つの特定のジョブ専用の小さな独立したサービスに構造化する必要があります。 注意深い読者は、私たちのやることアプリケーションがすでに小さく独立していて、それ自体で単一のマイクロサービスを表すことができることに気付くかもしれません。 これは、このアプリケーションがより広いコンテキストで動作する場合、私が自分でそれを処理する方法です。 ただし、マイクロサービスを特定するプロセスは完全に主観的なものであり、正しい答えは1つもないことを忘れないでください。

したがって、Frankenstein Migrationのプロセスをより詳細に確認するために、さらに一歩進んで、このToDoアプリケーションを2つの独立したマイクロサービスに分割できます。

  1. 新しいアイテムを追加するための入力フィールド。
    このサービスには、これらの要素の近接位置に純粋に基づいて、アプリケーションのヘッダーを含めることもできます。
  2. すでに追加されているアイテムのリスト。
    このサービスはより高度であり、リスト自体とともに、フィルタリング、リストアイテムのアクションなどのアクションも含まれています。
TodoMVCアプリケーションは2つの独立したマイクロサービスに分割されました
TodoMVCアプリケーションは、2つの独立したマイクロサービスに分割されました。 (大プレビュー)

ヒント選択したサービスが本当に独立しているかどうかを確認するには、これらの各サービスを表すHTMLマークアップを削除します。 残りの機能が引き続き機能することを確認してください。 この場合、リストなしで入力フィールドからlocalStorage (このアプリケーションがストレージとして使用している)に新しいエントリを追加できるはずですが、入力フィールドがない場合でも、リストはlocalStorageからのエントリをレンダリングします 潜在的なマイクロサービスのマークアップを削除したときにアプリケーションがエラーをスローする場合、そのようなケースに対処する方法の例については、パート1の「必要に応じてリファクタリング」セクションを参照してください。

もちろん、2番目のサービスとアイテムのリストをさらに分割して、特定のアイテムごとに独立したマイクロサービスにすることもできます。 ただし、この例では細かすぎる可能性があります。 したがって、今のところ、アプリケーションには2つのサービスがあると結論付けています。 それらは独立しており、それぞれが独自の特定のタスクに向けて機能します。 したがって、アプリケーションをマイクロサービスに分割しました。

2.ホストからエイリアンへのアクセスを許可する

これらが何であるかを簡単に思い出させてください。

  • 亭主
    これは、現在のアプリケーションと呼ばれるものです。 それは、私たちがこれから離れようとしているフレームワークで書かれています。 この特定のケースでは、jQueryアプリケーションです。
  • エイリアン
    簡単に言えば、これは、移動しようとしている新しいフレームワークでのHostの段階的な書き直しです。 繰り返しますが、この特定のケースでは、ReactまたはVueアプリケーションです。

ホストとエイリアンを分割するときの経験則は、いつでも、他の1つを壊すことなく、それらのいずれかを開発および展開できる必要があるということです。

ホストとエイリアンを互いに独立させることは、フランケンシュタインの移行にとって非常に重要です。 ただし、これにより、2つの間の通信の調整が少し難しくなります。 2つを一緒に壊さずに、ホストがエイリアンにアクセスできるようにするにはどうすればよいですか?

ホストのサブモジュールとしてエイリアンを追加する

必要なセットアップを実現する方法はいくつかありますが、この基準を満たすようにプロジェクトを編成する最も簡単な形式は、おそらくgitサブモジュールです。 これは、この記事で使用するものです。 この構造の制限と落とし穴を理解するために、gitのサブモジュールがどのように機能するかを注意深く読むのはあなたに任せます。

gitサブモジュールを使用したプロジェクトのアーキテクチャの一般原則は次のようになります。

  • ホストとエイリアンはどちらも独立しており、別々のgitリポジトリに保持されます。
  • ホストはAlienをサブモジュールとして参照します。 この段階で、ホストはエイリアンの特定の状態(コミット)を選択し、それをホストのフォルダー構造のサブフォルダーとして追加します。
gitサブモジュールとしてjQueryTodoMVCアプリケーションに追加されたReactTodoMVC
gitサブモジュールとしてjQueryTodoMVCアプリケーションに追加されたReactTodoMVC。 (大プレビュー)

サブモジュールを追加するプロセスは、どのアプリケーションでも同じです。 git submodulesの指導はこの記事の範囲を超えており、FrankensteinMigration自体とは直接関係ありません。 それでは、考えられる例を簡単に見てみましょう。

以下のスニペットでは、例としてReact方向を使用しています。 その他の移行方向については、 reactをFrankenstein TodoMVCのブランチの名前に置き換えるか、必要に応じてカスタム値に調整してください。

元のjQueryTodoMVCアプリケーションを使用して従う場合:

 $ git submodule add -b react [email protected]:mishunov/frankenstein-todomvc.git react $ git submodule update --remote $ cd react $ npm i

FrankensteinDemoリポジトリからのmigration/jquery-to-react (またはその他の移行方向)ブランチをたどると、Alienアプリケーションはすでにgit submoduleとしてそこにあり、それぞれのフォルダーが表示されます。 ただし、フォルダはデフォルトで空であり、登録されたサブモジュールを更新および初期化する必要があります。

プロジェクトのルート(ホスト)から:

 $ git submodule update --init $ cd react $ npm i

どちらの場合も、Alienアプリケーションの依存関係をインストールしますが、それらはサブフォルダーにサンドボックス化され、ホストを汚染しないことに注意してください。

Alienアプリケーションをホストのサブモジュールとして追加すると、(マイクロサービスの観点から)独立したAlienアプリケーションとHostアプリケーションを取得できます。 ただし、この場合、ホストはエイリアンをサブフォルダーと見なします。これにより、ホストは問題なくエイリアンにアクセスできるようになります。

3.エイリアンマイクロサービス/コンポーネントを作成します

このステップでは、最初に移行するマイクロサービスを決定し、エイリアン側で書き込み/使用する必要があります。 手順1で特定したのと同じサービスの順序に従い、最初のサービス、つまり新しいアイテムを追加するための入力フィールドから始めましょう。 ただし、始める前に、フロントエンドフレームワークの前提に向かって移動しているため、このポイントを超えて、マイクロサービスサービスの代わりに、より有利な用語コンポーネントを使用することに同意しましょう。用語コンポーネントは、ほとんどすべての最新の定義に従います。フレームワーク。

Frankenstein TodoMVCリポジトリのブランチには、最初のサービス「新しいアイテムを追加するための入力フィールド」をヘッダーコンポーネントとして表す結果のコンポーネントが含まれています。

  • Reactのヘッダーコンポーネント
  • Vueのヘッダーコンポーネント

選択したフレームワークでコンポーネントを作成することは、この記事の範囲を超えており、FrankensteinMigrationの一部ではありません。 ただし、エイリアンコンポーネントを作成する際には、注意すべき点がいくつかあります。

独立

まず、エイリアンのコンポーネントは、以前にホスト側で設定されたものと同じ独立性の原則に従う必要があります。コンポーネントは、他のコンポーネントに依存してはなりません。

相互運用性

サービスの独立性のおかげで、おそらく、ホスト内のコンポーネントは、状態管理システム、共有ストレージを介した通信、またはDOMイベントのシステムを介した直接の通信など、確立された方法で通信します。 Alienコンポーネントの「相互運用性」とは、ホストによって確立された同じ通信ソースに接続して、状態の変化に関する情報をディスパッチし、他のコンポーネントの変化をリッスンできる必要があることを意味します。 実際には、これは、ホスト内のコンポーネントがDOMイベントを介して通信する場合、残念ながら、状態管理のみを念頭に置いてAlienコンポーネントを構築しても、このタイプの移行では問題なく機能しないことを意味します。

例として、jQueryコンポーネントの主要な通信チャネルであるjs/storage.jsファイルを見てください。

 ... fetch: function() { return JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]"); }, save: function(todos) { localStorage.setItem(STORAGE_KEY, JSON.stringify(todos)); var event = new CustomEvent("store-update", { detail: { todos } }); document.dispatchEvent(event); }, ...

ここでは、 localStorage (この例はセキュリティ上重要ではないため)を使用してTo Doアイテムを保存し、ストレージへの変更が記録されると、任意のコンポーネントがリッスンできるdocument要素にカスタムDOMイベントをディスパッチします。

同時に、エイリアン側(Reactとしましょう)では、必要に応じて複雑な状態管理通信を設定できます。 ただし、将来のためにそれを維持することはおそらく賢明です。AlienReactコンポーネントをホストに正常に統合するには、ホストが使用するのと同じ通信チャネルに接続する必要があります。 この場合、それはlocalStorageです。 簡単にするために、HostのストレージファイルをAlienにコピーし、コンポーネントをそれに接続しました。

 import todoStorage from "../storage"; class Header extends Component { constructor(props) { this.state = { todos: todoStorage.fetch() }; } componentDidMount() { document.addEventListener("store-update", this.updateTodos); } componentWillUnmount() { document.removeEventListener("store-update", this.updateTodos); } componentDidUpdate(prevProps, prevState) { if (prevState.todos !== this.state.todos) { todoStorage.save(this.state.todos); } } ... }

これで、AlienコンポーネントはHostコンポーネントと同じ言語を話すことができ、その逆も可能です。

4.エイリアンサービスの周りにWebコンポーネントラッパーを書く

現在は4番目のステップに過ぎませんが、かなりの成果を上げています。

  • ホストアプリケーションを独立したサービスに分割しました。これらのサービスは、Alienサービスに置き換える準備ができています。
  • HostとAlienは互いに完全に独立しているが、 git submodulesを介して非常によく接続されているように設定しました。
  • 新しいフレームワークを使用して、最初のAlienコンポーネントを作成しました。

次に、新しいエイリアンコンポーネントがホストで機能できるように、ホストとエイリアンの間にブリッジを設定します。

パート1からのリマインダーホストにパッケージバンドラーが利用可能であることを確認してください。 この記事ではWebpackに依存していますが、この手法がRollupやその他の選択したバンドラーで機能しないという意味ではありません。 ただし、Webpackからのマッピングは実験に任せます。

命名規則

前の記事で述べたように、Webコンポーネントを使用してAlienをホストに統合します。 ホスト側では、新しいファイルjs/frankenstein-wrappers/Header-wrapper.jsを作成します。 (これが最初のフランケンシュタインラッパーになります。)たとえば、「- -wrapper 」サフィックスを追加するだけで、エイリアンアプリケーションのコンポーネントと同じ名前をラッパーに付けることをお勧めします。 これが良いアイデアである理由については後で説明しますが、今のところ、AlienコンポーネントがHeader.js (Reactの場合)またはHeader.vue (Vueの場合)と呼ばれる場合、対応するラッパーがホスト側はHeader-wrapper.jsと呼ばれる必要があります。

最初のラッパーでは、カスタム要素を登録するための基本的な定型文から始めます。

 class FrankensteinWrapper extends HTMLElement {} customElements.define("frankenstein-header-wrapper", FrankensteinWrapper);

次に、この要素のShadowDOMを初期化する必要があります。

Shadow DOMを使用する理由については、パート1を参照してください。

 class FrankensteinWrapper extends HTMLElement { connectedCallback() { this.attachShadow({ mode: "open" }); } }

これで、Webコンポーネントの重要な部分がすべてセットアップされ、Alienコンポーネントをミックスに追加するときが来ました。 まず、Frankensteinラッパーの最初に、Alienコンポーネントのレンダリングに関与するすべてのビットをインポートする必要があります。

 import React from "../../react/node_modules/react"; import ReactDOM from "../../react/node_modules/react-dom"; import HeaderApp from "../../react/src/components/Header"; ...

ここで少し一時停止する必要があります。 ホストのnode_modulesからエイリアンの依存関係をインポートしないことに注意してください。 すべてはreact/サブフォルダーにあるエイリアン自体から来ています。 そのため、ステップ2は非常に重要であり、ホストがエイリアンのアセットに完全にアクセスできるようにすることが重要です。

これで、AlienコンポーネントをWebComponentのShadowDOM内にレンダリングできます。

 ... connectedCallback() { ... ReactDOM.render(<HeaderApp />, this.shadowRoot); } ...

この場合、Reactは他に何も必要ありません。 ただし、Vueコンポーネントをレンダリングするには、次のようにVueコンポーネントを含むラッピングノードを追加する必要があります。

 ... connectedCallback() { const mountPoint = document.createElement("div"); this.attachShadow({ mode: "open" }).appendChild(mountPoint); new Vue({ render: h => h(VueHeader) }).$mount(mountPoint); } ...

この理由は、ReactとVueがコンポーネントをレンダリングする方法の違いです。Reactは参照されるDOMノードにコンポーネントを追加しますが、Vueは参照されるDOMノードをコンポーネントに置き換えます。 したがって、 Vueに対して.$mount(this.shadowRoot)を実行すると、基本的にShadowDOMが置き換えられます。

今のところ、ラッパーに対して行う必要があるのはこれだけです。 jQueryからReactへの移行とjQueryからVueへの移行の両方の方向でのFrankensteinラッパーの現在の結果は、次の場所にあります。

  • Reactコンポーネント用のフランケンシュタインラッパー
  • Vueコンポーネント用のフランケンシュタインラッパー

フランケンシュタインラッパーの仕組みを要約すると、次のようになります。

  1. カスタム要素を作成し、
  2. Shadow DOMを開始し、
  3. Alienコンポーネントのレンダリングに必要なすべてのものをインポートします。
  4. カスタム要素のShadowDOM内でAlienコンポーネントをレンダリングします。

ただし、これはホスト内のエイリアンを自動的にレンダリングしません。 既存のホストマークアップを新しいFrankensteinラッパーに置き換える必要があります。

シートベルトを締めてください、それは人が期待するほど簡単ではないかもしれません!

5.ホストサービスをWebコンポーネントに置き換えます

続けて、新しいHeader-wrapper.jsファイルをindex.htmlに追加し、既存のヘッダーマークアップを新しく作成された<frankenstein-header-wrapper>カスタム要素に置き換えます。

 ... <!-- <header class="header">--> <!-- <h1>todos</h1>--> <!-- <input class="new-todo" placeholder="What needs to be done?" autofocus>--> <!-- </header>--> <frankenstein-header-wrapper></frankenstein-header-wrapper> ... <script type="module" src="js/frankenstein-wrappers/Header-wrapper.js"></script>

残念ながら、これはそれほど単純には機能しません。 ブラウザを開いてコンソールを確認すると、 Uncaught SyntaxError待機しています。 ブラウザとES6モジュールのサポートに応じて、ES6のインポートまたはAlienコンポーネントのレンダリング方法に関連します。 いずれにせよ、私たちはそれについて何かをしなければなりませんが、問題と解決策はほとんどの読者にとってなじみがあり明確でなければなりません。

5.1。 必要に応じてWebpackとBabelを更新します

Frankensteinラッパーを統合する前に、WebpackとBabelの魔法を使う必要があります。 これらのツールのラングリングは記事の範囲を超えていますが、Frankensteinデモリポジトリで対応するコミットを確認できます。

  • Reactに移行するための構成
  • Vueに移行するための構成

基本的に、ファイルの処理と、 Webpackの構成新しいエントリポイントfrankensteinを設定して、Frankensteinラッパーに関連するすべてのものを1か所に含めます。

HostのWebpackがAlienコンポーネントとWebコンポーネントの処理方法を理解したら、Hostのマークアップを新しいFrankensteinラッパーに置き換える準備が整います。

5.2。 実際のコンポーネントの交換

これで、コンポーネントの交換は簡単になります。 ホストのindex.htmlで、次の手順を実行します。

  1. <header class="header"> DOM要素を<frankenstein-header-wrapper>置き換えます;
  2. 新しいスクリプトfrankenstein.jsを追加します。 これは、Frankensteinラッパーに関連するすべてを含むWebpackの新しいエントリポイントです。
 ... <!-- We replace <header class="header"> --> <frankenstein-header-wrapper></frankenstein-header-wrapper> ... <script src="./frankenstein.js"></script>

それでおしまい! 必要に応じてサーバーを再起動し、ホストに統合されたAlienコンポーネントの魔法を目撃してください。

しかし、まだ何かが欠けているようです。 ホストコンテキストのAlienコンポーネントは、スタンドアロンのAlienアプリケーションのコンテキストと同じようには見えません。 それは単にスタイルが設定されていません。

ホストに統合された後のスタイルなしのAlienReactコンポーネント
ホストに統合された後のスタイルなしのAlienReactコンポーネント(大プレビュー)

なんでそうなの? コンポーネントのスタイルは、Alienコンポーネントと自動的にホストに統合されるべきではありませんか? 私は彼らがそうすることを望みます、しかしあまりにも多くの状況のように、それは依存します。 フランケンシュタイン移行の難しい部分に到達しています。

5.3。 エイリアンコンポーネントのスタイリングに関する一般情報

まず第一に、皮肉なことに、物事の仕組みにバグはありません。 すべてが機能するように設計されています。 これを説明するために、コンポーネントのスタイリングのさまざまな方法について簡単に説明します。

グローバルスタイル

私たちは皆、これらに精通しています。グローバルスタイルは、特定のコンポーネントなしで配布でき(通常は配布され)、ページ全体に適用できます。 グローバルスタイルは、セレクターが一致するすべてのDOMノードに影響します。

グローバルスタイルの例としては、 index.htmlにある<style> >タグと<link rel="stylesheet">タグがあります。 または、グローバルスタイルシートをルートJSモジュールにインポートして、すべてのコンポーネントがそれにアクセスできるようにすることもできます。

この方法でアプリケーションをスタイリングすることの問題は明らかです。大規模なアプリケーションでモノリシックスタイルシートを維持することは非常に困難になります。 また、前の記事で見たように、グローバルスタイルは、ReactやVueのように、メインのDOMツリーでまっすぐにレンダリングされるコンポーネントを簡単に壊す可能性があります。

バンドルされたスタイル

これらのスタイルは通常、コンポーネント自体と緊密に結合されており、コンポーネントなしで配布されることはめったにありません。 スタイルは通常、コンポーネントと同じファイルにあります。 このタイプのスタイリングの良い例は、ReactまたはCSSモジュールのスタイル付きコンポーネントとVueの単一ファイルコンポーネントのスコープ付きCSSです。 ただし、バンドルされたスタイルを作成するためのさまざまなツールに関係なく、それらのほとんどの基本的な原則は同じです。ツールは、スタイルが他のコンポーネントやグローバルを壊さないように、コンポーネントで定義されたスタイルをロックダウンするスコーピングメカニズムを提供します。スタイル。

スコープスタイルが壊れやすいのはなぜですか?

パート1では、FrankensteinMigrationでのShadowDOMの使用を正当化する際に、スコーピングとカプセル化のトピックと、ShadowDOMのカプセル化がスコーピングスタイリングツールとどのように異なるかについて簡単に説明しました。 ただし、スコーピングツールがコンポーネントにこのような壊れやすいスタイリングを提供する理由については説明しませんでした。現在、スタイリングされていないAlienコンポーネントに直面すると、理解するために不可欠になります。

最新のフレームワークのすべてのスコーピングツールは同様に機能します。

  • スコープやカプセル化についてあまり考えずに、何らかの方法でコンポーネントのスタイルを記述します。
  • インポート/埋め込みスタイルシートを使用して、WebpackやRollupなどのバンドルシステムを介してコンポーネントを実行します。
  • バンドラーは、一意のCSSクラスまたはその他の属性を生成し、HTMLと対応するスタイルシートの両方に対して個別のセレクターを作成して挿入します。
  • バンドラーは、ドキュメントの<head><style>エントリを作成し、そこに独自の混合セレクターを使用してコンポーネントのスタイルを配置します。

それはほとんどそれです。 それは機能し、多くの場合正常に機能します。 そうでない場合を除いて:すべてのコンポーネントのスタイルがグローバルスタイリングスコープに存在する場合、たとえば、より高い特異性を使用して、それらを簡単に破ることができます。 これは、スコーピングツールの潜在的な脆弱性を説明していますが、なぜAlienコンポーネントが完全にスタイル設定されていないのですか?

DevToolsを使用して現在のホストを見てみましょう。 たとえば、新しく追加されたフランケンシュタインラッパーをAlien Reactコンポーネントで検査すると、次のようになります。

内部にエイリアンコンポーネントを含むフランケンシュタインラッパー。エイリアンのノードにある固有のCSSクラスに注意してください。
内部にエイリアンコンポーネントを含むフランケンシュタインラッパー。 エイリアンのノードにある固有のCSSクラスに注意してください。 (大プレビュー)

そのため、Webpackはコンポーネントに固有のCSSクラスを生成します。 すごい! では、スタイルはどこにありますか? ええと、スタイルは正確に設計された場所にあります—ドキュメントの<head>にあります。

AlienコンポーネントはFrankensteinラッパー内にありますが、そのスタイルはドキュメントの先頭にあります。
AlienコンポーネントはFrankensteinラッパー内にありますが、そのスタイルはドキュメントの<head>にあります。 (大プレビュー)

したがって、すべてが正常に機能し、これが主な問題です。 AlienコンポーネントはShadowDOMに存在するため、パート1で説明したように、Shadow DOMは、ページの残りの部分からのコンポーネントと、シャドウの境界を越えることができないコンポーネント用に新しく生成されたスタイルシートを含むグローバルスタイルの完全なカプセル化を提供します。 Alienコンポーネントにアクセスします。 したがって、エイリアンコンポーネントはスタイル設定されないままになります。 ただし、ここで、問題を解決するための戦術を明確にする必要があります。コンポーネントのスタイルを、コンポーネントが存在するのと同じShadow DOMに(ドキュメントの<head>ではなく)配置する必要があります。

5.4。 エイリアンコンポーネントのスタイルの修正

これまで、どのフレームワークにも移行するプロセスは同じでした。 ただし、ここで状況は分岐し始めます。すべてのフレームワークには、コンポーネントのスタイル設定方法に関する推奨事項があるため、問題に取り組む方法は異なります。 ここでは、最も一般的なケースについて説明しますが、使用するフレームワークでコンポーネントのスタイルを設定する独自の方法を使用する場合は、コンポーネントのスタイルを<head>ではなくShadowDOMに配置するなどの基本的な戦術を覚えておく必要があります。

この章では、次の修正について説明します。

  • VueのCSSモジュールにバンドルされたスタイル(スコープ付きCSSの戦術は同じです)。
  • Reactのstyled-componentsにバンドルされたスタイル。
  • 一般的なCSSモジュールとグローバルスタイル。 CSSモジュールは一般にグローバルスタイルシートに非常に似ており、任意のコンポーネントからインポートできるため、スタイルを特定のコンポーネントから切り離すことができるため、これらを組み合わせます。

最初の制約:スタイリングを修正するために行うことは、Alienコンポーネント自体を壊してはなりません。 そうしないと、エイリアンとホストシステムの独立性が失われます。 したがって、スタイリングの問題に対処するために、バンドラーの構成またはフランケンシュタインラッパーのいずれかに依存します。

VueとShadowDOMにバンドルされたスタイル

Vueアプリケーションを作成している場合は、おそらく単一のファイルコンポーネントを使用しています。 Webpackも使用している場合は、2つのローダーvue-loadervue-style-loaderに精通している必要があります。 前者を使用すると、これらの単一ファイルコンポーネントを記述できますが、後者を使用すると、コンポーネントのCSSを<style>タグとしてドキュメントに動的に挿入できます。 デフォルトでは、 vue-style-loaderはコンポーネントのスタイルをドキュメントの<head>に挿入します。 ただし、どちらのパッケージも構成でshadowModeオプションを受け入れるため、デフォルトの動作を簡単に変更し、(オプションの名前が示すように)スタイルをShadowDOMに挿入できます。 実際の動作を見てみましょう。

Webpackの構成

最低限、Webpack構成ファイルには次のものが含まれている必要があります。

 const VueLoaderPlugin = require('vue-loader/lib/plugin'); ... module: { rules: [ { test: /\.vue$/, loader: 'vue-loader', options: { shadowMode: true } }, { test: /\.css$/, include: path.resolve(__dirname, '../vue'), use: [ { loader:'vue-style-loader', options: { shadowMode: true } }, 'css-loader' ] } ], plugins: [ new VueLoaderPlugin() ] }

実際のアプリケーションでは、 test: /\.css$/ブロックは、ホストとエイリアンの両方の構成を考慮して、より洗練されたものになります(おそらくoneOfルールが含まれます)。 ただし、この場合、jQueryはindex.htmlの単純な<link rel="stylesheet">でスタイル設定されているため、Webpackを介してHostのスタイルを構築することはなく、Alienのみに対応するのが安全です。

ラッパー構成

Webpackの構成に加えて、Vueが正しいShadow DOMを指すように、Frankensteinラッパーを更新する必要もあります。 Header-wrapper.jsでは、Vueコンポーネントのレンダリングに、FrankensteinラッパーのshadowRootにつながるshadowRootプロパティを含める必要があります。

 ... new Vue({ shadowRoot: this.shadowRoot, render: h => h(VueHeader) }).$mount(mountPoint); ...

ファイルを更新してサーバーを再起動すると、DevToolsに次のようなものが表示されます。

フランケンシュタインラッパー内に配置されたAlienVueコンポーネントにバンドルされたスタイルで、すべての一意のCSSクラスが保持されます。
フランケンシュタインラッパー内に配置されたAlienVueコンポーネントにバンドルされたスタイルで、すべての一意のCSSクラスが保持されます。 (大プレビュー)

最後に、VueコンポーネントのスタイルはShadowDOM内にあります。 同時に、アプリケーションは次のようになります。

ヘッダーコンポーネントは、本来あるべきように見え始めます。しかし、まだ何かが欠けています。
ヘッダーコンポーネントは、本来あるべきように見え始めます。 しかし、まだ何かが欠けています。 (大プレビュー)

Vueアプリケーションに似たものを取得し始めます。コンポーネントにバンドルされているスタイルがラッパーのShadowDOMに挿入されますが、コンポーネントは想定どおりに表示されません。 その理由は、元のVueアプリケーションでは、コンポーネントがバンドルされたスタイルだけでなく、部分的にグローバルスタイルでスタイル設定されているためです。 ただし、グローバルスタイルを修正する前に、React統合をVue統合と同じ状態にする必要があります。

ReactおよびShadowDOMにバンドルされたスタイル

Reactコンポーネントのスタイルを設定する方法は多数あるため、Frankenstein MigrationでAlienコンポーネントを修正するための特定のソリューションは、最初にコンポーネントのスタイルを設定する方法によって異なります。 最も一般的に使用される代替案について簡単に説明します。

スタイル付きコンポーネント

styled-componentsは、Reactコンポーネントをスタイリングする最も一般的な方法の1つです。 ヘッダーReactコンポーネントの場合、styled-componentsはまさに私たちがスタイルを設定する方法です。 これは古典的なCSS-in-JSアプローチであるため、たとえば.cssファイルや.jsファイルの場合のように、バンドラーをフックできる専用の拡張子を持つファイルはありません。 幸い、styled-componentsを使用すると、 StyleSheetManager支援コンポーネントを使用して、ドキュメントのheadではなく、カスタムノード(この場合はShadow DOM)にコンポーネントのスタイルを挿入できます。 これは事前定義されたコンポーネントであり、 targetプロパティを受け入れるstyled-componentsパッケージとともにインストールされ、「スタイル情報を挿入するための代替DOMノード」を定義します。 まさに私たちが必要なものです! さらに、Webpackの構成を変更する必要もありません。すべてはFrankensteinラッパー次第です。

ReactAlienコンポーネントを含むHeader-wrapper.jsを次の行で更新する必要があります。

 ... import { StyleSheetManager } from "../../react/node_modules/styled-components"; ... const target = this.shadowRoot; ReactDOM.render( <StyleSheetManager target={target}> <HeaderApp /> </StyleSheetManager>, appWrapper ); ...

ここでは、 StyleSheetManagerコンポーネント(HostからではなくAlienから)をインポートし、Reactコンポーネントをラップします。 同時に、 shadowRootを指すtargetプロパティを送信します。 それでおしまい。 サーバーを再起動する場合は、DevToolsに次のようなものが表示される必要があります。

React Alienコンポーネントにバンドルされたスタイルは、すべての一意のCSSクラスが保持された状態でFrankensteinラッパー内に配置されます。
React Alienコンポーネントにバンドルされたスタイルは、すべての一意のCSSクラスが保持された状態でFrankensteinラッパー内に配置されます。 (大プレビュー)

これで、コンポーネントのスタイルは<head>ではなくShadowDOMになりました。 このようにして、アプリのレンダリングは、以前にVueアプリで見たものに似ています。

バンドルされたスタイルをフランケンシュタインラッパーに移動した後、AlienReactコンポーネントの見栄えが良くなり始めます。しかし、私たちはまだそこにいません。
バンドルされたスタイルをフランケンシュタインラッパーに移動した後、AlienReactコンポーネントの見栄えが良くなり始めます。 しかし、私たちはまだそこにいません。 (大プレビュー)

同じ話: styled-componentsは、Reactコンポーネントのスタイルのバンドルされた部分のみを担当し、グローバルスタイルが残りのビットを管理します。 もう1つのタイプのスタイリングコンポーネントを確認した後、少しでグローバルスタイルに戻ります。

CSSモジュール

以前に修正したVueコンポーネントを詳しく見ると、CSSモジュールがまさにそのコンポーネントのスタイル設定方法であることに気付くかもしれません。 However, even if we style it with Scoped CSS (another recommended way of styling Vue components) the way we fix our unstyled component doesn't change: it is still up to vue-loader and vue-style-loader to handle it through shadowMode: true option.

When it comes to CSS Modules in React (or any other system using CSS Modules without any dedicated tools), things get a bit more complicated and less flexible, unfortunately.

Let's take a look at the same React component which we've just integrated, but this time styled with CSS Modules instead of styled-components. The main thing to note in this component is a separate import for stylesheet:

 import styles from './Header.module.css'

The .module.css extension is a standard way to tell React applications built with the create-react-app utility that the imported stylesheet is a CSS Module. The stylesheet itself is very basic and does precisely the same our styled-components do.

Integrating CSS modules into a Frankenstein wrapper consists of two parts:

  • Enabling CSS Modules in bundler,
  • Pushing resulting stylesheet into Shadow DOM.

I believe the first point is trivial: all you need to do is set { modules: true } for css-loader in your Webpack configuration. Since, in this particular case, we have a dedicated extension for our CSS Modules ( .module.css ), we can have a dedicated configuration block for it under the general .css configuration:

 { test: /\.css$/, oneOf: [ { test: /\.module\.css$/, use: [ ... { loader: 'css-loader', options: { modules: true, } } ] } ] }

Note : A modules option for css-loader is all we have to know about CSS Modules no matter whether it's React or any other system. When it comes to pushing resulting stylesheet into Shadow DOM, however, CSS Modules are no different from any other global stylesheet.

By now, we went through the ways of integrating bundled styles into Shadow DOM for the following conventional scenarios:

  • Vue components, styled with CSS Modules. Dealing with Scoped CSS in Vue components won't be any different;
  • React components, styled with styled-components;
  • Components styled with raw CSS Modules (without dedicated tools like those in Vue). For these, we have enabled support for CSS modules in Webpack configuration.

However, our components still don't look as they are supposed to because their styles partially come from global styles . Those global styles do not come to our Frankenstein wrappers automatically. Moreover, you might get into a situation in which your Alien components are styled exclusively with global styles without any bundled styles whatsoever. So let's finally fix this side of the story.

Global Styles And Shadow DOM

Having your components styled with global styles is neither wrong nor bad per se: every project has its requirements and limitations. However, the best you can do for your components if they rely on some global styles is to pull those styles into the component itself. This way, you have proper easy-to-maintain self-contained components with bundled styles.

Nevertheless, it's not always possible or reasonable to do so: several components might share some styling, or your whole styling architecture could be built using global stylesheets that are split into the modular structure, and so on.

So having an opportunity to pull in global styles into our Frankenstein wrappers wherever it's required is essential for the success of this type of migration. Before we get to an example, keep in mind that this part is the same for pretty much any framework of your choice — be it React, Vue or anything else using global stylesheets!

Let's get back to our Header component from the Vue application. Take a look at this import:

 import "todomvc-app-css/index.css";

This import is where we pull in the global stylesheet. In this case, we do it from the component itself. It's only one way of using global stylesheet to style your component, but it's not necessarily like this in your application.

Some parent module might add a global stylesheet like in our React application where we import index.css only in index.js , and then our components expect it to be available in the global scope. Your component's styling might even rely on a stylesheet, added with <style> or <link> to your index.html . 関係ありません。 What matters, however, is that you should expect to either import global stylesheets in your Alien component (if it doesn't harm the Alien application) or explicitly in the Frankenstein wrapper. Otherwise, the wrapper would not know that the Alien component needs any stylesheet other than the ones already bundled with it.

Caution . If there are many global stylesheets to be shared between Alien components and you have a lot of such components, this might harm the performance of your Host application under the migration period.

Here is how import of a global stylesheet, required for the Header component, is done in Frankenstein wrapper for React component:

 // we import directly from react/, not from Host import '../../react/node_modules/todomvc-app-css/index.css'

Nevertheless, by importing a stylesheet this way, we still bring the styles to the global scope of our Host, while what we need is to pull in the styles into our Shadow DOM. これをどのように行うのですか?

Webpack configuration for global stylesheets & Shadow DOM

First of all, you might want to add an explicit test to make sure that we process only the stylesheets coming from our Alien. In case of our React migration, it will look similar to this:

 test: /\.css$/, oneOf: [ // this matches stylesheets coming from /react/ subfolder { test: /\/react\//, use: [] }, ... ]

In case of Vue application, obviously, you change test: /\/react\// with something like test: /\/vue\// . Apart from that, the configuration will be the same for any framework. Next, let's specify the required loaders for this block.

 ... use: [ { loader: 'style-loader', options: { ... } }, 'css-loader' ]

注意すべき2つのこと。 First, you have to specify modules: true in css-loader 's configuration if you're processing CSS Modules of your Alien application.

Second, we should convert styles into <style> tag before injecting those into Shadow DOM. In the case of Webpack, for that, we use style-loader . The default behavior for this loader is to insert styles into the document's head. 通常。 And this is precisely what we don't want: our goal is to get stylesheets into Shadow DOM. However, in the same way we used target property for styled-components in React or shadowMode option for Vue components that allowed us to specify custom insertion point for our <style> tags, regular style-loader provides us with nearly same functionality for any stylesheet: the insert configuration option is exactly what helps us achieve our primary goal. 素晴らしいニュース! Let's add it to our configuration.

 ... { loader: 'style-loader', options: { insert: 'frankenstein-header-wrapper' } }

However, not everything is so smooth here with a couple of things to keep in mind.

グローバルスタイルシートとstyle-loaderinsertオプション

このオプションのドキュメントを確認すると、このオプションは構成ごとに1つのセレクターを使用することがわかります。 つまり、グローバルスタイルを必要とする複数のエイリアンコンポーネントをフランケンシュタインラッパーに取り込む場合は、フランケンシュタインラッパーごとにstyle-loaderを指定する必要があります。 実際には、これは、おそらく、すべてのラッパーにサービスを提供するために、構成ブロックのoneOfルールに依存する必要があることを意味します。

 { test: /\/react\//, oneOf: [ { test: /1-TEST-FOR-ALIEN-FILE-PATH$/, use: [ { loader: 'style-loader', options: { insert: '1-frankenstein-wrapper' } }, `css-loader` ] }, { test: /2-TEST-FOR-ALIEN-FILE-PATH$/, use: [ { loader: 'style-loader', options: { insert: '2-frankenstein-wrapper' } }, `css-loader` ] }, // etc. ], }

あまり柔軟ではありません、私は同意します。 それでも、移行するコンポーネントが何百もない限り、大したことではありません。 そうしないと、Webpack構成の保守が困難になる可能性があります。 ただし、実際の問題は、ShadowDOM用のCSSセレクターを記述できないことです。

これを解決しようとすると、 insertオプションがプレーンセレクターの代わりに関数を使用して、挿入用のより高度なロジックを指定できることに気付くかもしれません。 これにより、このオプションを使用して、スタイルシートをShadowDOMに直接挿入できます。 簡略化した形式では、次のようになります。

 insert: function(element) { var parent = document.querySelector('frankenstein-header-wrapper').shadowRoot; parent.insertBefore(element, parent.firstChild); }

魅力的ですね。 ただし、これは私たちのシナリオでは機能しないか、最適とはほど遠いものになります。 <frankenstein-header-wrapper>は、実際にindex.htmlから入手できます(ステップ5.2で追加したため)。 ただし、WebpackがAlienコンポーネントまたはFrankensteinラッパーのいずれかのすべての依存関係(スタイルシートを含む)を処理する場合、ShadowDOMはFrankensteinラッパーでまだ初期化されていません。インポートはその前に処理されます。 したがって、 insertをshadowRootに直接向けると、エラーが発生します。

Webpackがスタイルシートの依存関係を処理する前にShadowDOMが初期化されることを保証できるのは1つのケースだけです。 Alienコンポーネントがスタイルシート自体をインポートせず、それをインポートするのがFrankensteinラッパーに任されている場合は、Shadow DOMを設定した後、動的インポートを使用して必要なスタイルシートをインポートできます。

 this.attachShadow({ mode: "open" }); import('../vue/node_modules/todomvc-app-css/index.css');

これは機能します。このようなインポートを上記のinsert構成と組み合わせると、実際に正しいShadow DOMが検出され、それに<style>タグが挿入されます。 それでも、スタイルシートの取得と処理には時間がかかります。つまり、接続が遅いユーザーやデバイスが遅いユーザーは、スタイルシートがラッパーのShadow DOM内に配置される前に、スタイルが設定されていないコンポーネントの瞬間に直面する可能性があります。

スタイルが設定されていないAlienコンポーネントは、グローバルスタイルシートがインポートされてShadowDOMに追加される前にレンダリングされます。
スタイルが設定されていないAlienコンポーネントは、グローバルスタイルシートがインポートされてShadowDOMに追加される前にレンダリングされます。 (大プレビュー)

したがって、全体として、 insertは関数を受け入れますが、残念ながらそれだけでは不十分であり、 frankenstein-header-wrapperようなプレーンなCSSセレクターにフォールバックする必要があります。 ただし、これによってスタイルシートがShadow DOMに自動的に配置されることはなく、スタイルシートはShadowDOMの外部の<frankenstein-header-wrapper>に存在します。

style-loaderは、インポートされたスタイルシートをFrankensteinラッパーに配置しますが、ShadowDOMの外部に配置します。
style-loaderは、インポートされたスタイルシートをFrankensteinラッパーに配置しますが、ShadowDOMの外部に配置します。 (大プレビュー)

パズルのピースがもう1つ必要です。

グローバルスタイルシートとShadowDOMのラッパー構成

幸い、ラッパー側での修正は非常に簡単です。ShadowDOMが初期化されると、現在のラッパーで保留中のスタイルシートをチェックし、ShadowDOMにプルする必要があります。

グローバルスタイルシートのインポートの現在の状態は次のとおりです。

  • ShadowDOMに追加する必要のあるスタイルシートをインポートします。 スタイルシートは、Alienコンポーネント自体にインポートすることも、Frankensteinラッパーに明示的にインポートすることもできます。 たとえば、Reactに移行する場合、インポートはラッパーから初期化されます。 ただし、Vueへの移行では、同様のコンポーネント自体が必要なスタイルシートをインポートするため、ラッパーに何もインポートする必要はありません。
  • 上で指摘したように、WebpackがAlienコンポーネントの.cssインポートを処理するとき、 style-loaderinsertオプションのおかげで、スタイルシートはFrankensteinラッパーに挿入されますが、ShadowDOMの外部に挿入されます。

フランケンシュタインラッパーでのShadowDOMの簡略化された初期化は、現在(スタイルシートをプルする前に)次のようになります。

 this.attachShadow({ mode: "open" }); ReactDOM.render(); // or `new Vue()`

ここで、スタイルが設定されていないコンポーネントのちらつきを避けるために、Shadow DOMの初期化、Alienコンポーネントのレンダリングの前に、必要なすべてのスタイルシートをプルする必要があります。

 this.attachShadow({ mode: "open" }); Array.prototype.slice .call(this.querySelectorAll("style")) .forEach(style => { this.shadowRoot.prepend(style); }); ReactDOM.render(); // or new Vue({})

これは多くの詳細を含む長い説明でしたが、主に、グローバルスタイルシートをShadowDOMに取り込むために必要なすべてのことです。

  • Webpack構成で、必要なフランケンシュタインラッパーを指すinsertオプション付きstyle-loaderを追加します。
  • ラッパー自体で、Shadow DOMの初期化後、Alienコンポーネントのレンダリングの前に「保留中」のスタイルシートをプルします。

これらの変更を実装した後、コンポーネントには必要なものがすべて揃っているはずです。 追加したい(これは必須ではありません)唯一のものは、ホストの環境でAlienコンポーネントを微調整するためのカスタムCSSです。 Hostで使用する場合は、Alienコンポーネントのスタイルを完全に変えることもできます。 これは記事の要点を超えていますが、ラッパーの最終的なコードを見てください。ここでは、ラッパーレベルで単純なスタイルをオーバーライドする方法の例を見つけることができます。

  • Reactコンポーネントのフランケンシュタインラッパー
  • Vueコンポーネントのフランケンシュタインラッパー

移行のこのステップで、Webpackの構成を確認することもできます。

  • スタイル付きコンポーネントと反応す​​るための移行
  • CSSモジュールと反応するための移行
  • Vueへの移行

そして最後に、コンポーネントは意図したとおりに表示されます。

VueとReactで記述されたヘッダーコンポーネントを移行した結果。やること項目のリストはまだjQueryアプリケーションです。
VueとReactで記述されたヘッダーコンポーネントを移行した結果。 やること項目のリストはまだjQueryアプリケーションです。 (大プレビュー)

5.5。 エイリアンコンポーネントの修正スタイルの概要

これは、この章でこれまでに学んだことを要約する絶好の機会です。 エイリアンコンポーネントのスタイリングを修正するために膨大な作業をしなければならなかったように見えるかもしれません。 ただし、要約すると次のようになります。

  • ReactまたはCSSモジュールのstyled-componentsとVueのスコープ付きCSSで実装されたバンドルされたスタイルの修正は、FrankensteinラッパーまたはWebpack構成の数行と同じくらい簡単です。
  • CSSモジュールで実装されたスタイルの修正は、 css-loader構成の1行から始まります。 その後、CSSモジュールはグローバルスタイルシートとして扱われます。
  • グローバルスタイルシートを修正するには、Webpackのinsertオプションを使用してstyle-loaderパッケージを構成し、ラッパーのライフサイクルの適切なタイミングでスタイルシートをShadowDOMに取り込むようにFrankensteinラッパーを更新する必要があります。

結局のところ、適切なスタイルのAlienコンポーネントがホストに移行されました。 ただし、移行先のフレームワークに応じて、気になる場合と気にならない場合が1つだけあります。

最初の朗報: Vueに移行する場合、デモは正常に機能し、移行されたVueコンポーネントから新しいToDoアイテムを追加できるはずです。 ただし、 Reactに移行していて、新しいTo Doアイテムを追加しようとすると、成功しません。 新しいアイテムの追加は単に機能せず、エントリはリストに追加されません。 しかし、なぜ? どうしたの? 偏見はありませんが、Reactにはいくつかの点について独自の意見があります。

5.6。 ShadowDOMでのReactおよびJSイベント

Reactのドキュメントに何が記載されていても、ReactはWebコンポーネントにあまり馴染みがありません。 ドキュメントの例の単純さは批判に耐えるものではなく、Webコンポーネントでリンクをレンダリングするよりも複雑なものはいくつかの調査と調査が必要です。

Alienコンポーネントのスタイリングを修正しているときに見たように、Vueではほとんど箱から出してWebコンポーネントに適合しますが、ReactはWebコンポーネントに対応していません。 今のところ、Reactコンポーネントを少なくともWebコンポーネント内で見栄えよくする方法を理解していますが、修正する機能とJavaScriptイベントもあります。

簡単に言うと、Shadow DOMはイベントをカプセル化してリターゲットしますが、 ReactはShadow DOMのこの動作をネイティブにサポートしていないため、ShadowDOM内からのイベントをキャッチしません。 この振る舞いにはさらに深い理由があり、さらに詳細や議論を掘り下げたい場合は、Reactのバグトラッカーにも未解決の問題があります。

幸いなことに、賢い人々が私たちのために解決策を用意してくれました。 @josephnvuがソリューションの基盤を提供し、LukasBombachがそれをreact-shadow-dom-retarget-events eventsnpmモジュールに変換しました。 したがって、パッケージをインストールし、パッケージのページの指示に従い、ラッパーのコードを更新すると、Alienコンポーネントが魔法のように機能し始めます。

 import retargetEvents from 'react-shadow-dom-retarget-events'; ... ReactDOM.render( ... ); retargetEvents(this.shadowRoot);

パフォーマンスを向上させたい場合は、パッケージのローカルコピーを作成し(MITライセンスで許可されています)、Frankensteinデモリポジトリで行われるように、リッスンするイベントの数を制限できます。 この例では、どのイベントをリターゲットしてそれらのみを指定する必要があるかを知っています。

これで、最初のスタイルで完全に機能するAlienコンポーネントの適切な移行が最終的に完了しました(長いプロセスでした)。 おいしい飲み物を手に入れましょう。 あなたはそれに値する!

6.すべてのコンポーネントをすすぎ、繰り返します

最初のコンポーネントを移行した後、すべてのコンポーネントに対してこのプロセスを繰り返す必要があります。 フランケンシュタインデモの場合、残っているのは1つだけです。1つは、やること項目のリストをレンダリングする責任があります。

新しいコンポーネント用の新しいラッパー

新しいラッパーを追加することから始めましょう。 上記で説明した命名規則に従って(ReactコンポーネントはMainSection.jsと呼ばれるため)、Reactへの移行における対応するラッパーはMainSection-wrapper.jsと呼ばれる必要があります。 同時に、Vueの同様のコンポーネントはListing.vueと呼ばれるため、Vueへの移行で対応するラッパーはListing-wrapper.jsと呼ばれる必要があります。 ただし、命名規則に関係なく、ラッパー自体は、すでに持っているものとほぼ同じになります。

  • Reactリストのラッパー
  • Vueリスト用ラッパー

Reactアプリケーションのこの2番目のコンポーネントで紹介する興味深いことが1つだけあります。 そのため、または別の理由で、コンポーネントでjQueryプラグインを使用したい場合があります。 Reactコンポーネントの場合、2つのことを導入しました。

  • jQueryを使用するBootstrapのツールチッププラグイン、
  • .addClass().removeClass() )などのCSSクラスの切り替え。

    クラスを追加/削除するためのjQueryのこの使用は、純粋に例示的なものです。 実際のプロジェクトでは、このシナリオにjQueryを使用しないでください。代わりにプレーンJavaScriptを使用してください。

もちろん、jQueryから移行するときにAlienコンポーネントにjQueryを導入するのは奇妙に見えるかもしれませんが、ホストはこの例のホストとは異なる可能性があります。AngularJSなどから移行する可能性があります。 また、コンポーネントのjQuery機能とグローバルjQueryは必ずしも同じものではありません。

ただし、問題は、コンポーネントがAlienアプリケーションのコンテキストで正常に機能することを確認しても、Shadow DOMに配置すると、jQueryプラグインやjQueryに依存するその他のコードが機能しないことです。

jQuery In Shadow DOM

ランダムなjQueryプラグインの一般的な初期化を見てみましょう。

 $('.my-selector').fancyPlugin();

このように、 .my-selectorを持つすべての要素は、 fancyPluginによって処理されます。 この形式の初期化は、 .my-selectorがグローバルDOMに存在することを前提としています。 ただし、スタイルの場合と同様に、このような要素がShadow DOMに配置されると、シャドウ境界によってjQueryがその要素に侵入するのを防ぎます。 その結果、jQueryはShadowDOM内の要素を見つけることができません。

解決策は、jQueryが検索するルート要素を定義するセレクターにオプションの2番目のパラメーターを提供することです。 そして、これは、 shadowRootを提供できる場所です。

 $('.my-selector', this.shadowRoot).fancyPlugin();

このように、jQueryセレクターとその結果、プラグインは問題なく機能します。

ただし、Alienコンポーネントは、ShadowDOMのないAlienとShadowDOM内のHostの両方で使用することを目的としていることに注意してください。 したがって、デフォルトでShadowDOMの存在を想定しないより統一されたソリューションが必要です。

ReactアプリケーションのMainSectionコンポーネントを分析すると、 documentRootプロパティが設定されていることがわかります。

 ... this.documentRoot = this.props.root? this.props.root: document; ...

したがって、渡されたrootプロパティをチェックし、存在する場合は、これをdocumentRootとして使用します。 それ以外の場合は、 documentにフォールバックします。

このプロパティを使用するツールチッププラグインの初期化は次のとおりです。

 $('[data-toggle="tooltip"]', this.documentRoot).tooltip({ container: this.props.root || 'body' });

ボーナスとして、この場合、ツールチップを挿入するためのコンテナを定義するために同じrootプロパティを使用します。

これで、Alienコンポーネントがrootプロパティを受け入れる準備ができたら、対応するFrankensteinラッパーのコンポーネントのレンダリングを更新します。

 // `appWrapper` is the root element within wrapper's Shadow DOM. ReactDOM.render(<MainApp root={ appWrapper } />, appWrapper);

以上です! コンポーネントは、Shadow DOMでも、グローバルDOMと同じように正常に機能します。

マルチラッパーシナリオのWebpack構成

いくつかのラッパーを使用する場合、Webpackの構成でエキサイティングな部分が発生します。 VueコンポーネントのCSSモジュールやReactのスタイル付きコンポーネントのようなバンドルされたスタイルには何の変更もありません。 ただし、グローバルスタイルは今少しひねりを加える必要があります。

style-loader (グローバルスタイルシートを正しいShadow DOMに挿入する責任がある)は、 insertオプションに一度に1つのセレクターしか必要としないため、柔軟性がないことを思い出してください。 つまり、Webpack以外のバンドラーを使用している場合は、 oneOfルールなどを使用してラッパーごとに1つのサブルールを持つようにWebpackの.cssルールを分割する必要があります。

例を使用して説明する方が常に簡単なので、今回はVueへの移行からのものについて話しましょう(ただし、Reactへの移行におけるものはほぼ同じです)。

 ... oneOf: [ { issuer: /Header/, use: [ { loader: 'style-loader', options: { insert: 'frankenstein-header-wrapper' } }, ... ] }, { issuer: /Listing/, use: [ { loader: 'style-loader', options: { insert: 'frankenstein-listing-wrapper' } }, ... ] }, ] ...

構成はすべての場合で同じであるため、 css-loaderを除外しました。 代わりにstyle-loaderについて話しましょう。 この構成では、スタイルシートを要求するファイルの名前(Webpackのissuerルール)に応じて、 <style>タグを*-header-*または*-listing-* *のいずれかに挿入します。 ただし、Alienコンポーネントのレンダリングに必要なグローバルスタイルシートは、次の2つの場所にインポートされる可能性があることを覚えておく必要があります。

  • エイリアンコンポーネント自体、
  • フランケンシュタインのラッパー。

そしてここで、エイリアンコンポーネントの名前と対応するラッパーが一致する場合、上記のラッパーの命名規則を理解する必要があります。 たとえば、 Header.vueというVueコンポーネントにインポートされたスタイルシートがある場合、 *-header-*ラッパーが修正されます。 同時に、代わりにラッパーにスタイルシートをインポートする場合、構成を変更せずにラッパーがHeader-wrapper.jsと呼ばれる場合、そのようなスタイルシートはまったく同じルールに従います。 Listing.vueコンポーネントとそれに対応するラッパーListing-wrapper.jsについても同じです。 この命名規則を使用して、バンドラーの構成を減らします。

すべてのコンポーネントが移行されたら、移行の最終ステップに進みます。

7.エイリアンに切り替えます

ある時点で、移行の最初のステップで特定したコンポーネントがすべてフランケンシュタインラッパーに置き換えられていることがわかります。 jQueryアプリケーションは実際には残されておらず、基本的に、Hostの手段を使用して結合されたAlienアプリケーションがあります。

たとえば、jQueryアプリケーションのindex.htmlのコンテンツ部分(両方のマイクロサービスの移行後)は、次のようになります。

 <section class="todoapp"> <frankenstein-header-wrapper></frankenstein-header-wrapper> <frankenstein-listing-wrapper></frankenstein-listing-wrapper> </section>

現時点では、jQueryアプリケーションを維持する意味はありません。代わりに、 Vueアプリケーションに切り替えて、すべてのラッパー、Shadow DOM、および派手なWebpack構成を忘れる必要があります。 これを行うために、私たちはエレガントなソリューションを持っています。

HTTPリクエストについて話しましょう。 ここではApacheの構成について説明しますが、これは実装の詳細にすぎません。Nginxなどで切り替えを行うのは、Apacheの場合と同じくらい簡単なはずです。

サーバーの/var/www/htmlフォルダーからサイトを提供していると想像してください。 この場合、 httpd.confまたはhttpd-vhost.confには、次のようなそのフォルダーを指すエントリが含まれている必要があります。

 DocumentRoot "/var/www/html"

FrankensteinをjQueryからReactに移行した後にアプリケーションを切り替えるには、 DocumentRootエントリを次のように更新するだけです。

 DocumentRoot "/var/www/html/react/build"

Alienアプリケーションをビルドし、サーバーを再起動すると、アプリケーションはAlienのフォルダーから直接提供されます。Reactアプリケーションはreact/フォルダーから提供されます。 ただし、Vueや、移行した他のフレームワークについても同じことが言えます。 このステップでエイリアンがホストになるため、いつでもホストとエイリアンを完全に独立させて機能させることが非常に重要であるのはこのためです。

これで、すべてのShadow DOM、Frankensteinラッパー、その他の移行関連のアーティファクトを含む、Alienのフォルダー周辺のすべてを安全に削除できます。 現時点では大まかな道のりでしたが、サイトを移行しました。 おめでとう!

結論

この記事では、確かにやや起伏の多い地形を通り抜けました。 ただし、jQueryアプリケーションを開始した後、それをVueとReactの両方に移行することができました。 その過程で、予期しない、それほど重要ではない問題を発見しました。スタイルを修正する必要があり、JavaScript機能を修正する必要があり、いくつかのバンドラー構成を導入する必要がありました。 しかし、それは私たちに実際のプロジェクトで何を期待するかについてのより良い概観を与えてくれました。 結局、移行の進行中に最終結果に懐疑的なすべての権利を持っていたとしても、jQueryアプリケーションからの残りのビットがない現代的なアプリケーションを手に入れました。

エイリアンへの切り替え後、フランケンシュタインは引退することができます。
エイリアンへの切り替え後、フランケンシュタインは引退することができます。 (大プレビュー)

フランケンシュタインの移行は特効薬ではなく、恐ろしいプロセスでもありません。 これは、多くのプロジェクトに適用できる定義済みのアルゴリズムであり、プロジェクトを予測可能な方法で新しく堅牢なものに変換するのに役立ちます。