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