You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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='&copy; <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='&copy; <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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 22:45:45