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

React集成OpenLayers弹窗offsetWidth报错及位置异常求助

React集成OpenLayers弹窗的问题与解决

问题描述

尝试在React中集成OpenLayers弹窗,参考官方纯JS示例实现,但遇到两个问题:

  • 报错 Cannot read properties of null (reading 'offsetWidth')
  • 弹窗固定显示在地图左下角,而非点击位置

原实现代码

import {createRef, useEffect, useRef, useState} from 'react';
import './Map.css';

import Map from '../node_modules/ol/Map.js';
import Overlay from '../node_modules/ol/Overlay.js';
import TileLayer from '../node_modules/ol/layer/Tile.js';
import View from '../node_modules/ol/View.js';
import XYZ from '../node_modules/ol/source/XYZ.js';
import {toLonLat} from '../node_modules/ol/proj.js';
import {toStringHDMS} from '../node_modules/ol/coordinate.js';

const MapExample = () => {
    const [popupContent, setPopupContent] = useState('');
    const containerRef = createRef();
    const contentRef = createRef();
    const closerRef = createRef();
    
    const key = 'CvOgKFhRDDHIDOwAPhLI';

    const overlay = new Overlay({
        element: containerRef.current,
        autoPan: {
            animation: {
                duration: 250,
            },
        },
    });
    
    const handleCloser = () => {
        overlay.setPosition(undefined);
        closerRef.current.blur();
        return false;
    };
    
    useEffect(() => {
        const map = new Map({
            layers: [
                new TileLayer({
                    source: new XYZ({
                        url: 'https://api.maptiler.com/maps/streets/{z}/{x}/{y}.png?key=' + key,
                        tileSize: 512,
                    }),
                }),
            ],
            overlays: [overlay],
            target: 'map',
            view: new View({
                center: [0, 0],
                zoom: 2,
            }),
        });


        map.on('singleclick', (evt) => {
            const coordinate = evt.coordinate;
            const hdms = toStringHDMS(toLonLat(coordinate));

            setPopupContent('You clicked here: ' + hdms)
            overlay.setPosition(coordinate);
        });
    }, [])
    
    return (
        <div>
            <div id="map" className="map"></div>
            <div ref={containerRef} className="ol-popup">
                <a ref={closerRef} href="#" className="ol-popup-closer" onClick={handleCloser}></a>
                <div ref={contentRef}>{popupContent}</div>
            </div>
        </div>
    )
}

export default MapExample;

问题原因

核心问题是组件初始化阶段,弹窗DOM还未挂载,containerRef.current为null:

  1. 在组件顶层直接创建Overlay时,React尚未渲染弹窗DOM,OpenLayers无法读取null元素的offsetWidth属性,触发报错
  2. Overlay绑定的是null元素,定位逻辑失效,弹窗默认显示在地图原点(左下角)

修复方案

修复后代码

import {useRef, useState, useEffect} from 'react';
import './Map.css';

import Map from '../node_modules/ol/Map.js';
import Overlay from '../node_modules/ol/Overlay.js';
import TileLayer from '../node_modules/ol/layer/Tile.js';
import View from '../node_modules/ol/View.js';
import XYZ from '../node_modules/ol/source/XYZ.js';
import {toLonLat} from '../node_modules/ol/proj.js';
import {toStringHDMS} from '../node_modules/ol/coordinate.js';

const MapExample = () => {
    const [popupContent, setPopupContent] = useState('');
    const containerRef = useRef(null);
    const closerRef = useRef(null);
    const mapRef = useRef(null);
    const overlayRef = useRef(null);
    
    const key = 'CvOgKFhRDDHIDOwAPhLI';
    
    const handleCloser = () => {
        overlayRef.current.setPosition(undefined);
        closerRef.current?.blur();
        return false;
    };
    
    useEffect(() => {
        // DOM挂载后再初始化Overlay
        const overlay = new Overlay({
            element: containerRef.current,
            autoPan: {
                animation: {
                    duration: 250,
                },
            },
            positioning: 'bottom-center', // 显式设置弹窗相对点击点的位置
        });
        overlayRef.current = overlay;

        const map = new Map({
            layers: [
                new TileLayer({
                    source: new XYZ({
                        url: `https://api.maptiler.com/maps/streets/{z}/{x}/{y}.png?key=${key}`,
                        tileSize: 512,
                    }),
                }),
            ],
            overlays: [overlay],
            target: 'map',
            view: new View({
                center: [0, 0],
                zoom: 2,
            }),
        });
        mapRef.current = map;

        const handleMapClick = (evt) => {
            const coordinate = evt.coordinate;
            const hdms = toStringHDMS(toLonLat(coordinate));

            setPopupContent(`You clicked here: ${hdms}`);
            overlay.setPosition(coordinate);
        };

        map.on('singleclick', handleMapClick);

        // 清理函数避免内存泄漏
        return () => {
            map.un('singleclick', handleMapClick);
            map.dispose();
        };
    }, [key])
    
    return (
        <div>
            <div id="map" className="map"></div>
            <div ref={containerRef} className="ol-popup">
                <a ref={closerRef} href="#" className="ol-popup-closer" onClick={handleCloser}></a>
                <div>{popupContent}</div>
            </div>
        </div>
    )
}

export default MapExample;

关键修复点

  1. 将Overlay创建移至useEffect内部:确保弹窗DOM挂载后再初始化Overlay,避免传入null元素
  2. 替换createRef为useRef:函数组件中推荐使用useRef,它在组件生命周期内保持同一个引用,避免重复创建
  3. 添加positioning属性:显式定义弹窗相对于点击坐标的位置,确保定位逻辑生效
  4. 添加清理函数:移除地图事件监听并销毁地图实例,避免内存泄漏
  5. 可选:使用可选链操作符:避免访问null/undefined属性时触发报错

额外样式要求

确保Map.css包含弹窗基础样式,否则弹窗可能显示异常:

.ol-popup {
  position: absolute;
  background-color: white;
  box-shadow: 0 1px 4px rgba(0,0,0,0.2);
  padding: 15px;
  border-radius: 10px;
  border: 1px solid #cccccc;
  bottom: 12px;
  left: -50px;
  min-width: 280px;
}
.ol-popup:after, .ol-popup:before {
  top: 100%;
  border: solid transparent;
  content: " ";
  height: 0;
  width: 0;
  position: absolute;
  pointer-events: none;
}
.ol-popup:after {
  border-top-color: white;
  border-width: 10px;
  left: 48px;
  margin-left: -10px;
}
.ol-popup:before {
  border-top-color: #cccccc;
  border-width: 11px;
  left: 48px;
  margin-left: -11px;
}
.ol-popup-closer {
  text-decoration: none;
  position: absolute;
  top: 2px;
  right: 8px;
}
.ol-popup-closer:after {
  content: "✖";
}

内容的提问来源于stack exchange,提问作者Yelnur Zhazitov

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 16:05:53