更改集群的样式不适用于 SSR 反应应用程序中的延迟加载传单

问题描述 投票:0回答:1

我在部署在服务器端渲染 (SSR) 环境中的 React 应用程序中延迟加载 Leaflet 地图时遇到了一些挑战。该应用程序涉及使用 Leaflet 在模式中加载产品列表,当用户将鼠标悬停在产品上时,我需要相应地更改地图上标记的图标和集群的样式。 我尝试了不同的方法,包括延迟加载 Leaflet 组件和使用 React-leaflet,但我遇到了一些问题:

  • 当我不对模态组件使用延迟加载时,本地一切工作正常,但部署后,pod 会进入崩溃循环,在尝试加载 Leaflet.js 库时引用“窗口未定义”。

/node_modules/leaflet/dist/leaflet-src.js:230
  var requestFn = window.requestAnimationFrame || getPrefixed('RequestAnimationFrame') || timeoutDefer;
                  ^

ReferenceError: window is not defined

  • 当我向组件添加延迟加载时,我可以部署项目而不会出现崩溃循环问题。但是,尽管更改单个标记图标仍然可以正常工作,但更改群集样式的功能不再有效。

起初,我认为传单可能无法使用延迟加载,因此我使用react-leaflet重写了所有代码,但问题仍然存在。 任何有关如何解决这些问题的见解或想法将不胜感激。

这是我的代码:

