网站搜索

typescript - 即使在 webpack 配置中添加了 output.library,从库中导入 react 组件仍然得到空对象


我正在编写一个示例的 TypeScript React 组件库,文件夹结构如下,只导出一个按钮组件。在运行 npm run build 时,只生成了一个 js 包。该包中包含了 React 库的代码(尚未在 webpack 配置中使用 externals 属性),但不包含导出的按钮组件及按钮标签(在包文件中搜索关键词 button 时,没有结果)。我还添加了包/构建文件的代码。

不确定我哪里出错了。

在导出简单的 JavaScript 函数时,webpack 包不会出现此问题,即生成的包中可以找到导出的 JavaScript 函数,但无法找到导出的 React 组件。


更新:

在我的 webpack 配置中添加 output.library.name="template-react-component-library" 和 output.library.type="umd" 之后,我能够在 webpack 打包文件中看到按钮标签(导出的组件),但是从 template-react-component-library 导入同一个按钮组件时,我得到的是一个空对象。此外,通过相同的 webpack 配置从库中导出普通的 JavaScript 函数时,我通过从库中导入该函数能得到实际的 JavaScript 函数,而不是空对象,而且这甚至不需要添加任何 output.library 配置。

现在问题是如何解决从库导入组件时得到空对象,以及为什么 output.library 在暴露 JavaScript 函数和 React 组件时表现不同。


仓库链接: https://github.com/dhiren-eng/template-react-component-library

文件夹结构:

webpack.config.js :

const path = require("path")
module.exports = {
entry: "./src/index.ts",
output: {
    filename: "main.js",
    path: path.resolve(__dirname, 'build'),
    clean: true
},
module: {
    rules: [
        { test: /\.(jsx|js|tsx|ts)$/, exclude: /node_modules/, use: {loader: "babel-loader", options: {presets: ["@babel/preset-env","@babel/preset-react"]}} },
        { test: /\.(tsx|ts)$/, exclude: /node_modules/, use: ["ts-loader"] }
    ]
},
resolve: {
    extensions: ['.ts', '.tsx', '.js', '.jsx']
}
}

tsconfig.json :

{
compilerOptions: {
"jsx": "react",
"module": "ESNext",
"declaration": true,
"declarationDir": "types",
"sourceMap": true,
"outDir": "build",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true
}}

已安装以下软件包。package.json :

{
  "name": "template-react-component-library",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
     "test": "echo \"Error: no test specified\" && exit 1",
     "build": "webpack --config webpack.config.js"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
  "@babel/cli": "^7.21.5",
  "@babel/core": "^7.21.8",
  "@babel/preset-env": "^7.21.5",
  "@babel/preset-react": "^7.18.6",
  "@types/react": "^18.0.27",
  "babel-loader": "^9.1.2",
  "react": "^18.2.0",
  "ts-loader": "^9.4.2",
  "typescript": "^4.9.5",
  "webpack": "^5.76.2",
  "webpack-cli": "^5.0.1"
}

src/components/Button/Button.tsx :

import React from "react";

export interface ButtonProps { 
  label: string;
}

const Button = (props: ButtonProps) => {
  return <button>{props.label}</button>;
};

export default Button;

src/components/Button/index.ts :

export {default} from './Button'

src/components/index.ts :

export {default as Button} from './Button'

src/index.ts :

export {Button} from './components'

生成的构建文件,main.js :

    /*! For license information please see main.js.LICENSE.txt */
(()=>{"use strict";var t={408:(t,e)=>{Symbol.for("react.element"),Symbol.for("react.portal"),Symbol.for("react.fragment"),Symbol.for("react.strict_mode"),Symbol.for("react.profiler"),Symbol.for("react.provider"),Symbol.for("react.context"),Symbol.for("react.forward_ref"),Symbol.for("react.suspense"),Symbol.for("react.memo"),Symbol.for("react.lazy"),Symbol.iterator;var o={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},r=Object.assign,a={};function n(t,e,r){this.props=t,this.context=e,this.refs=a,this.updater=r||o}function c(){}function p(t,e,r){this.props=t,this.context=e,this.refs=a,this.updater=r||o}n.prototype.isReactComponent={},n.prototype.setState=function(t,e){if("object"!=typeof t&&"function"!=typeof t&&null!=t)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,t,e,"setState")},n.prototype.forceUpdate=function(t){this.updater.enqueueForceUpdate(this,t,"forceUpdate")},c.prototype=n.prototype;var s=p.prototype=new c;s.constructor=p,r(s,n.prototype),s.isPureReactComponent=!0;Array.isArray,Object.prototype.hasOwnProperty},294:(t,e,o)=>{o(408)}},e={};!function o(r){var a=e[r];if(void 0!==a)return a.exports;var n=e[r]={exports:{}};return t[r](n,n.exports,o),n.exports}(294)})();

