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 个文件夹:
- 位于 tsconfig.json 的 outDir 属性中提到的 Types 文件夹
- 由 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 没有被正确解析。
我在这里没有找到任何理想的解决方案,但对我有效的是这个:
- 在
webpack.config.js中添加externalsType: "import",。这将动态导入react,但不理想,因为它会在输出中增加大约 2kB 的运行时。 - 在你的源文件中使用
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())。

