解决Webpack和Storybook构建的组件库中React的多个版本

Resolving multiple versions of React in component library built with Webpack and Storybook

我正在尝试构建一个基于 MUI 并使用 Storybook 和 TypeScript 的 React 组件库。因为 Storybook(使用 create-react-app)基于 Webpack,并且因为我的组件库包含 SASS 无法使用 tsc 编译的文件,所以我使用 Webpack 来构建捆。然后我将组件库导入到另一个 React 应用程序中,它有自己的 React 版本。

为了对此进行测试,我构建了一个普通的 TypeScript create-react-app 演示应用程序,并从我托管的 Github 存储库中的特定分支导入我的库。当我尝试包含库中的组件时,TS 类型正确显示,但应用程序抛出 one of these errors,指向底层 MUI 库中挂钩的使用。在我看来,这极有可能是 React 版本竞争问题,而不是 hooks 问题,因为 A) MUI 有效,B) Storybook 中的组件有效。

这是我的组件库的基本结构。

我将与构建无关的应用程序依赖项(React、MUI、MUI 依赖项)放在 peerDependencies 中,这样它们就不会重复。

组件库

package.json

{
  "name": "my-component-library",
  "version": "0.1.0",
  "private": true,
  "license": "MIT",
  "main": "dist/index.js",
  "types": "dist/types/index.d.ts",
  "dependencies": {
    "react-scripts": "5.0.0",
    "web-vitals": "^1.0.1"
  },
  "scripts": {
    "build": "webpack"
    // ...
  },
  // ... Lotsa devDependencies
  "peerDependencies": {
    "@emotion/react": "^11.7.1",
    "@emotion/styled": "^11.6.0",
    "@mui/icons-material": "^5.4.1",
    "@mui/material": "^5.4.1",
    "classnames": "^2.3.1",
    "lodash": "^4.17.21",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-router-dom": "^6.2.1"
  }
}

webpack.config.js

const path = require("path");

module.exports = {
  entry: "./src/index.ts",
  devtool: "source-map",
  output: {
    filename: "index.js",
    path: path.resolve(__dirname, "dist"),
    library: "MyComponentLibrary",
    libraryTarget: "umd",
    clean: true,
  },
  mode: "development",
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        loader: "ts-loader",
        options: { configFile: "tsconfig.build.json" },
        exclude: /node_modules/,
      },
      {
        test: /\.s[ac]ss$/i,
        use: ["style-loader", "css-loader", "sass-loader"],
      },
    ],
  },
  resolve: {
    extensions: [".tsx", ".ts", ".js"]
  },
  optimization: {
    minimize: false,
  },
  target: "node",
};

tsconfig.build.json

{
  "compilerOptions": {
    "allowJs": true,
    "allowSyntheticDefaultImports": true,
    "baseUrl": "src",
    "declaration": true,
    "esModuleInterop": true,
    "emitDeclarationOnly": true,
    "forceConsistentCasingInFileNames": true,
    "isolatedModules": true,
    "jsx": "react-jsx",
    "lib": ["es6", "dom"],
    "module": "esnext",
    "moduleResolution": "node",
    "noEmit": false,
    "noFallthroughCasesInSwitch": true,
    "noImplicitAny": true,
    "noImplicitThis": true,
    "outDir": "dist/types",
    "resolveJsonModule": true,
    "skipLibCheck": true,
    "strict": true,
    "strictNullChecks": true,
    "sourceMap": true,
    "target": "es5"
  },
  "include": ["src"],
  "exclude": ["src/**/*.test.tsx", "src/**/*.stories.tsx", "src/__mocks__"]
}

当我运行npm run build的时候,webpack正确的把文件编译成dist/index.js,类型也放上去了(一堆无关的东西我想整理一下)进入 dist/types.

演示应用程序

然后,我的消费演示应用程序是一个基本的 CRA 应用程序,带有 TypeScript 模板。以下是包文件的主要组成部分:

package.json

{
  "name": "cld",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@types/jest": "^27.4.1",
    "@types/node": "^16.11.27",
    "@types/react": "^17.0.1",
    "@types/react-dom": "^17.0.1",
    "react": "^17.0.1",
    "react-dom": "^17.0.1",
    "react-scripts": "5.0.1",
    "typescript": "^4.6.3",
    "web-vitals": "^2.1.4",

    // Note the Github require
    "my-component-library": "github:my-private-handle/component-library#branch"
  },
}

在我的 App.tsx 文件中:

App.tsx

import React from "react";
import { Button } from "my-component-library";

function App() {
  return (
    <div className="App">
      <Button>Hello</Button>
    </div>
  );
}

export default App;

