create-react-app
を使用してカスタマイズされた3つのreact-app-rewired
sがあり、どちらも次のパッケージの同じバージョンを使用しています。
"react": "^16.12.0",
"react-app-rewired": "^2.1.5",
"react-dom": "^16.12.0",
"react-router-dom": "^5.1.2",
"react-scripts": "3.3.0",
アプリ1、「メイン」アプリケーションは他の2つのアプリ用の非常にシンプルなシェルです。public/index.html
は次のようになります。
<body>
<div class="main-app"></div>
<div class="sub-app-1"></div>
<div class="sub-app-2"></div>
<script src="sub-app-1/static/js/bundle.js" async></script>
<script src="sub-app-1/static/js/0.chunk.js" async></script>
<script src="sub-app-1/static/js/main.chunk.js" async></script>
<script src="sub-app-2/static/js/bundle.js" async></script>
<script src="sub-app-2/static/js/0.chunk.js" async></script>
<script src="sub-app-2/static/js/main.chunk.js" async></script>
</body>
これは十分に機能し、3つのアプリはすべて正しくレンダリングされます。要件が少し変更され、sub-app-2
などの<PictureFrame />
の唯一のコンポーネントをsub-app-1
に含める必要がある場合、メインプロジェクトに次のようにスタブコンポーネントを作成しました。
const NullPictureFrame = () => {
return <div></div>;
}
export const PictureFrame = (props: Props) => {
const forceUpdate = useForceUpdate();
useEventListener(window, "PictureFrameComponent.Initialized" as string, () => forceUpdate());
const PictureFrame = window.PictureFrameComponent || NullPictureFrame;
return <PictureFrame />
}
フックの詳細は重要ではないと思いますが、スタンドアローンで実行しても機能します。 sub-app-2
はこれに似た処理を行います
window.PictureFrameComponent = () => <PictureFrame />
理論的には機能するようですが、実際には次のエラーが発生します
The above error occurred in the <PictureFrame> component:
in PictureFrame (at src/index.tsx:17)
in Router (at src/index.tsx:16)
in Unknown (at PictureFrames/index.tsx:21)
in PictureFrame (at src/index.tsx:32) <--- in `main-app`
Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://reactjs.org/docs/error-boundaries.html to learn more about error boundaries. index.js:1
Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://reactjs.org/warnings/invalid-hook-call-warning.html for tips about how to debug and fix this problem.
main-app
とsub-app-2
は2つの異なるバージョンのreact
をロードしており、これがフックの使用時に問題を引き起こしています。
this github thread のアドバイスに基づいてwebpack設定を更新しようとしましたが、sub-app-2
がこのアプローチを使用してreact
への参照を見つけられないようです。私のmain-app/config-overrides.js
は次のようになります:
config.resolve.alias["react"] = path.resolve(__dirname, "./node_modules/react");
config.resolve.alias["react-dom"] = path.resolve(__dirname, "./node_modules/react-dom");
私のsub-app-1/config-overrides.js
は次のようになります:
config.externals = config.externals || {};
config.externals.react = {
root: "React",
commonjs2: "react",
commonjs: "react",
AMD: "react"
};
config.externals["react-dom"] = {
root: "ReactDOM",
commonjs2: "react-dom",
commonjs: "react-dom",
AMD: "react-dom"
};
その結果、エラーは発生しませんが、sub-app-2
のコードがreact
/react-dom
としてWebpackによって初期化されないこともありません。
編集1:明確化
Window.postMessage
などを使用することによるこの非常に疎結合です。const PictureFrame = window.PictureFrameComponent || NullPictureFrame;
の私の最初のアプローチはうまく機能しますなしこれまでチームが使用していたフックを使用して、上記の詳細な依存関係を非常に失うという考えを強化しましたmain-app
のexternals
の一部に依存するものを作成し、それらをそのようにロードする「明白な」方法があります。expose-loader
webpackプラグインを使用するexpose-loader
でmain-app
を使用すると、sub-app-1
とsub-app-2
の両方がmain-app
のライブラリを使用できるようになります
sub-app-1
およびsub-app-2
には、external
ライブラリに依存していることを示すconfig-overrides.js
があります
module.exports = function override(config, env) {
config.externals = config.externals || {};
config.externals.axios = "axios";
config.externals.react = "React";
config.externals["react-dom"] = "ReactDOM";
config.externals["react-router"] = "ReactRouterDOM";
return config;
}
main-app
は、これらのライブラリを提供する責任があります。これは、<script />
タグを使用してライブラリを埋め込むか、または expose-loader
を使用して実現できます。config-overrides.js
は次のようになります。
module.exports = function override(config, env) {
config.module.rules.unshift({
test: require.resolve("react"),
use: [
{
loader: "expose-loader",
options: "React"
}
]
});
config.module.rules.unshift({
test: require.resolve("react-dom"),
use: [
{
loader: "expose-loader",
options: "ReactDOM"
}
]
});
config.module.rules.unshift({
test: /\/react-router-dom\//, // require.resolve("react-router-dom"), did not work
use: [
{
loader: "expose-loader",
options: "ReactRouterDOM"
}
]
});
return config;
};
main-app
ページにアクセスすると、ウィンドウオブジェクトにReact
、ReactDOM
、ReactRouterDOM
が表示されます。アプリが順不同で読み込まれる可能性がありますが、main-app
が最初に読み込まれることを確認する必要があります。幸い、ややまっすぐな方法があります。 public/index.html
で、参照されているsub-apps
を削除します
<body>
<div class="main-app"></div>
<div class="sub-app-1"></div>
<div class="sub-app-2"></div>
- <script src="sub-app-1/static/js/bundle.js" async></script>
- <script src="sub-app-1/static/js/0.chunk.js" async></script>
- <script src="sub-app-1/static/js/main.chunk.js" async></script>
- <script src="sub-app-2/static/js/bundle.js" async></script>
- <script src="sub-app-2/static/js/0.chunk.js" async></script>
- <script src="sub-app-2/static/js/main.chunk.js" async></script>
</body>
index.tsx
では、問題のスクリプトを動的にロードして、それらを待つことができます
// Will dynamically add the requested scripts to the page
function load(url: string) {
return new Promise(function(resolve, reject) {
var script = document.createElement("script");
script.type = "text/javascript";
script.async = true;
script.src = url;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
// Load Sub App 1
Promise.all([
load(`${process.env.SUB_APP_1_URL}/static/js/bundle.js`),
load(`${process.env.SUB_APP_1_URL}/static/js/0.chunk.js`),
load(`${process.env.SUB_APP_1_URL}/static/js/main.chunk.js`)
]).then(() => {
console.log("Sub App 1 has loaded");
window.dispatchEvent(new Event("SubAppOne.Initialized"));
const SubAppOne = window.SubAppOne;
ReactDOM.render(<SubAppOne />, document.querySelector(".sub-app-1"))
});
// Load Sub App 2
Promise.all([
load(`${process.env.SUB_APP_2_URL}/static/js/bundle.js`),
load(`${process.env.SUB_APP_2_URL}/static/js/0.chunk.js`),
load(`${process.env.SUB_APP_2_URL}/static/js/main.chunk.js`)
]).then(() => {
console.log("Sub App 2 has loaded");
window.dispatchEvent(new Event("PictureFrameComponent.Initialized")); // Kicks off the rerender of the Stub Component
window.dispatchEvent(new Event("SubAppTwo.Initialized"));
const SubAppTwo = window.SubAppTwo;
ReactDOM.render(<SubAppTwo />, document.querySelector(".sub-app-2"))
});
これでmain-app
はReact
のようなすべての共有依存関係を持ち、依存関係の準備ができると2つのsub-apps
をロードします。 PictureFrameComponent
がフックで期待どおりに機能するようになりました。 sub-app-1
の前にmain-app
をロードできますが、main-app
の重要性と、PictureFrameComponent
によって提供される機能のマイナーな追加を考えると、これは、一部のプロジェクトではまずまずの解決策になる可能性があります。