运行 webpack 构建后,会生成 2 个文件夹:

  1. 位于 tsconfig.json 的 outDir 属性中提到的 Types 文件夹
  2. 由 webpack 生成的构建文件夹

答案#1

好的,我应该从哪里开始呢?首先让我说,我非常支持以 ESM(Ecma Script 模块或简称模块)格式分发库,因为这是目前最有前途、最现代的模块格式。

这并不意味着你不能以其他格式(例如 UMD)分发你的库,但我在回答中会专注于 ESM,因为这是在 React 应用中使用组件库的首选方式。

配置 Webpack

既然我们知道我们希望 Webpack 生成 ESM 输出,让我告诉你,Webpack 对 ESM 的支持仍然是实验性的,所以在过程中可能会遇到一些问题。

让我们修改 webpack.config.js 来生成 ESM:

  output: {
    ...,
    library: {
      type: "module",
    },
  },
  externals: {
    react: "react",
  },
  experiments: {
    outputModule: true,
  },
  ...

使用此配置,输出将包含类似 import * as React from 'react' 的内容,这正是我们想要的。

现在有一个问题,在 TS 转译后,Webpack 出于某种原因不遵循 TS 配置中的 "allowSyntheticDefaultImports": true,这个配置本应使 import React from 'react' 正常工作,即使 'react' 没有提供任何默认导出。但出于某种原因,输出仍尝试从 react 中获取 default,而实际上并不存在。这导致了问题

Cannot read properties of undefined (reading 'createElement')

这意味着 react 没有被正确解析。

我在这里没有找到任何理想的解决方案,但对我有效的是这个:

  1. 在 webpack.config.js 中添加 externalsType: "import",。这将动态导入 react,但不理想,因为它会在输出中增加大约 2kB 的运行时。
  2. 在你的源文件中使用 import * as React from 'react'。

这两种方法都可行,并且可以让你通过 import { Button } from 'your-lib'; 使用你的组件。但它们两种也都有点糟糕。

替代

这就引出了一个问题,如果没有更合适的库打包工具的话。我建议使用 Rollup,无论是直接使用还是通过 Vite 使用。与 Webpack 不同,它开箱即用地对 ESM 格式提供了出色的支持。

添加 rollup.config.js

import typescript from "@rollup/plugin-typescript";
import babel from "@rollup/plugin-babel";
import external from "rollup-plugin-peer-deps-external";
import terser from "@rollup/plugin-terser";

export default {
  input: "src/Button.tsx",
  output: {
    file: "build/main.js",
    format: "es",
    sourcemap: true,
  },
  plugins: [
    typescript(),
    babel({
      presets: ["@babel/preset-react"],
    }),
    external(),
    terser(),
  ],
  external: ["react"],
};

这个配置是你 Webpack 配置的精确镜像,并且在我看来更容易配置和阅读。当然,你需要安装所需的依赖项

yarn add -D rollup rollup-plugin-peer-deps-external @rollup/plugin-babel @rollup/plugin-terser @rollup/plugin-typescript tslib

另一个副作用是,至少在你的情况下,Rollup 的输出要小得多,基本上相当于:

import t from "react";
const e = (e) => t.createElement("button", null, e.label);
export { e as Button };

而 Webpack 的要大得多。

附注:如果你因为 Rollup 配置使用了 import 和 export 而遇到问题,只需在你的 package.json 中添加 "type": "module",,这将强制你的项目文件使用 ESM 而不是 CommonJS(及其 module.exports 和 require())。