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

SSR React应用中Leaflet懒加载时集群样式失效问题排查

解决SSR React应用中Leaflet地图的两个核心问题

问题概述

  • 未懒加载模态组件时:本地运行正常,部署后Pod崩溃,Leaflet加载时抛出错误:
    /node_modules/leaflet/dist/leaflet-src.js:230
      var requestFn = window.requestAnimationFrame || getPrefixed('RequestAnimationFrame') || timeoutDefer;
                      ^
    ReferenceError: window is not defined
    
  • 添加懒加载后:部署正常,但集群样式修改功能失效,仅标记图标修改正常,改用react-leaflet重写后问题仍存在

解决方案

一、修复SSR下window未定义问题

Leaflet在模块导入阶段就会访问window对象,而SSR环境没有浏览器全局对象,必须确保Leaflet相关代码只在客户端执行。

  1. 动态导入Leaflet依赖
    移除组件顶层的Leaflet导入,改为在客户端渲染时动态加载:

    useEffect(() => {
      if (isMapViewModalOpen) {
        import('leaflet').then(async (L) => {
          const markerCluster = await import('leaflet.markercluster');
          // 动态加载CSS
          import('leaflet/dist/leaflet.css');
          import('leaflet.markercluster/dist/MarkerCluster.css');
          import('leaflet.markercluster/dist/MarkerCluster.Default.css');
          // 执行地图初始化逻辑
          initializeMap(L, markerCluster);
        });
      }
    }, [isMapViewModalOpen]);
    
  2. 用React.lazy包裹模态组件
    把包含地图的模态组件用React.lazy和Suspense包裹,确保组件只在客户端渲染:

    import { lazy, Suspense } from 'react';
    const MapModal = lazy(() => import('./MapModal'));
    
    // 使用时
    <Suspense fallback={<div>Loading...</div>}>
      {isModalOpen && <MapModal />}
    </Suspense>
    

二、修复集群样式不更新问题

refreshClusters()不会重新触发iconCreateFunction,需要强制集群重新计算图标逻辑:

  1. 优化hover状态更新逻辑
    当hoveredProductId变化时,更新标记状态后,重新设置集群的图标生成函数并触发重绘:

    useEffect(() => {
      if (!markerClusterRef.current || !L) return;
    
      // 更新标记图标和状态
      markerClusterRef.current.eachLayer(layer => {
        if ('productID' in layer) {
          const shouldBeHighlighted = layer.productID === hoveredProductId;
          layer.setIcon(shouldBeHighlighted ? highlightedIcon : defaultIcon);
          layer.isHovered = shouldBeHighlighted;
        }
      });
    
      // 重新设置iconCreateFunction
      markerClusterRef.current.options.iconCreateFunction = (cluster) => {
        const childCount = cluster.getChildCount();
        const hasRelatedProduct = cluster
          .getAllChildMarkers()
          .some((marker) => marker.isHovered);
        return hasRelatedProduct
          ? highlightedClusterIcon(childCount)
          : defaultClusterIcon(childCount);
      };
    
      // 强制集群重绘
      markerClusterRef.current.refreshClusters();
      markerClusterRef.current.redraw();
    }, [hoveredProductId, L]);
    
  2. 避免硬编码图标生成函数
    把图标生成函数改为可复用变量,方便后续更新:

    let clusterIconCreator;
    const createMarkerCluster = (L, markerCluster) => {
      clusterIconCreator = (cluster) => {
        const childCount = cluster.getChildCount();
        const hasRelatedProduct = cluster
          .getAllChildMarkers()
          .some((marker) => marker.isHovered);
        return hasRelatedProduct
          ? highlightedClusterIcon(childCount)
          : defaultClusterIcon(childCount);
      };
      const clusters = markerCluster.MarkerClusterGroup({
        iconCreateFunction: clusterIconCreator
      });
      markerClusterRef.current = clusters;
      return clusters;
    };
    

修改后的完整代码示例

import { useEffect, useRef, useState } from 'react';
import { createRoot } from 'react-dom/client';

import mapMarkerHover from './assets/mapMarkerHover.png';
import { CustomMapMarker } from '../../types/customMapMarker';
import { ProductListItem } from '../../types/productListItem';
import mapMarker from './assets/mapMarker.png';
import { MapViewPopUpProduct } from './MapViewPopUpProduct';