import { useEffect, useRef } from 'react';
import L, { DivIcon, MarkerClusterGroup } from 'leaflet';
import 'leaflet.markercluster/dist/leaflet.markercluster';
import 'leaflet.markercluster/dist/MarkerCluster.css';
import 'leaflet.markercluster/dist/MarkerCluster.Default.css';
import 'leaflet/dist/leaflet.css';
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: boolean,
    productListItems: ProductListItem[],
    hoveredProductId?: string,
): {
    mapRef: React.MutableRefObject<L.Map | null>;
    markerClusterRef: React.MutableRefObject<L.MarkerClusterGroup | null>;
} => {
    const mapRef = useRef<L.Map | null>(null);
    const markerClusterRef = useRef<L.MarkerClusterGroup | null>(null);

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

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

    const getClusterSize = (childCount: number): string => {
        if (childCount < 10) {
            return '-small';
        }

        if (childCount < 100) {
            return '-medium';
        }

        return '-large';
    };

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

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

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

    const initializeMap = (): L.Map | null => {
        const container = document.getElementById('map');
        if (!container) 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 = (): MarkerClusterGroup => {
        const clusters = L.markerClusterGroup({
            iconCreateFunction(cluster) {
                const childCount = cluster.getChildCount();
                const hasRelatedProduct = cluster
                    .getAllChildMarkers()
                    .some((marker: CustomMapMarker) => {
                        return marker.isHovered;
                    });

                return hasRelatedProduct
                    ? highlightedClusterIcon(childCount)
                    : defaultClusterIcon(childCount);
            },
        });
        markerClusterRef.current = clusters;
        return clusters;
    };

    const addMarkersToCluster = (map: L.Map, markers: MarkerClusterGroup): void => {
        productListItems.forEach((item, index) => {
            const currentItem = { ...item };
            if (currentItem.product !== undefined) {
                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,
                        }) as CustomMapMarker;
                        marker.productID = currentItem.product?.productId;

                        if (currentItem.product) {
                            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) {
            setTimeout(() => {
                mapRef.current = initializeMap();

                const clusters = createMarkerCluster();

                if (mapRef.current) addMarkersToCluster(mapRef.current, clusters);
            }, 0);
        }
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [isMapViewModalOpen]);

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

        markerClusterRef.current.eachLayer(layer => {
            if ('productID' in layer) {
                const marker = layer as CustomMapMarker;
                const shouldBeHighlighted = marker.productID === hoveredProductId;
                marker.setIcon(shouldBeHighlighted ? highlightedIcon : defaultIcon);
                marker.isHovered = shouldBeHighlighted;
            }
        });

        markerClusterRef.current.refreshClusters();
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [hoveredProductId]);

    return { mapRef, markerClusterRef };
};

export { MapView };

reactjs leaflet server-side-rendering react-leaflet leaflet.markercluster
1个回答
0
投票

window is not defined
可能是因为传单不太喜欢SSR。 确保动态加载使用传单组件的组件:

const Map: React.FC<MapProps> = (props) => {
  const Map = React.useMemo(
    () =>
      dynamic(() => import("src/components/Map/Leaflet/LeafletMap"), {
        loading: () => <Skeleton height={"400px"} />,
        ssr: false,
      }),
    []
  );
  return <Map {...props} />;
};

当谈到集群标记时,我注意到它的react-leaflet包非常轻,这使得在我自己的代码中复制适应变得很容易,下面你可以看到我的适应改变图标并添加其他功能标记:

import { createPathComponent } from "@react-leaflet/core";
import L, { LeafletMouseEventHandlerFn } from "leaflet";
import "leaflet.markercluster";
import { ReactElement, useMemo } from "react";
import { Building, BuildingStore, Circle } from "tabler-icons-react";
import { createLeafletIcon } from "./utils";
import styles from "./LeafletMarkerCluster.module.css";
import "leaflet.markercluster/dist/MarkerCluster.css";
type ClusterType = { [key in string]: any };

type ClusterEvents = {
  onClick?: LeafletMouseEventHandlerFn;
  onDblClick?: LeafletMouseEventHandlerFn;
  onMouseDown?: LeafletMouseEventHandlerFn;
  onMouseUp?: LeafletMouseEventHandlerFn;
  onMouseOver?: LeafletMouseEventHandlerFn;
  onMouseOut?: LeafletMouseEventHandlerFn;
  onContextMenu?: LeafletMouseEventHandlerFn;
};

// Leaflet is badly typed, if more props needed add them to the interface.
// Look in this file to see what is available.
// node_modules/@types/leaflet.markercluster/index.d.ts
// MarkerClusterGroupOptions
export interface LeafletMarkerClusterProps {
  spiderfyOnMaxZoom?: boolean;
  children: React.ReactNode;
  size?: number;
  icon?: ReactElement;
  maxClusterRadius?: number;
}
const createMarkerCluster = (
  {
    children: _c,
    size = 30,
    icon = <Circle size={size} />,
    maxClusterRadius = 40,
    ...props
  }: LeafletMarkerClusterProps,
  context: any
) => {
  const markerIcons = {
    default: <Circle size={size} />,
    property: <Building size={size} />,
    business: <BuildingStore size={size} />,
  } as { [key in string]: ReactElement };

  const clusterProps: ClusterType = {
    maxClusterRadius,
    iconCreateFunction: (cluster: any) => {
      const markers = cluster.getAllChildMarkers();

      const types = markers.reduce(
        (
          acc: { [x: string]: number },
          marker: {
            key: string;
            options: { icon: { options: { className: string } } };
          }
        ) => {
          const key = marker?.key || "";
          const type =
            marker.options.icon.options.className || key.split("-")[0];
          const increment = (key.split("-")[1] as unknown as number) || 1;

          if (type in markerIcons) {
            return { ...acc, [type]: (acc[type] || 0) + increment };
          }
          return { ...acc, default: (acc.default || 0) + increment };
        },
        {}
      ) as { [key in string]: number };
      const typeIcons = Object.entries(types).map(([type, count], index) => {
        if (count > 0) {
          const typeIcon = markerIcons[type];
          return (
            <div key={`${type}-${count}`} style={{ display: "flex" }}>
              <span>{typeIcon}</span>
              <span style={{ width: "max-content" }}>{count}</span>
            </div>
          );
        }
      });
      const iconWidth = typeIcons.length * size;

      return createLeafletIcon(
        <div style={{ display: "flex" }} className={"cluster-marker"}>
          {typeIcons}
        </div>,
        iconWidth,
        undefined,
        iconWidth,
        30
      );
    },
    showCoverageOnHover: false,
    animate: true,
    animateAddingMarkers: false,
    removeOutsideVisibleBounds: false,
  };
  const clusterEvents: ClusterType = {};
  // Splitting props and events to different objects
  Object.entries(props).forEach(([propName, prop]) =>
    propName.startsWith("on")
      ? (clusterEvents[propName] = prop)
      : (clusterProps[propName] = prop)
  );

  const instance = new (L as any).MarkerClusterGroup(clusterProps);

  instance.on("spiderfied", (e: any) => {
    e.cluster._icon?.classList.add(styles.spiderfied);
  });
  instance.on("unspiderfied", (e: any) => {
    e.cluster._icon?.classList.remove(styles.spiderfied);
  });

  // This is not used at the moment, but could be used to add events to the cluster.
  // Initializing event listeners
  Object.entries(clusterEvents).forEach(([eventAsProp, callback]) => {
    const clusterEvent = `cluster${eventAsProp.substring(2).toLowerCase()}`;
    instance.on(clusterEvent, callback);
  });
  return {
    instance,
    context: {
      ...context,
      layerContainer: instance,
    },
  };
};

// No update needed since leaflet cluster does not really support updates
const updateMarkerCluster = (instance: any, props: any, prevProps: any) => {};

const LeafletMarkerCluster = createPathComponent(
  createMarkerCluster,
  updateMarkerCluster
);

const LeafletMarkerClusterWrapper: React.FC<LeafletMarkerClusterProps> = ({
  children,
  maxClusterRadius = 40,
  spiderfyOnMaxZoom,
  ...props
}) => {
  const markerCluster = useMemo(() => {
    return (
      <LeafletMarkerCluster
        // Adding a key to force re-rendering when maxClusterRadius changes
        key={`marker-cluster-${maxClusterRadius}`}
        maxClusterRadius={maxClusterRadius}
        spiderfyOnMaxZoom={spiderfyOnMaxZoom}
      >
        {children}
      </LeafletMarkerCluster>
    );
  }, [children, maxClusterRadius, spiderfyOnMaxZoom]);
  return <>{markerCluster}</>;
};

export default LeafletMarkerClusterWrapper;

请注意,LeafletMarkerClusterWrapper 将触发标记的重新创建,因为群集标记本身并不能很好地适应更新。

这也是一些需要的CSS:

.leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow {
  -webkit-transition: -webkit-transform 0.3s ease-out, opacity 0.3s ease-in;
  -moz-transition: -moz-transform 0.3s ease-out, opacity 0.3s ease-in;
  -o-transition: -o-transform 0.3s ease-out, opacity 0.3s ease-in;
  transition: transform 0.3s ease-out, opacity 0.3s ease-in;
}

.leaflet-cluster-spider-leg {
  /* stroke-dashoffset (duration and function) should match with leaflet-marker-icon transform in order to track it exactly */
  -webkit-transition: -webkit-stroke-dashoffset 0.3s ease-out, -webkit-stroke-opacity 0.3s ease-in;
  -moz-transition: -moz-stroke-dashoffset 0.3s ease-out, -moz-stroke-opacity 0.3s ease-in;
  -o-transition: -o-stroke-dashoffset 0.3s ease-out, -o-stroke-opacity 0.3s ease-in;
  transition: stroke-dashoffset 0.3s ease-out, stroke-opacity 0.3s ease-in;
}

/* to hide cluster marker on spiderfy */
.spiderfied{
    opacity: 0 !important 
  }

请记住,您可能还需要动态导入 LeafletMarkerCluster 组件。

© www.soinside.com 2019 - 2024. All rights reserved.