如果我将鼠标悬停在 Button 上,我会看到组件文档,如果我给它错误的 props,TS 会显示错误。但是如果我 运行 应用程序,由于不匹配的 React 版本(我认为),文件会抛出上述错误。

但是,React 和 React DOM 似乎只有一个版本——由消费应用程序定义的版本。

npm list react react-dom
cld@0.1.0 /Users/sasha/code/cld
├─┬ react-dom@17.0.2
│ └── react@17.0.2 deduped
├─┬ react-scripts@5.0.1
│ └── react@17.0.2 deduped
├── react@17.0.2
└─┬ my-component-library@0.1.0 (git+ssh://git@github.com/private-handle/component-library.git#09b5db39d8aa8a76f7c1fecacdc3afaecd57812e)
  ├─┬ @emotion/react@11.9.0
  │ └── react@17.0.2 deduped
  ├─┬ @emotion/styled@11.8.1
  │ └── react@17.0.2 deduped
  ├─┬ @mui/icons-material@5.6.1
  │ └── react@17.0.2 deduped
  ├─┬ @mui/material@5.6.1
  │ ├─┬ @mui/base@5.0.0-alpha.76
  │ │ ├── react-dom@17.0.2 deduped
  │ │ └── react@17.0.2 deduped
  │ ├─┬ @mui/system@5.6.1
  │ │ ├─┬ @mui/private-theming@5.6.1
  │ │ │ └── react@17.0.2 deduped
  │ │ ├─┬ @mui/styled-engine@5.6.1
  │ │ │ └── react@17.0.2 deduped
  │ │ └── react@17.0.2 deduped
  │ ├─┬ @mui/utils@5.6.1
  │ │ └── react@17.0.2 deduped
  │ ├── react-dom@17.0.2 deduped
  │ ├─┬ react-transition-group@4.4.2
  │ │ ├── react-dom@17.0.2 deduped
  │ │ └── react@17.0.2 deduped
  │ └── react@17.0.2 deduped
  ├── react-dom@17.0.2 deduped
  ├─┬ react-router-dom@6.3.0
  │ ├── react-dom@17.0.2 deduped
  │ ├─┬ react-router@6.3.0
  │ │ └── react@17.0.2 deduped
  │ └── react@17.0.2 deduped
  └── react@17.0.2 deduped

更新: 根据调试建议 here,我在 receiving/demo 应用程序中执行了此操作:

// Add this in node_modules/react-dom/index.js
window.React1 = require('react');

// Add this in your component file
require('react-dom');
window.React2 = require('react');
console.log(window.React1 === window.React2);

截至最新 运行,此 returns false,因为 window.React1 未定义 -- 并且本地 node_modules/react-dom/index.js 文件似乎不是尽管 require("react-dom") 命中。 require 的结果是 一个 ReactDOM 对象,所以我不确定它来自哪里。很奇怪,但可能是问题的根源。


更新: 我已经确认这是回购协议中的唯一问题。不基于 MUI 构建且不使用挂钩(直接或通过 MUI)的组件呈现得很好。从同一个库导出的实用程序和类型一样完美。只是任何使用 MUI 的组件或使用 React hooks 的另一个库都会抛出错误。这对我来说毫无意义,因为应用程序中只有一个 React 版本。

我在开发 React 组件库时遇到了完全相同的问题。

一直对我有用的唯一解决方案是使用相对 npm link 从示例文件夹到 webpack 文件夹,并在 webpack 项目上强制使用此特定版本。 重新阅读 this 部分的最后几行并尝试一下。请注意,对我来说只有 npm link 有效(yarn link 没有)
这是一个使用这种方法的真实项目,以及处理它的脚本 https://github.com/Eliav2/react-xarrows/blob/197326ccb032d5b5a3a7e18def596abac7327b05/examples/package.json#L34。 确保根据您的情况调整相对路径

(抱歉格式问题,从移动设备回答)

另一种选择是使用 monorepo,然后将安装一个 React 副本,您可以从示例项目中导入您的 Lib

问题归结为:Webpack 正在构建一个包 React、ReactDOM 和React Router。捆绑包中的 React 等版本与“接收”应用程序中的版本冲突,即使 npm list 没有显示多个版本。

为了解决这个问题,我在我的 webpack 配置中使用了 externals 功能:

{
  ...
  externals: {
    'react': 'react',
    'react-dom': 'react-dom',
    'react-router-dom': 'react-router-dom',
    'react-router': 'react-router'
  }
}

然后构建到 dist 文件夹,但构建中不包含 React 等,而是期望将其安装在接收应用程序中。该构建包已发布。目前,我只是从 Github 中提取它,但这应该可以发布到 NPM 或私有包存储库。

然后 consuming/receiving 应用程序可以导入 files/types 就好了,没有冲突。