Vite生产构建浏览器运行时错误:无法解构null的'basename'属性
问题背景
我用Rollup构建了一个依赖react-router-dom的React组件包,已将react-router-dom设为peer依赖并加入Rollup配置的external项。依赖该组件包的UI包采用Vite构建,开发模式运行正常,生产构建可成功完成,但浏览器加载时出现运行时错误。
运行时错误信息
TypeError: Cannot destructure property 'basename' of 'reactExports.useContext(...)' as it is null. at index.js:11:2890 at Nh (react-dom.production.min.js:160:137) at Yi (react-dom.production.min.js:192:57) at Vk (react-dom.production.min.js:293:275) at Uk (react-dom.production.min.js:279:389) at Tk (react-dom.production.min.js:279:320) at Ik (react-dom.production.min.js:279:180) at Nk (react-dom.production.min.js:270:88) at Ek (react-dom.production.min.js:272:300) at jg (react-dom.production.min.js:127:105)
相关代码片段
UI包的App.tsx
import React from "react"; import { BrowserRouter as Router } from "react-router-dom"; import { AppContainer } from "app-libs-ui/components"; import { store } from "app-libs-ui/redux"; import { UserService } from "app-libs-ui/services"; import "@flaticon/flaticon-uicons/css/all/all.css"; import './index.css'; import { createRoot } from 'react-dom/client'; import { Provider } from 'react-redux' // Custom imports: import { SideBarSections } from "./model/custom/sidebar/SideBarSections.js"; import { typeSpecs } from "./model/custom/index.js"; import { categorySpecs } from "./model/custom/index.js"; // End Custom imports const appContainer = document.getElementById('app'); const root = createRoot(appContainer!); export const renderApp = () => root.render( <Router> <Provider store={store}> <AppContainer typeSpecs={typeSpecs} categorySpecs={categorySpecs} sideBarSections={SideBarSections} /> </Provider> </Router> ); UserService.initKeycloak(renderApp);
UI包的vite.config.js
import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import path from 'path'; export default defineConfig({ root: 'src', build: { outDir: '../build', assetsDir: '', sourcemap: true, emptyOutDir: true, commonjsOptions: { exclude: [ "common-models-ts", "app-libs-ui", "app-libs-ui/*", "app-models-specification-json", "app-models-specification-json/*", "app-models-specification-ts", "app-models-specification-ts/*", "onchaindata-models-specification-ts", "onchaindata-models-specification-ts/*", ] }, }, plugins: [ react(), ], resolve: { alias: { 'react-router-dom': path.resolve(__dirname, 'node_modules/react-router-dom'), } }, optimizeDeps: { include: [ "react-router-dom", "common-models-ts", "app-libs-ui", "app-models-specification-ts", "onchaindata-models-specification-ts", ] }, publicDir: 'assets', server: { hmr: { clientPort: 443, overlay: false, } }, esbuild: { logOverride: { 'this-is-undefined-in-esm': 'silent' } } });
组件包的Rollup配置
import { nodeResolve } from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; import typescript from '@rollup/plugin-typescript'; import json from '@rollup/plugin-json'; import yaml from '@rollup/plugin-yaml'; import fs from 'fs'; import postcss from "rollup-plugin-postcss"; import terser from '@rollup/plugin-terser'; import peerDepsExternal from 'rollup-plugin-peer-deps-external'; const extensions = ['.js', '.ts', '.jsx', '.tsx']; const getFiles = (entry, extensions = [], excludeExtensions = []) => { let fileNames = []; const dirs = fs.readdirSync(entry); dirs.forEach((dir) => { const path = `${entry}/${dir}`; if (fs.lstatSync(path).isDirectory()) { fileNames = [ ...fileNames, ...getFiles(path, extensions, excludeExtensions), ]; return; } if (!excludeExtensions.some((exclude) => dir.endsWith(exclude)) && extensions.some((ext) => dir.endsWith(ext)) ) { fileNames.push(path); } }); return fileNames; }; export default { input: [ './src/index.ts', './src/index.css', ...getFiles('./src/components', extensions), ...getFiles('./src/constants', extensions), ...getFiles('./src/enums', extensions), ...getFiles('./src/errors', extensions), ...getFiles('./src/lib', extensions), ...getFiles('./src/model', extensions), ...getFiles('./src/pages', extensions), ...getFiles('./src/redux', extensions), ...getFiles('./src/services', extensions), ...getFiles('./src/util', extensions), ], output: { dir: 'dist', format: 'esm', preserveModules: true, preserveModulesRoot: 'src', sourcemap: true, }, plugins: [ peerDepsExternal(), nodeResolve({ jsnext: true, preferBuiltins: true, browser: true }), json(), yaml(), commonjs(), typescript({ tsconfig: './tsconfig.build.json', declaration: true, declarationDir: 'dist', }), postcss(), terser(), ], external: ['react', 'react-dom', 'react-router-dom'], };
问题原因
核心问题是生产构建时出现了react-router-dom的双实例:组件包和UI包各自加载了一份react-router-dom,导致组件包内的useContext获取的是自身实例的Router上下文,而UI包的Router是另一个实例提供的,上下文无法共享,最终返回null,解构basename时触发错误。
开发模式正常是因为Vite开发环境会强制共享依赖实例,但生产构建的树摇和打包策略,加上你配置的react-router-dom别名,干扰了Vite的依赖解析逻辑,导致组件包没有正确复用UI包的react-router-dom实例。
解决方案
移除Vite配置中
react-router-dom的别名
直接删除vite.config.js里resolve.alias中的'react-router-dom': path.resolve(__dirname, 'node_modules/react-router-dom'),这个配置会破坏Vite的依赖共享机制。确认组件包peerDependencies配置
检查组件包的package.json,确保react-router-dom在peerDependencies中,且版本范围与UI包兼容,示例:"peerDependencies": { "react": "^18.x", "react-dom": "^18.x", "react-router-dom": "^6.x" }优化Vite的commonjsOptions配置
将commonjsOptions.exclude中的"app-libs-ui"改为正则/app-libs-ui/,确保所有子路径都被排除在CommonJS转换之外:commonjsOptions: { exclude: [ /common-models-ts/, /app-libs-ui/, /app-models-specification-json/, /app-models-specification-ts/, /onchaindata-models-specification-ts/, ] }验证组件包的Rollup配置
你的Rollup配置已经正确使用peerDepsExternal()和手动声明external,确认组件包dist目录下的文件没有打包react-router-dom代码,所有引用都是import形式。清理缓存重新构建
- 删除UI包的
node_modules和build目录 - 删除组件包的
dist目录 - 重新执行
npm install安装依赖 - 先构建组件包,再构建UI包
- 删除UI包的
验证方法
生产构建完成后,通过以下方式确认依赖实例唯一:
- 打开浏览器Network面板,检查是否只加载了一份
react-router-dom资源 - 在组件包和UI包的代码中分别添加日志
console.log(require.resolve('react-router-dom')),查看打印的路径是否一致
内容的提问来源于stack exchange,提问作者Jason Separovic

