【问题标题】:Using one webpack project inside another with react hooks causes an error在另一个带有反应钩子的 webpack 项目中使用一个 webpack 项目会导致错误
【发布时间】:2020-05-04 05:37:37
【问题描述】:

使用react-app-rewired定制的三个create-react-apps,都使用相同版本的以下包:

"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,“主”应用程序是其他两个应用程序的一个非常简单的外壳,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 中的唯一一个组件,例如 &lt;PictureFrame /&gt; 需要包含在 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 的两个不同版本,这在使用挂钩时会导致问题。

我已尝试根据 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 的代码没有被 webpack 初始化为 react/react-dom 找不到


编辑 1:澄清

  • 有问题的 3 个应用是 3 个完全独立的 (git) 项目,它们都使用相同的依赖项。
  • 项目之间唯一允许的“依赖关系”是这种通过使用全局变量、事件、Window.postMessage 或类似的非常松散的耦合。
  • 我最初使用 const PictureFrame = window.PictureFrameComponent || NullPictureFrame; 的方法效果很好不使用团队一直在使用的钩子,这强化了上面详述的非常丢失依赖项的想法
  • 有一种“明显”的方式可以让“react”、“react-dom”和任何依赖它们的东西成为main-appexternals 的一部分并以这种方式加载它们

【问题讨论】:

    标签: reactjs webpack react-hooks create-react-app


    【解决方案1】:

    正如React.js documentation所引用的,


    反应钩子:

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

    使用钩子时要记住的规则

    1. 仅在顶层调用 Hooks

    2. 仅来自 React 函数的调用挂钩

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

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


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

    public/index.html

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

    创建另一个组件Main App并在其中包含sub-app-1sub-app-2

    由于您使用的是"react-dom": "^16.12.0""react-router-dom": "^5.1.2", 设置路线会更容易。


    不能说这会解决您的问题,但包含在内是一个很好的做法。

    一切顺利。

    【讨论】:

      【解决方案2】:

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

      你可以阅读我写的一篇关于 Share React Native components to multiple projects 的文章,我建议了 3 种在项目之间共享代码的方法。我将重点介绍 Webpack/Babel 方式,您可以使用 module-resolver 包来配置目录别名。您可以使用.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 和任何包,因为每个项目将是一个独立的代码库,具有自己的依赖项,但具有相同的共享组件/功能。

      【讨论】:

      【解决方案3】:

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

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

      npm link ../main_app/node_modules/react
      

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

      【讨论】:

        【解决方案4】:

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

        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 负责提供这些库。这可以通过使用&lt;script /&gt; 标签嵌入库或使用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 提供的功能的少量添加,这可能是一个一些项目的解决方案。

        【讨论】:

          猜你喜欢
          • 2021-11-29
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2014-02-08
          • 1970-01-01
          • 2020-02-02
          • 2018-11-30
          • 1970-01-01
          相关资源
          最近更新 更多