如何让Webpack仅在react-dom 18+存在时才打包'react-dom/client'?
问题根源
Webpack是静态打包工具,构建阶段会静态扫描所有代码中的require/import语句收集依赖,完全不感知运行时的版本判断逻辑。只要代码中静态存在require('react-dom/client')语句,Webpack就会强制解析该模块路径。React 17及更早版本的react-dom包不存在client.js入口文件,会直接触发构建失败。
此前尝试的require.context方案本身用法错误:
require.context是Webpack专属的上下文依赖API,第一个参数接收的是相对当前文件的目录路径,不支持直接传入npm包名按Node.js模块规则解析- 该API只会在指定的单个目录下匹配文件,不会递归遍历上级
node_modules目录查找依赖,因此最终生成的上下文模块没有任何导出(对应日志中[no exports]的记录),根本无法正确加载react-dom/client
可行解决方案
方案1:跳过Webpack静态解析(通用库首选)
使用Webpack内置的__non_webpack_require__标识符书写动态加载逻辑,该标识符会被Webpack直接跳过不做静态解析,模块加载完全在运行时执行,同时兼容其他构建工具(Rollup/Vite等)和原生Node.js环境:
const ReactDOM = require('react-dom'); const reactMajorVersion = parseInt(ReactDOM.version.split('.')[0], 10); // 统一封装渲染方法,对外暴露一致接口 let renderRoot, hydrateRoot; if (reactMajorVersion >= 18) { const ReactDOMClient = typeof __non_webpack_require__ !== 'undefined' ? __non_webpack_require__('react-dom/client') : require('react-dom/client'); renderRoot = (container, element) => { const root = ReactDOMClient.createRoot(container); root.render(element); return root; }; hydrateRoot = ReactDOMClient.hydrateRoot; } else { renderRoot = (container, element) => { ReactDOM.render(element, container); return { unmount: () => ReactDOM.unmountComponentAtNode(container) }; }; hydrateRoot = (container, element) => { ReactDOM.hydrate(element, container); return { unmount: () => ReactDOM.unmountComponentAtNode(container) }; }; } module.exports = { renderRoot, hydrateRoot };
这个写法不会在代码中保留可被Webpack静态扫描到的react-dom/client引用,React 17及以下环境构建时不会触发模块解析错误,运行时会根据实际安装的React版本加载对应逻辑。
方案2:使用Webpack忽略注释(业务项目适用)
如果是在业务项目中编写适配逻辑,不需要兼容其他构建工具,可以直接在require语句前加Webpack专属的忽略注释,注释必须紧贴require语句上一行,不能有空行:
const reactMajorVersion = parseInt(ReactDOM.version.split('.')[0], 10); let createRoot; if (reactMajorVersion >= 18) { // webpackIgnore: true const ReactDOMClient = require('react-dom/client'); createRoot = ReactDOMClient.createRoot; } else { createRoot = (container) => ({ render: (element) => ReactDOM.render(element, container) }); }
方案3:配置Webpack外部依赖(项目级配置适用)
如果不想修改业务代码,可以在Webpack配置中将react-dom/client标记为外部依赖,告诉Webpack不要解析、打包该模块:
// webpack.config.js module.exports = { // 其余配置省略 externals: { 'react': 'React', 'react-dom': 'ReactDOM', 'react-dom/client': 'ReactDOMClient' } };
该方案需要项目自行保证运行时环境中存在对应全局变量,不适合通用库使用。
关于../../node_modules路径查找问题的说明
这个路径是Node.js模块解析规则和require.context的路径处理逻辑共同导致的:
- Node.js查找npm包时,默认会从当前文件所在目录开始,逐级向上遍历所有父级目录下的
node_modules文件夹,直到找到对应包或到达文件系统根目录 - 库文件被安装在
node_modules/react-on-rails/node_package/lib/目录下,require.context传入react-dom参数时,会从当前文件目录出发解析相对路径,遍历两级上级目录后到达node_modules/react-on-rails/目录,就会尝试查找该目录下的node_modules/react-dom路径,对应日志中显示的../../node_modules/react-dom/ - 由于
require.context不会递归遍历所有上级node_modules层级,最终没有找到匹配的client.js文件,生成了空的上下文模块。
内容的提问来源于stack exchange,提问作者Alexey Romanov

