在另一个带有反应钩子的 webpack 项目中使用一个 webpack 项目会导致错误



有三个create-react-app使用react-app-rewired自定义,都使用以下软件包的相同版本:

"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,"主"应用程序是其他两个应用程序的非常简单的 shell,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>

这足够好用,所有三个应用程序都正确呈现。现在要求略有变化,例如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-appsub-app-2正在加载两个不同版本的react,这在使用钩子时会导致问题。

我试图根据这个 github 线程中的建议更新我的 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的代码没有被 webpack 初始化为找不到react/react-dom


编辑1:澄清

  • 有问题的 3 个应用程序是 3 个完整的独立 (Git) 项目,只是都使用相同的依赖项。
  • 项目之间唯一允许的"依赖关系"是使用全局、事件、Window.postMessage或类似内容的这种非常松散的耦合。
  • 我最初的const PictureFrame = window.PictureFrameComponent || NullPictureFrame;方法在不使用钩子的情况下工作得很好,团队一直在使用钩子,这强化了上面详述的非常失去依赖的想法
  • 有一种"明显"的方式可以使"react","react-dom"以及任何依赖于它们的东西成为main-appexternals的一部分,并以这种方式加载它们

我看到您已经采取了一种将多个项目放在一个目录/存储库/代码库下的方法,我认为这不是最好的方法。您应该拆分并独立拥有所有项目,这是一个库子项目,您可以在其中拥有所有共享代码/组件,然后使每个项目都可以访问它需要的内容。

你可以阅读我写的一篇关于将 React Native 组件共享到多个项目的文章,我建议在项目之间共享代码的 3 种方法。我将强调 Webpack/Babel 方式,您可以利用模块解析器包来配置目录别名。可以使用.babelrcmodule-resolver来配置外部依赖项,如下所示:

{
"presets": ["@babel/preset-react"]
"plugins": [
"@babel/plugin-transform-runtime",
[
"module-resolver",
{
"root": ["./"],
"extensions": [".js", ".jsx", ".ts", ".tsx"],
"stripExtensions": [".js", ".jsx", ".ts", ".tsx"],
"alias": {
"@components": "./components",
"@screens": "./screens",
"@utils": "./utils",
"@data": "./data",
"@assets": "./assets",
"@app": "./",
"@myprojectlib": "../MyProject-Library",
"@myprojecti18n": "../MyProject-Library/i18n/rn",
"@myprojectbackend": "../MyProject-Backend/firebase/functions",
}
}
]
]
}

之后,您可以使用以下内容:

import { PictureFrame } from '@myprojectlib/components/PictureFrame';

您将在配置为有权访问"@myprojectlib": "../MyProject-Library"的每个项目中使用它。

通过这种方式,您可以在项目中拥有多个版本的 React 和任何包,因为每个项目都将是一个独立的代码库,具有自己的依赖项,但具有相同的共享组件/函数。

正如 React .js 文档中引用的,


反应钩子:

钩子本质上是为了访问无类组件中的状态和其他反应功能而构建的。

使用钩子时要记住的规则

  1. 仅在顶层调用挂钩

  2. 仅从 React 函数调用钩子

从您发布的详细信息来看,我相信您已经考虑了上述几点。

对于根本原因分析,您可能需要在代码中设置一些调试语句,或者在控制台上设置日志消息,以调查组件生命周期。


此外,如果您可以按如下方式重新构造/重新排列应用程序,情况会好得多:

public/index.html

<body>
<div id="root" class="main-app"></div>
<script>
<!-- scrpits can be kept here -->
</script>
</body>

创建另一个组件应用程序,并在其中包含子应用程序1 和子应用程序2

既然你使用的是"react-dom": "^16.12.0""react-router-dom": "^5.1.2", 设置路线会更容易。


不能真的说这会解决你的案子,但这是一个很好的做法。

万事如意。

我在项目中遇到了同样的问题,解决方案很简单。

您可以转到所有子项目并运行 Bellow 命令。

npm link ../main_app/node_modules/react

裁判: https://reactjs.org/warnings/invalid-hook-call-warning.html#duplicate-react

部分解决方案:使用expose-loaderwebpack 插件

使用main-app中的expose-loader允许sub-app-1sub-app-2使用main-app中的库

sub-app-1sub-app-2有一个config-overrides.js来表明他们依赖于external

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页面时,我可以看到窗口对象上的ReactReactDOMReactRouterDOM。现在我的应用程序可能会乱序加载,但现在我们需要确保首先加载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,并将在依赖项准备就绪时加载这两个sub-appsPictureFrameComponent现在与钩子一样工作!如果装载可以按任何顺序发生,即sub-app-1可以在main-app之前加载,但是考虑到main-app的重要性以及PictureFrameComponent提供的功能的少量添加,对于某些项目来说,这可能是一个过得去的解决方案。

最新更新