T3栈中集成React-Leaflet构建Next.js时出现构建失败问题
问题分析
你遇到的window is not defined错误,核心原因有两个:
- ReactMap被当作路由页面预渲染:你的
ReactMap.tsx放在pages/Estate目录下,Next.js会自动将其识别为路由页面/Estate/ReactMap,构建时会尝试服务器端预渲染这个页面。而leaflet库在导入阶段就会访问window对象,导致服务器端执行时报错。 - 组件外部代码触发window访问:
ReactMap.tsx中const markerIcon = new L.Icon(...)这行代码在组件外部执行,不管组件是否渲染,服务器端构建时都会运行这段代码,直接触发window未定义的错误。
解决方案
方案一:将ReactMap改为普通组件(推荐)
把ReactMap.tsx从pages目录移到components目录下,避免Next.js将其当作路由页面预渲染,仅作为Home页面的动态导入组件使用。同时调整组件内的代码,确保所有依赖window的逻辑只在客户端执行:
修改后的ReactMap.tsx关键部分:
import 'leaflet/dist/leaflet.css'; import { useState, useMemo } from 'react'; import { MapContainer, TileLayer, useMapEvents, Marker, Polyline, WMSTileLayer } from 'react-leaflet'; import { getEstateInfo } from '../../server/api/norkart'; import { EstateInfo } from '../../types'; const isServer = (): boolean => typeof window === 'undefined'; const LocationMarker = ({ markerIcon }: { markerIcon: any }) => { const [position, setPosition] = useState<any>(); const [estateInfo, setEstateInfo] = useState<EstateInfo>(); useMapEvents({ click(e) { setPosition(e.latlng); getEstateInfo([e.latlng.lat, e.latlng.lng]).then((data) => { setEstateInfo(data); }); } }); return position === undefined ? null : ( <div> <Marker position={position} icon={markerIcon} /> {estateInfo && estateInfo.Geometri && ( <Polyline pathOptions={{ color: 'purple', opacity: 1, fillColor: 'black', fillOpacity: 0.5 }} positions={estateInfo.Geometri} /> )} </div> ); }; const Map = () => { // 将L.Icon的创建移到组件内部,仅在客户端执行 const markerIcon = useMemo(() => { if (isServer()) return null; // 延迟导入L对象,避免服务器端加载 const L = require('leaflet'); require('leaflet-defaulticon-compatibility'); require('leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css'); return new L.Icon({ iconUrl: './marker.png', iconRetinaUrl: './marker.png', iconAnchor: [32, 64], iconSize: [64, 64] }); }, []); if (isServer()) return null; return ( <div> <MapContainer center={[58, 8]} zoom={12} style={{ height: '100vh', borderRadius: '5', position: 'relative' }} > <TileLayer attribution='© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors' url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' /> {false && ( <WMSTileLayer layers='webatlas-orto-newup' url='https://www.webatlas.no/maptiles/wms' maxZoom={21} minZoom={19} attribution='© 2023 Norkart AS/Geovekst og kommunene/OpenStreetMap/NASA, Meti' /> )} <LocationMarker markerIcon={markerIcon} /> </MapContainer> </div> ); }; export default Map;
方案二:保留路由页面,禁用该页面的SSR
如果你需要保留/Estate/ReactMap这个路由页面,在ReactMap.tsx顶部添加以下代码,强制该页面仅在客户端渲染:
// 强制页面完全在客户端渲染,跳过服务器端预渲染 export const dynamic = "force-dynamic";
同时,把组件外部的markerIcon创建逻辑移到组件内部,确保只在客户端执行(参考方案一中的useMemo写法)。
额外注意事项
- 移除
next.config.js中的styledComponents配置项,Next.js会识别为无效配置(构建日志中已有警告),如果需要启用styled-components,应该放在compiler配置下:module.exports = { compiler: { styledComponents: true, }, };
内容的提问来源于stack exchange,提问作者Olav Ausland
相关产品推荐
相关产品推荐

