Webpack打包React应用中Window未定义错误排查求助
我用Rollup打包了名为prism的React组件库,在Webpack打包的React Web应用beacon中引入该组件库后,运行应用时控制台抛出以下错误:
VM5253 main.js:1139 Uncaught TypeError: Cannot read properties of undefined (reading 'requestAnimationFrame') at eval (VM5329 index.js:31:477331) at eval (VM5329 index.js:31:477487) at prism/dist/esm/index.js (VM5253 main.js:1107:1) at __webpack_require__ (VM5253 main.js:1136:33) at fn (VM5253 main.js:1378:21) at eval (VM5328 Models.js:7:70) at ./src/components/views/Models.js (VM5253 main.js:118:1) at __webpack_require__ (VM5253 main.js:1136:33) at fn (VM5253 main.js:1378:21) at eval (VM5323 Router.js:10:75)
我推测错误源于prism依赖的react-dimensions调用了window.requestAnimationFrame,但不清楚为什么window变量会未定义。目前发现将react-dimensions设为prism的peer依赖并在beacon中单独安装可以解决问题,但我不想采用这种方式,希望找到其他解决方案。
相关配置文件
Prism(React组件库)
package.json
{"name": "prism","version": "1.0.4","description": "","type": "module","main": "dist/cjs/index.js","module": "dist/esm/index.js","scripts": {"test": "echo \"Error: no test specified\" && exit 1","build": "rollup -c"},"author": "","license": "ISC","devDependencies": {"@babel/cli": "^7.23.4","@babel/core": "^7.23.7","@babel/preset-env": "^7.23.8","@babel/preset-react": "^7.23.3","@rollup/plugin-babel": "^6.0.4","@rollup/plugin-commonjs": "^25.0.7","@rollup/plugin-image": "^3.0.3","@rollup/plugin-node-resolve": "^15.2.3","@rollup/plugin-terser": "^0.4.4","@rollup/plugin-typescript": "^11.1.6","@svgr/webpack": "^8.1.0","babel-plugin-add-module-exports": "^1.0.4","rollup": "^4.9.5","rollup-plugin-dts": "^6.1.0","rollup-plugin-node-polyfills": "^0.2.1","rollup-plugin-peer-deps-external": "^2.2.4","rollup-plugin-postcss": "^4.0.2"},"dependencies": {"@rollup/plugin-json": "^6.1.0","axios": "^1.6.5","babel-loader": "^9.1.3","fast-memoize": "^2.5.2","global": "^4.4.0","luxon": "^3.4.4","nodemon": "^3.0.2","react-grid-layout": "^1.4.4","rollup-plugin-inject": "^3.0.2","svg-inline-loader": "^0.8.2","victory": "^36.8.1","react-dimensions": "^1.3.1"},"peerDependencies": {"react": "^18.2.0","react-dom": "^18.2.0"}}
rollup.config.js
import terser from '@rollup/plugin-terser';import resolve from "@rollup/plugin-node-resolve";import commonjs from "@rollup/plugin-commonjs";import { babel } from '@rollup/plugin-babel';import peerDepsExternal from 'rollup-plugin-peer-deps-external';import image from '@rollup/plugin-image';import json from '@rollup/plugin-json';import packageJson from "./package.json" assert { type: "json" };export default {input: "src/index.js",output: [{file: packageJson.main,format: "cjs",sourcemap: "inline",},{file: packageJson.module,format: "esm",sourcemap: "inline",},],plugins: [peerDepsExternal(),commonjs({ include: /node_modules/ }),babel({ babelHelpers: 'bundled' }),terser(),json(),image(),resolve({browser: true}),],};
.babelrc
{"presets": ["@babel/preset-react","@babel/preset-env"]}
Beacon(React Web应用)
package.json
{"name": "beacon","version": "0.1.0","private": true,"main": "./src/index.js","dependencies": {"prism": "1.0.2","dotenv-webpack": "^8.0.1","html-webpack-plugin": "^5.6.0","lodash": "^4.17.21","react": "^18.2.0","react-dom": "^18.2.0","react-redux": "^9.1.0","react-router-dom": "^6.21.2","serve": "^14.2.1"},"scripts": {"start": "serve -s public/ -l 3000","dev": "NODE_ENV=development webpack-dev-server .","build": "webpack"},"devDependencies": {"@babel/cli": "^7.23.4","@babel/core": "^7.23.7","@babel/eslint-parser": "^7.23.3","@babel/plugin-transform-runtime": "^7.23.7","@babel/preset-env": "^7.23.8","@babel/preset-react": "^7.23.3","@babel/runtime": "^7.23.8","@svgr/webpack": "^8.1.0","babel-loader": "^9.1.3","css-loader": "^6.9.0","node-polyfill-webpack-plugin": "^3.0.0","style-loader": "^3.3.4","webpack": "^5.89.0","webpack-cli": "^5.1.4","webpack-dev-server": "^4.15.1"}}
webpack.config.js
const path = require("path");const HtmlWebpackPlugin = require('html-webpack-plugin');const Dotenv = require('dotenv-webpack');const NodePolyfillPlugin = require("node-polyfill-webpack-plugin")module.exports = {mode: "development",entry: "./src/index.js",output: {path: path.resolve(__dirname, "public"),filename: "main.js",publicPath: "/",},target: "web",devServer: {port: "3000",static: ["./public"],open: true,hot: true,liveReload: true,historyApiFallback: true,},resolve: {extensions: ['.js', '.jsx', '.json'],alias: {'react': path.resolve(__dirname, 'node_modules/react'),'react-dom': path.resolve(__dirname, 'node_modules/react-dom'),},fallback: {"fs": false,"tls": false,"net": false,"path": false,"zlib": false,"http": false,"https": false,"stream": false,"crypto": false,"util": false,"url": false,"assert": false,"tty": false,"os": false,}},module: {rules: [{test: /\.(js|jsx)$/,exclude: /node_modules/,loader: 'babel-loader',options: {presets: ['@babel/env', '@babel/preset-react']},},{test: /\.svg$/,use: ['@svgr/webpack'],},{test: /\.css$/,use: ['style-loader','css-loader']}]},plugins: [new NodePolyfillPlugin(),new HtmlWebpackPlugin({template: './public/index.html',}),new Dotenv(".env.development"),],}
解决方案
1. 在Rollup配置中注入全局window对象
修改prism的rollup.config.js,引入并配置rollup-plugin-inject,将window注入到依赖模块中:
import terser from '@rollup/plugin-terser'; import resolve from "@rollup/plugin-node-resolve"; import commonjs from "@rollup/plugin-commonjs"; import { babel } from '@rollup/plugin-babel'; import peerDepsExternal from 'rollup-plugin-peer-deps-external'; import image from '@rollup/plugin-image'; import json from '@rollup/plugin-json'; import inject from 'rollup-plugin-inject'; import packageJson from "./package.json" assert { type: "json" }; export default { input: "src/index.js", output: [ { file: packageJson.main, format: "cjs", sourcemap: "inline", }, { file: packageJson.module, format: "esm", sourcemap: "inline", }, ], plugins: [ peerDepsExternal(), commonjs({ include: /node_modules/ }), babel({ babelHelpers: 'bundled' }), terser(), json(), image(), resolve({browser: true}), inject({ window: 'global/window' }) ], };
该配置会让Rollup在打包时,将依赖中对window的引用替换为global/window,确保在浏览器环境下能正确识别全局对象。
2. 调整Babel预设,明确目标环境为浏览器
修改prism的.babelrc,给@babel/preset-env指定浏览器目标,避免编译出不兼容的代码:
{ "presets": [ "@babel/preset-react", ["@babel/preset-env", { "targets": "> 0.25%, not dead", "modules": false }] ] }
3. 让Webpack转译组件库代码
修改beacon的webpack.config.js中的babel-loader规则,取消对prism的排除,让Webpack转译组件库的代码,确保window在浏览器环境下被正确处理:
{ test: /\.(js|jsx)$/, exclude: /node_modules\/(?!prism)/, loader: 'babel-loader', options: { presets: ['@babel/env', '@babel/preset-react'] } }
4. 确保组件在客户端渲染时才执行
如果prism中的组件存在服务端渲染场景,可在组件中通过环境判断,确保react-dimensions相关代码只在浏览器中执行:
import React, { useEffect, useState } from 'react'; const MyComponent = () => { const [isClient, setIsClient] = useState(false); useEffect(() => { setIsClient(true); }, []); if (!isClient) return null; const DimensionedComponent = require('react-dimensions')(SomeComponent); return <DimensionedComponent />; }; export default MyComponent;
原因说明
当react-dimensions被打包进prism时,Rollup默认不会自动注入浏览器全局对象window,导致代码在Webpack打包后,window被视为未定义变量。而将其设为peer依赖时,react-dimensions由beacon的Webpack处理,Webpack的target: "web"配置会自动处理全局对象,因此不会报错。
内容的提问来源于stack exchange,提问作者Jack