const MapView = ({
  isMapViewModalOpen,
  productListItems,
  hoveredProductId
}) => {
  const mapRef = useRef(null);
  const markerClusterRef = useRef(null);
  const [L, setL] = useState(null);
  const [markerClusterLib, setMarkerClusterLib] = useState(null);

  const defaultIcon = () => L?.icon({
    iconUrl: mapMarker,
    iconSize: [24, 32],
  });

  const highlightedIcon = () => L?.icon({
    iconUrl: mapMarkerHover,
    iconSize: [24, 32],
  });

  const getClusterSize = (childCount) => {
    if (childCount < 10) return '-small';
    if (childCount < 100) return '-medium';
    return '-large';
  };

  const defaultClusterIcon = (childCount) => L?.divIcon({
    html: `<div><span>${childCount}</span></div>`,
    className: `marker-cluster marker-cluster${getClusterSize(childCount)}`,
    iconSize: [40, 40],
  });

  const highlightedClusterIcon = (childCount) => L?.divIcon({
    html: `<div><span>${childCount}</span></div>`,
    className: `marker-cluster marker-cluster${getClusterSize(childCount)} cluster-hovered`,
    iconSize: [40, 40],
  });

  let clusterIconCreator;

  const closePopup = () => {
    if (mapRef.current) mapRef.current.closePopup();
  };

  const initializeMap = () => {
    const container = document.getElementById('map');
    if (!container || !L) return null;
    const map = L.map(container, {
      center: [51.1657, 10.4515],
      zoom: 6,
      zoomControl: false,
    });
    mapRef.current = map;

    map.addControl(new L.Control.Zoom({ position: 'bottomright' }));
    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
    return map;
  };

  const createMarkerCluster = () => {
    if (!markerClusterLib) return null;
    clusterIconCreator = (cluster) => {
      const childCount = cluster.getChildCount();
      const hasRelatedProduct = cluster
        .getAllChildMarkers()
        .some((marker) => marker.isHovered);
      return hasRelatedProduct
        ? highlightedClusterIcon(childCount)
        : defaultClusterIcon(childCount);
    };
    const clusters = markerClusterLib.MarkerClusterGroup({
      iconCreateFunction: clusterIconCreator,
    });
    markerClusterRef.current = clusters;
    return clusters;
  };

  const addMarkersToCluster = (map, markers) => {
    if (!L) return;
    productListItems.forEach((item) => {
      const currentItem = { ...item };
      if (currentItem.product?.locations) {
        const popupNode = document.createElement('div');
        const root = createRoot(popupNode);
        root.render(
          <MapViewPopUpProduct {...currentItem.product} closePopup={closePopup} />
        );

        currentItem.product.locations.forEach(location => {
          const marker = L.marker([location.lat, location.long], {
            icon: defaultIcon(),
          });
          marker.productID = currentItem.product.productId;
          marker.isHovered = false;

          const popup = L.popup({
            closeButton: false,
            autoClose: false,
            closeOnClick: true,
            closeOnEscapeKey: true,
            minWidth: 350,
            className: 'custom-popup',
          })
            .setLatLng(map.getBounds().getCenter())
            .setContent(popupNode);
          marker.bindPopup(popup);

          markers.addLayer(marker);
        });
      }
    });
    map.addLayer(markers);
  };

  useEffect(() => {
    if (isMapViewModalOpen && !L) {
      // 动态加载Leaflet及相关依赖
      import('leaflet').then(async (leafletModule) => {
        setL(leafletModule.default);
        const mcModule = await import('leaflet.markercluster');
        setMarkerClusterLib(mcModule.default);
        // 加载CSS
        import('leaflet/dist/leaflet.css');
        import('leaflet.markercluster/dist/MarkerCluster.css');
        import('leaflet.markercluster/dist/MarkerCluster.Default.css');
      });
    }
  }, [isMapViewModalOpen, L]);

  useEffect(() => {
    if (isMapViewModalOpen && L && markerClusterLib && !mapRef.current) {
      setTimeout(() => {
        const map = initializeMap();
        const clusters = createMarkerCluster();
        if (map && clusters) addMarkersToCluster(map, clusters);
      }, 0);
    }
  }, [isMapViewModalOpen, L, markerClusterLib]);

  useEffect(() => {
    if (!markerClusterRef.current || !L) return;

    // 更新标记状态
    markerClusterRef.current.eachLayer(layer => {
      if ('productID' in layer) {
        const shouldBeHighlighted = layer.productID === hoveredProductId;
        layer.setIcon(shouldBeHighlighted ? highlightedIcon() : defaultIcon());
        layer.isHovered = shouldBeHighlighted;
      }
    });

    // 重新设置图标生成函数并触发重绘
    markerClusterRef.current.options.iconCreateFunction = clusterIconCreator;
    markerClusterRef.current.refreshClusters();
    markerClusterRef.current.redraw();
  }, [hoveredProductId, L]);

  return <div id="map" style={{ width: '100%', height: '400px' }} />;
};

export { MapView };

内容的提问来源于stack exchange,提问作者Ehsan Zand

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 13:27:06