You need to enable JavaScript to run this app. 如何在ReactDOM createPortal渲染的OverlayView上使用Google Map MarkerClusterer
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.16 09:15:04