如何在ReactDOM createPortal渲染的OverlayView上使用Google Map MarkerClusterer
解决方案
步骤1:修改CustomMarkerNew组件,补充必要方法与实例上报逻辑
调整后的组件会在初始化完成后将OverlayView实例抛出给父组件,同时补充MarkerClusterer必须依赖的getPosition方法:
import React, { useState, useEffect, useCallback } from "react"; import ReactDOM from "react-dom"; // 新增onMount、onUnmount两个回调props const OverlayView = (props) => { const { google } = window; const { googleMap, position, children, onMount, onUnmount } = props; const [latlng, setLatlng] = useState(position); const [container, setContainer] = useState(document.createElement("div")); const [overlay, setOverlay] = useState(undefined); const initOverlay = useCallback(() => { const overlayView = new google.maps.OverlayView(); overlayView.setMap(googleMap); overlayView.getDraggable = () => true; // 新增getPosition方法,供MarkerClusterer读取位置 overlayView.getPosition = () => new google.maps.LatLng(latlng.lat, latlng.lng); setOverlay(overlayView); setLatlng(position); google.maps.OverlayView.preventMapHitsFrom(container); }, [googleMap, container, latlng]); const onAdd = useCallback(() => { if (!container.classList.contains("customMarker")) { container.classList.add("customMarker"); container.style.position = "absolute"; const panes = overlay.getPanes(); panes.overlayImage.appendChild(container); } overlay.setPosition(); }, [overlay]); const draw = useCallback(() => { container.style.position = "absolute"; container.draggable = true; overlay.getPanes().floatPane.appendChild(container); overlay.setPosition(); }, [overlay, container, latlng]); const onRemove = useCallback(() => { ReactDOM.unmountComponentAtNode(container); setContainer(undefined); overlay.setMap(null); // 卸载时触发回调通知父组件 onUnmount?.(overlay); }, [container, overlay, onUnmount]); const setPosition = (pos) => { if (pos) setLatlng(pos); const projection = overlay.getProjection(); if (!projection) return; const point = projection.fromLatLngToDivPixel(latlng); if (point && point.x && point.y) { container.style.left = `${point.x}px`; container.style.top = `${point.y}px`; } }; const bindFunctions = useCallback(() => { overlay.onAdd = onAdd.bind(this); overlay.draw = draw.bind(this); overlay.onRemove = onRemove.bind(this); overlay.setPosition = setPosition.bind(this); overlay.getDraggable = () => true; }, [overlay]); useEffect(() => { if (overlay) { bindFunctions(); // 初始化完成后上报实例给父组件 onMount?.(overlay); } }, [overlay, onMount]); useEffect(() => { initOverlay(); }, []); return ( (overlay && children && container && ReactDOM.createPortal(children, container)) || null ); }; export default React.memo(OverlayView);
步骤2:父组件收集实例,初始化MarkerClusterer
父组件中维护marker实例集合,待所有实例加载完成后初始化聚合器,同时处理marker更新时的聚合刷新逻辑:
import { useRef, useEffect, useState } from 'react'; import MarkerClusterer from '@googlemaps/markerclustererplus'; const MapContainer = ({ height, width, mapMarkers }) => { const [gmap, setGmap] = useState(null); // 用ref存储marker实例和聚合器实例,避免触发不必要的重渲染 const markerInstancesRef = useRef([]); const clusterRef = useRef(null); // 处理marker实例上报 const handleMarkerMount = (marker) => { if (!markerInstancesRef.current.find(m => m === marker)) { markerInstancesRef.current.push(marker); } }; // 处理marker实例卸载 const handleMarkerUnmount = (marker) => { markerInstancesRef.current = markerInstancesRef.current.filter(m => m !== marker); }; // 初始化/更新聚合器 useEffect(() => { if (!gmap) return; // 每次marker列表变化时,先销毁原有聚合器 if (clusterRef.current) { clusterRef.current.clearMarkers(); clusterRef.current = null; } // 延迟执行,等待所有marker实例完成上报 setTimeout(() => { if (markerInstancesRef.current.length === 0) return; clusterRef.current = new MarkerClusterer(gmap, markerInstancesRef.current, { calculator: (gMarkers, numStyles) => { let index = 0; const count = gMarkers.length; let dv = count; while (dv !== 0) { dv = parseInt(dv / 10, 10); index += 1; } index = Math.min(index, numStyles); return { text: `${count}`, index }; }, gridSize: 50, styles: [ { className: "clusterMarker", width: 50, height: 18 } ] }); }, 0); }, [gmap, mapMarkers]); // 组件卸载时销毁聚合器 useEffect(() => { return () => { if (clusterRef.current) { clusterRef.current.clearMarkers(); clusterRef.current = null; } }; }, []); return ( <Map id="map" mapHeight={height} mapWidth={width} onLoad={setGmap}> {gmap && mapMarkers?.length > 0 ? ( <> {mapMarkers.map((m, i) => { return ( <CustomMarkerNew key={i} position={m.position} googleMap={gmap} onMount={handleMarkerMount} onUnmount={handleMarkerUnmount} > {m.icon} </CustomMarkerNew> ); })} </> ) : null} </Map> ); };
核心逻辑说明
MarkerClusterer要求传入的标记实例必须能读取位置信息,因此我们给自定义OverlayView补充了getPosition方法。同时通过挂载/卸载回调将所有渲染生成的OverlayView实例收集到父组件,就可以和之前类式创建的标记一样传入聚合器使用。
内容的提问来源于stack exchange,提问作者mcclosa
相关产品推荐
相关产品推荐

