初めまして、2021年度新入社員の関と申します。
普段の開発ではAWSリソースをTerraform環境で管理できる様にする業務やチームで使用するSlackボットの作成などを行っています。
今回は、React内にSvelteを埋め込んでマイクロフロントエンドもどきなことをやってみたため、紹介します。
この記事は、Reactの実装方法やSvelteについて基礎的な知識があり、webpackなどもある程度理解していることを前提としています。
また、環境としてnodeやnpmコマンドなどが動かせる環境があることを前提とします。
なぜやろうとしたのか
自分はフロントエンドに興味があり、数年前から聞くようになったマイクロフロントエンドについて興味がありました。
しかし、実際にどのように行えば良いのかなどは試したことがありませんでした。
そのため、このブログを良い機会と考え、試してみようと思いました。
マイクロフロントエンドとは何か
ブログ冒頭からマイクロフロントエンドと何度も繰り返していますが、そもそもマイクロフロントエンドとは何かについて少し話します。
ここの部分の話では以下のような簡単なニュースサイトを例に話します
バックエンド開発のマイクロサービス化
この話をするにはまずバックエンド開発について話す必要があります。
昔のバックエンドは一つのサーバーのみで動かしてすべてのAPIをそこで管理する様にしていました。しかし、この方法では処理が集中してしまうなどの問題が生まれました。(そもそもフロントとバックを分けないモノリシックな設計については割愛します。)
この問題を解決するために、APIの種類ごとに分けてサーバーを立てるマイクロサービスという考えができました。そして、バックエンドでは機能ごとに別のサーバーを立ててアクセスする方法が考えられました。
たとえば冒頭のニュースサイトならばニュース情報取ってくるのはこっちのサーバー(API)、検索はこっちのサーバー(API)、ログイン部分はこっちのサーバー(API)などで分割していることは多くの会社や開発で行っていると思います。
フロントエンドの昔と今
昔はフロントエンドでそこまで膨大な処理を書く必要はありませんでした。せいぜいhtmlを書いて、cssでデザイン、JavaScriptで動きをつけるくらいしかやらなかったためです。
しかし、Ajaxの登場などにより、フロントエンド側でもいろいろな処理が行われるようになり、さまざまなことがフロントエンドでできるようになりました。
そして、レスポンスの速さなどの理由で、多くのことをフロントエンドで行うことが増え、フロントエンド処理を担当する専門部隊が散見されるようになりました。
そんなこんなでフロントエンドで処理をいろいろと行っていると、フロントエンド層の肥大化が起こり管理が難しくなることが多くなってきました。
それにより、以下のような問題が生まれてしまいました。
- フロントエンドの技術が偏ってしまうため、フロントエンド領域の変化への追従が困難である。
- フロントエンドがサービスの成長のボトルネックとなる。
- すべてのページを一気通貫で作成しているため、ある特定の技術に精通する人材を採用する必要がある。
- 機能追加の際に全体のバランスを考えた設計が必要になる。
- テストや調査が困難である。
- 同じような機能のコンポーネント(処理)が重複する。
このことから、バックエンドで行われるマイクロシステムの考えを、フロントエンドにも適用しようではないか、ということで作られたのがこの概念なわけです。
マイクロフロントエンドとその利点
マイクロフロントエンドでは、バックエンドで行っていたように機能を複数に分けます。例のニュースサイトで言えば、ニュース情報、検索、ログイン部分という感じで分けます。
以下の色違いで囲った部分ごとで別の開発をしていき、最後に合体させるようにするわけです。
つまり、システムとしては以下のように分割されています。
このように機能ごとに分けることで、以下のことを解決することができます。
- 技術スタックが偏ってしまう問題の解決。
- コンポーネント単位で機能を作成することで、コンポーネント単位で新しい技術スタックを用いた開発が可能になる。
- 依存関係が複雑になる問題の解決。
- ある機能がほかの機能に影響しないため、依存関係が複雑になるために、設計やテストが楽になる。
- 新たな機能追加が大変な問題の解決。
- 技術スタックの選択が容易や依存性が低いことにより、新たな機能追加が比較的容易に可能である。
- 機能を別のページで共有できない問題の解決。
- 検索機能などをある一つのサイトだけでなく別のサイトで共有して用いることが可能になり、機能共有が可能。工数の削減につながる。
まとめると、膨大になりつつあるフロントエンド開発を細分化して管理しやすくするための考えがこのマイクロフロントエンドという訳です。
詳しくは、以下のページなどを読むと非常に理解が深まると思います。
今回は、マイクロフロントエンドを行うためのさわりもさわり、別のライブラリのコンポーネントを別のライブラリで読み込んでみるということを行っていきます。
共通コンポーネントとしてのSvelte
なぜSvelteを用いるのかを説明します。
Svelteはいわゆるコンポーネントで開発を行うwebフレームワークです。
記述方法などがReactやVueなどと似ており、それらと比較されることが多々あります。
しかし、SvelteはReactやVueとは似て非なると言えます。というのも、Svelteは仮想DOMを使っていないためです。
Svelteは仮想DOMを使わず、ビルド時に状態変化後のすべての可能性を網羅する形でpureJSの生成を行うことでリアクティブな処理を実現しています(参考)。
今回重要なのはこのPureJSに変換され仮想DOMを用いることがないという点です。
pureJSになるということはJavaScriptが動く環境であれば動かすことが可能であるということになります。
つまり、ReactやVueの環境でも動かすことが可能ということです。
この性質からさまざまなアプリで共通で動かせるコンポーネントを作成する上では選択肢としてピッタリということができる訳です。
この件は以下のサイトなどで詳しく述べられています。
Svelteコンポーネント、なぜReact / Vue 上で動いてるの?
実際に共通コンポーネントとして読み込んでみる
今回はSvelteのコンポーネントを共通コンポーネントとして用意して、Reactに埋め込むということを行います。
SvelteをReactのアプリケーション上で動かす方法は以下の二つあります。
- SvelteのインスタンスをReactのコンポーネントに変換して行う方法。
- webpack5のModule Federationを使用する方法。
このブログではこの二つの方法での実装を説明しつつ、最後にこの二つの方法を比較します。
SvelteのインスタンスをReactのコンポーネントに変換して行う方法
reactの環境を作成する
まず、簡単なReactの実行環境を作成します。
任意の場所に以下のような構造のディレクトリを作成してください。(例ではreact-svelteディレクトリを作成しています)
ファイル内はとりあえず空で大丈夫です
1 2 3 4 5 6 7 8 9 |
react-svelte ├── index.js ├── public │ └── index.html ├── src │ ├── App.jsx │ └── svelte │ └── test.svelte └── webpack.config.js |
このディレクトリ内で以下のコマンドを実行してください
1 |
npm init -y |
package.jsonが生成されます。
次にwebpackとbabelをインストールしていきます。
1 2 |
npm install -D webpack webpack-cli webpack-dev-server html-webpack-plugin npm install -D @babel/core @babel/runtime @babel/plugin-transform-runtime @babel/preset-env babel-loader |
Reactをインストールします
1 2 |
npm install react react-dom npm install -D @babel/preset-react |
ここまでできたら、webpack.config.jsを記述します。以下のように記述してください
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
const path = require("path"); const HtmlWebpackPlugin = require("html-webpack-plugin"); module.exports = { mode: "development", entry: path.resolve(__dirname, "index.js"), output: { path: path.resolve(__dirname, "dist"), filename: "app.js", }, resolve: { modules: [path.resolve(__dirname, "node_modules")], extensions: [".js", ".jsx"], }, module: { rules: [ { test: [/\\.js$/, /\\.jsx$/], use: [ { loader: "babel-loader", options: { presets: ["@babel/preset-env", "@babel/preset-react"], }, }, ], }, ], }, plugins: [ new HtmlWebpackPlugin({ template: path.resolve(__dirname, "public/index.html"), }), ], devServer: { static: { port: "8001", directory: path.join(__dirname, "dist"), }, }, };<code class="language-jsx"> |
index.htmlとApp.jsxの作成
次にindex.htmlとApp.jsxを作成します。
以下の様に記述してください
index.html
1 2 3 4 5 6 7 8 9 10 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <title>test</title> </head> <body> <div id="root"></div> </body> </html> |
App.jsx
1 2 3 4 |
import React from "react"; const App = () => <>hello world</>; export default App; |
最後に、以下のコマンドを入力します
1 |
npx webpack serve --config webpack.config.js |
localhost:8001にアクセスするとHello Worldと表示されるはずです。
Svelte-loaderとSvelte-adapterのインストールと設定
次にSvelte-loaderとSvelte-adapterをインストールしていきます。
Svelte-loaderはwebpackを使ってSvelteを動かす際に必要なもので、svelte-adapterはReactの中にSvelteを埋め込むクラスを提供してくれるライブラリです。
以下のコマンドでインストールします。
1 |
npm install svelte-loader svelte-adapter |
その後webpack.config.jsの中にSvelteのコードがロードされるようにrulesを追加します
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// ...省略 rules: [ { test: /\\.svelte$/, use: { loader: "svelte-loader", options: { emitCss: true, }, }, }, // ...省略 ] // ...省略 |
test.svelteファイルの中身は以下の様に記述してください
1 2 3 4 5 |
<script> export let name = ''; </script> <input bind:value={name}> <h1>{name}</h1> |
App.jsの書き換え
次にApp.jsの内容を書き換えます
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import React from "react"; import test from "./svelte/test.svelte"; import toReact from "svelte-adapter/react"; const App = () => { const SvelteInReact = toReact(test, {}, "div"); return ( <> hello world <SvelteInReact name="ここはsvelte部分よ"/> </> ); }; export default App; |
コードの説明をするとtoReactというものがsvelte-adapterでSvelteをReactで使う様にするために必要になるものです。
1 |
import toReact from "svelte-adapter/react"; |
そして、このアダプターは以下の様に引数を指定します。
1 |
toReact({svelteのコンポーネント要素}, {cssの記述}, {コンポーネントの最上位のHTML要素を指定する}); |
今回はtest.svelteのtestコンポーネントを使い、cssはなく、囲う要素はdivなので以下の様に記述しています
1 |
const SvelteInReact = toReact(test, {}, "div"); |
そして作成したコンポーネントをreactコンポーネントとしてJSX上に記述します。また、stateに「ここはSvelte部分よ」というテキストを渡しています。
1 2 3 |
... <SvelteInReact name="ここはSvelte部分よ"/> ... |
サーバーを立ち上げてみる
さて、ここまできたら、SvelteがReact内で動く様になるはずです。
以下のコマンドでサーバーを立ち上げます
1 |
npx webpack serve --config webpack.config.js |
サーバーを立ち上げ、localhost:8001にアクセスするとhello worldの下にテキストボックスとテキストが表示されるはずです。
さらに、テキストボックスの中の値を変更すると下のテキストも変更されるはずです。
このテキストボックスと変更されているテキストはSvelteのコードを元に作られたものになっています。
React内にSvelteを埋め込むことができました。
(余談)どうやって表示しているのか?
これに関しては以下のYoutubeや先述したブログで説明しています。
How to use Svelte component inside a React app?
Svelteコンポーネント、なぜReact / Vue 上で動いてるの?
また、svelte-adapterのソースコードも参考になると思います。
https://github.com/pngwn/svelte-adapter/blob/master/react.js
Svelteは前述したようにコンパイル後はpureJSになるのでそのクラス要素をインスタンス化して、その後、propsや値の変化を受け取るためのリスナーを登録しています。
webpack5のModule Federationを使用する方法
次に、Module Federationというwebpack5で導入された別サーバーにあるwebコンポーネントを読み込んで仕様できる仕組みを使用した方法を紹介します。
今回はsvelteで行いますが、この方法を使うとreactでもvueでもweb componentに変換することで別のライブラリで読み込める様になります。
さて、早速やっていきたいと思います。
今回はReactとSvelte別々にサーバーを立てていきます。
Reactの環境を作る
こちらは前述したReactの環境を作成すると全く同じ方法で構築してください。
なお、最後の方で少し設定は変更します。
Svelteの環境を作る – ファイル構造と初期化
次にSvelteの環境を作っていきます。svelte-cliを使うとバンドルツールがrollupで環境が作られますが、今回はwebpack5で環境の構築を行っていきます。
任意の場所に以下の様な構造のディレクトリを作成してください。
ファイル内は今は空で問題ないです
1 2 3 4 5 6 7 |
svelte ├── public │ └── index.html ├── src │ ├── index.js │ └── test.svelte └── webpack.config.js |
まずはpackage.jsonファイルを作成します。
1 |
npm init -y |
Svelteの環境を作る – パッケージのインストール
次にパッケージをインストールします。
まずwebpackをインストールしましょう
1 |
npm install -D webpack webpack-cli webpack-dev-server html-webpack-plugin serve |
次にbabelのインストールをします。
1 |
npm install -D babel-loader @babel/core @babel/preset-env |
最後にSvelteとsvelte-loaderをインストールします。
1 |
npm install svelte-loader svelte |
Svelteの環境を作る – index.htmlとtest.svelteファイルの作成
次にindex.htmlとtest.svelteファイルを作成していきます
index.htmlは以下の様に記述してください
1 2 3 4 5 |
<!DOCTYPE html> <meta charset=UTF-8> <title>Document</title> <link rel=stylesheet href=./main.css> <script src=./main.js defer></script> |
次にtest.svelteファイルを作成します。
1 2 3 4 5 6 |
<svelte:options tag={null} /> <script> export let name = ''; </script> <input bind:value={name}> <h1>{name}</h1> |
次にindex.jsファイルを作成します。こちらもsrcファイル内で作成してください
1 2 3 |
import test from "./test.svelte"; customElements.define('test-1', test) |
さて、Svelteファイルを記述したところで記述内容が違うことに気がついた人もいると思います。
具体的には以下の点が違っています
- <svelte:options tag={null} />の追加
- let nameにexportが付いている
これはSvelteをweb コンポーネントに変換するために追加している処理になっています。
今回のModule Federationではwebコンポーネントに変更して処理を行う必要があるためです。
また、index.js内のcustomElements.defineもtestというコンポーネントをtest-1という名前でwebコンポーネントとして出すことを指定しています。
ここまできたらwebpack.config.jsを記述していきます。
以下の様に記述してください
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin"); module.exports = { entry: './src/index.js', mode: process.env.NODE_ENV || "development", module: { rules: [ { test: /\\.svelte$/, use: { loader: "svelte-loader", options: { emitCss: true, compilerOptions:{ customElement: true } }, }, }, ] }, output: { path: `${__dirname}/dist`, filename: "main.js" }, resolve: { extensions: [".mjs", ".js", ".svelte"], }, plugins: [ new ModuleFederationPlugin({ name: "hello_world", filename: "remoteEntry.js", exposes: { "./Hello": "./src/index.js", }, }), ], devServer: { port: "8000", } }; |
さて、注目するべきなのは二点あります。
一点目はsvelte loaderのoption部分でcompilerOptionsの指定をしていることです。
1 2 3 |
compilerOptions:{ customElement: true } |
このオプションはsvelteファイルをwebコンポーネントに変換して使用する際にそれを指定する設定になっています。これがないとwebコンポーネントに変更できないエラーが出ます。
二点目はModuleFederationPluginというものが使われている部分です。
1 2 3 4 5 6 7 8 9 10 |
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin"); ... new ModuleFederationPlugin({ name: "hello_world", filename: "remoteEntry.js", exposes: { "./Hello": "./src/index.js", }, }), ... |
この部分が今回の最も重要な部分でModule Federationを使うために必要なものとなります。
設定項目は以下の様になっています
1 2 3 4 5 6 7 |
new ModuleFederationPlugin({ name: "hello_world", //このモジュールにアクセスするための名前 filename: "remoteEntry.js", //出力されるentryのファイル名 exposes: { "./Hello": "./src/index.js", //"expose後に参照する為に設定するファイル名": "exposeされるファイル名" }, }), |
詳しくは公式ドキュメントを参照してください
ここまできたらサーバーを立ち上げてhttp://localhost:8080/remoteEntry.jsにアクセスしてみてください。
1 |
npx webpack serve --config webpack.config.js |
以下の様な画面が表示されるはずです
Reactのwebpack.config.jsを書き換える
先ほど、出力したwebコンポーネントを使用するためにReact側のwebpack.config.jsも書き換えが必要です。
具体的には以下の様にModuleFederationPluginをplugins内で作成して、先ほど作ったweb componentを読み込みます。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin"); ... plugins: [ new ModuleFederationPlugin({ name: "main_contents", filename: "remoteEntry.js", remotes: { helloWorld_app: "hello_world@<http://localhost:8080/remoteEntry.js>", // {読み込み時のディレクトリ名}: "{svelteのwebpack内で指定したnameの値}@{svelteの方でexposeしたjsファイルへの絶対パス}" } }), ] ... |
React側でコンポーネントを読み込む
さてここまで設定したら、ReactのApp.jsファイルを以下の様に変更してください
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import 'helloWorld_app/Hello'; function App() { return ( <div className="App"> <header className="App-header"> <test-1 name="ここはsvelte部分よ"/> この文はReactだよ </header> </div> ); } export default App; |
また、index.jsとは別にboot.jsファイルをルートディレクトリに作成しindex.jsの内容を全て移動してください
ファイル構造は以下の様になると思います
1 2 3 4 5 6 7 8 9 10 11 12 |
react-svelte ├── boot.js ←新しく追加 ├── index.js ├── package-lock.json ├── package.json ├── public │ └── index.html ├── src │ ├── App.jsx │ └── svelte │ └── test.svelte └── webpack.config.js |
そしてindex.jsは以下の様に修正してください
1 |
import ('./boot.js') |
この状態でSvelteのサーバーを立ち上げ、Reactのサーバーを立ち上げてください。どちらのサーバーも以下のコマンドで立ち上げることができます。
1 |
npx webpack serve --config webpack.config.js |
localhost:8001にアクセスすると以下の様にsvelteのコンポーネントが読み込めているはずです。
問題点
この手法の問題点としてSvelteをwebコンポーネントに変換したものはSvelteとは少し挙動が違う点が挙げられます。
詳しくは以下のzennにまとまっていますが、Svelteの機能を完全にweb componentsに変換することはできない様です。
Svelte で Web Components を開発するときの Tips (2021年7月時点)
二つの手法の比較
今回はSvelteを共通コンポーネントとして使用するための二つの手法について紹介しました。
最後にて二つの手法のメリットとデメリットを比較します。
useRef使ってReactに埋め込む方法
メリット
- いちいち、使うコンポーネントの面倒な設定は必要ない
- コンパイルしてpurejs化したSvelteを使う。そのため、web component化はしないので、Svelteの機能はすべて使える(Reactとvue側で実装すれば)
デメリット
- webpackではsrcファイル内でリソースを完結させる必要がある
- 設定でどうにかなる(How to import an external file from project root with webpack?)
- ただおそらくあまり良くはない。基本的にはソースコードはsrcで完結させることが普通な気がする
webpack使う方法
メリット
- React内やvue内で使うためのコードを用意する必要はない
- 完全に別のサーバーとしてコンポーネントを用意し読み込ませることが可能
デメリット
- web component化するための問題がある場合がある。詳しくは参考サイトのSvelteでweb componentを参照
- webpack5の機能なのでrollupやviteなどでSvelteを使いたい場合は使えない
- ※ ほかのバンドルツールにもあるものはある(ただあくまで有志の人が作っている)
- rollup(module-federation/rollup-federation)
- esbuild(esbuild-module-federation)
- vite(vite-plugin-federation)
- ※ ほかのバンドルツールにもあるものはある(ただあくまで有志の人が作っている)
個人的にはuseRefの方法はいろいろと処理を追加する上で大変な気がするのと、共通コンポーネントとして使用するには別のファイルを参照するようにwebpackの内容を変更するというのがあまり良くないと思うため、webpackの方法の方が共通コンポーネントとして作成する場合は良いのかなと思いました。
まとめ
今回はSvelteで共通コンポーネントを作成するために使用できるかもしれない手法を二つ紹介しました。
今回やったことはさわりもさわりなので、今後はこれらの手法を使用してマイクロフロントエンドな設計で何か作成するなどやっていきたいと思います。
参考文献
マイクロフロントエンドについて
Svelteについて
Svelteコンポーネント、なぜReact / Vue 上で動いてるの?
SvelteのインスタンスをReactのコンポーネントに変換して行う方法
How to use Svelte component inside a React app?
Next.js + SvelteによるnoteのフロントエンドApp分割
webpack5のModule Federationを使用する方法
Webpack5 Module Federation で始めるマイクロフロントエンド