NextJs中为自定义类型添加Leaflet Icon时出现ReferenceError
问题描述
在Next.js项目中为React Leaflet地图添加自定义Icon时,出现错误:
ReferenceError: window is not defined
仅当自定义类型中包含Leaflet Icon时触发该错误,不包含Icon时地图可正常渲染。目标是实现不同类型图标的生成功能,相关代码如下:
Map组件代码
import React, { useState, useEffect } from "react"; import "leaflet/dist/leaflet.css"; import { MapContainer, TileLayer, Popup, Marker as Marker2, useMapEvents, } from "react-leaflet"; import { type LatLngExpression, } from "leaflet"; import { Icon } from "leaflet"; import { ForestTypes } from "../../../types/leaflet"; export default function Map({ forestType }: { forestType: ForestTypes }) { const initialArray: { lat: number; lng: number; image: string; iconName: Icon; }[] = []; const [forestArray2, setForestArray2] = useState(initialArray); const LocationFinderDummy = () => { const map = useMapEvents({ click(e) { setForestArray2((prevValue) => [ ...prevValue, { lat: e.latlng.lat, lng: e.latlng.lng, image: forestType.image, iconName: forestType.iconName, }, ]); }, }); return null; }; return ( <MapContainer center={{ lat: 0.67, lng: 114.0 }} zoom={7} style={{ height: "100vh" }} > <LocationFinderDummy /> {forestArray2.map((item, index) => ( <Marker2 icon={item.iconName} key={index} position={[item.lat, item.lng]} title={`${item.lat}`} /> ))} <TileLayer attribution='© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors' url="https://stamen-tiles-{s}.a.ssl.fastly.net/watercolor/{z}/{x}/{y}.jpg" /> </MapContainer> ); }
调用地图的Home页面代码
import { NextPage } from "next"; import React, { useState } from "react"; import "leaflet/dist/leaflet.css"; import { forestTypes } from "../../data/ForestTypes"; import classNames from "classnames"; import dynamic from "next/dynamic"; import { type ForestTypes } from "../../types/leaflet"; import Image from "next/image"; import { green } from "../../data/ForestTypes"; const MapWithNoSSR = dynamic(() => import("../components/Map/Map"), { ssr: false, }); const Home: NextPage = () => { const [forestType, setForestType] = useState<ForestTypes>({ id: 1, name: "Forest1", image: "images/three.svg", iconName: green, }); return ( <> <main> <section id="hero"> <div className="sm:py-18 bg-white py-12 px-6 lg:px-8"> <div className="mx-auto max-w-2xl text-center"> <div> {forestTypes.map((forest) => ( <button className={classNames( forest !== forestType ? "" : "bg-blue-200", " px-6 py-5 text-center text-sm font-medium" )} key={forest.id} onClick={() => setForestType(forest)} > <Image src={forest.image} height="100" width="100" alt="forest" /> <div className="flex flex-row justify-center py-2"> {forest.name} </div> </button> ))} </div> </div> </div> </section> <div className="z-0 mx-auto w-3/4"> <MapWithNoSSR key={new Date().getTime()} forestType={forestType} /> </div> </main> </> ); }; export default Home;
ForestTypes数据定义
import { Icon } from "leaflet"; import { type ForestTypes } from "../types/leaflet"; export const green = new Icon({ iconUrl: "/images/tree.svg", iconSize: [36, 36], }); export const pink = new Icon({ iconUrl: "/images/tree2.svg", iconSize: [36, 36], }); export const forestTypes: ForestTypes[] = [ { id: 1, name: "Forest1", image: "/images/tree.svg", iconName: green, }, { id: 2, name: "Forest2", image: "/images/tree2.svg", iconName: pink, }, ];
类型定义文件leaflet.d.ts
import type { Icon } from "leaflet"; export interface ForestTypes { id: number; name: string; image: string; iconName: Icon; }
已通过动态导入禁用地图组件的SSR,但使用Icon时仍报错,需解决该问题并实现多类型图标渲染。
解决方案
问题根源
Leaflet的Icon构造函数依赖浏览器环境的window对象,而你的forestTypes数据文件在服务端渲染阶段就会执行,直接实例化Icon会导致服务端找不到window,触发错误。
具体修复步骤
1. 修改ForestTypes数据结构,存储Icon配置而非实例
把iconName改为存储Icon的配置对象,避免在服务端创建Icon实例:
// data/ForestTypes.ts import { type IconOptions } from "leaflet"; import { type ForestTypes } from "../types/leaflet"; export const greenIconConfig: IconOptions = { iconUrl: "/images/tree.svg", iconSize: [36, 36], }; export const pinkIconConfig: IconOptions = { iconUrl: "/images/tree2.svg", iconSize: [36, 36], }; export const forestTypes: ForestTypes[] = [ { id: 1, name: "Forest1", image: "/images/tree.svg", iconConfig: greenIconConfig, }, { id: 2, name: "Forest2", image: "/images/tree2.svg", iconConfig: pinkIconConfig, }, ];
2. 更新类型定义
修改leaflet.d.ts中的ForestTypes接口:
// types/leaflet.d.ts import type { IconOptions } from "leaflet"; export interface ForestTypes { id: number; name: string; image: string; iconConfig: IconOptions; }
3. 在客户端动态创建Icon实例
在Map组件中,仅在客户端环境下(判断window存在)创建Icon实例:
// components/Map/Map.tsx import React, { useState, useMemo } from "react"; import "leaflet/dist/leaflet.css"; import { MapContainer, TileLayer, Marker as Marker2, useMapEvents, } from "react-leaflet"; import { Icon, type IconOptions } from "leaflet"; import { ForestTypes } from "../../../types/leaflet"; export default function Map({ forestType }: { forestType: ForestTypes }) { const initialArray: { lat: number; lng: number; image: string; iconConfig: IconOptions; }[] = []; const [forestArray2, setForestArray2] = useState(initialArray); // 仅在客户端创建当前选中类型的Icon实例 const currentIcon = useMemo(() => { if (typeof window !== "undefined") { return new Icon(forestType.iconConfig); } return null; }, [forestType.iconConfig]); const LocationFinderDummy = () => { const map = useMapEvents({ click(e) { setForestArray2((prevValue) => [ ...prevValue, { lat: e.latlng.lat, lng: e.latlng.lng, image: forestType.image, iconConfig: forestType.iconConfig, }, ]); }, }); return null; }; return ( <MapContainer center={{ lat: 0.67, lng: 114.0 }} zoom={7} style={{ height: "100vh" }} > <LocationFinderDummy /> {forestArray2.map((item, index) => { // 为每个标记动态创建Icon实例 const markerIcon = typeof window !== "undefined" ? new Icon(item.iconConfig) : null; return markerIcon ? ( <Marker2 icon={markerIcon} key={index} position={[item.lat, item.lng]} title={`${item.lat}`} /> ) : null; })} <TileLayer attribution='© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors' url="https://stamen-tiles-{s}.a.ssl.fastly.net/watercolor/{z}/{x}/{y}.jpg" /> </MapContainer> ); }
4. 更新Home页面的状态初始化
用数组默认项初始化状态,避免手动创建对象:
// pages/index.tsx import { NextPage } from "next"; import React, { useState } from "react"; import "leaflet/dist/leaflet.css"; import { forestTypes } from "../../data/ForestTypes"; import classNames from "classnames"; import dynamic from "next/dynamic"; import { type ForestTypes } from "../../types/leaflet"; import Image from "next/image"; const MapWithNoSSR = dynamic(() => import("../components/Map/Map"), { ssr: false, }); const Home: NextPage = () => { const [forestType, setForestType] = useState<ForestTypes>(forestTypes[0]); return ( <> <main> <section id="hero"> <div className="sm:py-18 bg-white py-12 px-6 lg:px-8"> <div className="mx-auto max-w-2xl text-center"> <div> {forestTypes.map((forest) => ( <button className={classNames( forest !== forestType ? "" : "bg-blue-200", " px-6 py-5 text-center text-sm font-medium" )} key={forest.id} onClick={() => setForestType(forest)} > <Image src={forest.image} height="100" width="100" alt="forest" /> <div className="flex flex-row justify-center py-2"> {forest.name} </div> </button> ))} </div> </div> </div> </section> <div className="z-0 mx-auto w-3/4"> <MapWithNoSSR key={forestType.id} forestType={forestType} /> </div> </main> </> ); }; export default Home;
核心逻辑说明
通过将Icon的配置信息和实例化操作分离,确保只有在客户端环境下才调用Leaflet的Icon构造函数,完全避开服务端渲染时的window缺失问题,同时保留多类型图标的切换和渲染功能。
内容的提问来源于stack exchange,提问作者peternovak
相关产品推荐
相关产品推荐

