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

Blazor与OpenLayers地图渲染间歇性异常问题求助

问题:Blazor + OpenLayers 间歇性地图渲染失败(容器宽高为0)

使用Blazor、JavaScript及OpenLayers开发时,遇到间歇性问题:系统包含搜索条件和搜索结果组件,用户点击搜索后,Blazor加载地图结果页并调用加载地图的JS函数。多数情况功能正常,但偶尔地图无法渲染,控制台提示:

No map visible because the map container's width or height are 0

CSS已设置容器宽高,怀疑是Blazor与JS的时序问题,但不确定。


Blazor 代码

<link rel="stylesheet" href="./css/routes.css">
<PageTitle>Officer Locator</PageTitle>

@if (employeeCoordinates.Any())
{
    @foreach (var coordinate in employeeCoordinates)
    {
        <input type="hidden" class="employeeCoordinates" value="@coordinate.EmployeeId"     
               data-latitude="@coordinate.Latitude" 
               data-longitude="@coordinate.Longitude" 
               data-displaytext="@coordinate.DisplayText" />
    }
    <br />
    <div class="row col-lg-12 green-heading">
        <div class="col-lg-6">
            <div>Current Time: @DateTime.UtcNow.ToLocalTime().ToString("dd MMM yyyy HH:mm")</div>
        </div>
    </div>
    <br />
    <div class="row col-lg-12">
        <div id="map" @ref="mapElement"></div>
    </div>
    <br />
    <br />
    <p>Choose an item from the following list(s) in order to center the map on it.</p>
    <div class="row col-lg-8">
        <label asp-for="centerOn" class="col-lg-5 col-form-label">Displayed Items</label>
        <div class="col-lg-6">
            <select class="form-select long-select" aria-label="centerOn" name="centerOn" id="centerOn">
                @foreach (var coordinate in employeeCoordinates)
                {
                    <option data-latitude="@coordinate.Latitude" 
                            data-longitude="@coordinate.Longitude" 
                            value="@coordinate.EmployeeId">@coordinate.DisplayText</option>
                }
            </select>
        </div>
    </div>

    <input type="hidden" id="mapEndpoint" value="@mapEndpoint" data-endpoint="@mapEndpoint" />
}
else
{    
    <div class="alert alert-warning row col-lg-12" role="alert">
        No officers found for that search criteria
    </div>
}
<br />
<br />

@code {
    private IJSObjectReference? olModule;
    private IJSObjectReference? routeMapModule;    
    private ElementReference mapElement;
    private List<OfficerCoordinatesDto> employeeCoordinates = new();
    private string mapEndpoint = string.Empty;

    [Parameter]
    public List<OfficerCoordinatesDto> SearchResults { get; set; } = new ();    
       

    protected override async Task OnInitializedAsync()
    {                
        mapEndpoint = _config["MapIntegration:MapEndpoint"];
        employeeCoordinates = SearchResults;
    }
    

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            olModule = await _jsRuntime.InvokeAsync<IJSObjectReference>("import", "./js/ol.js");
            routeMapModule = await _jsRuntime.InvokeAsync<IJSObjectReference>("import", "./js/employee-map.js");            
            await routeMapModule.InvokeVoidAsync("addEmployeeMap");
        }
    }       

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (olModule is not null)
        {
            await olModule.DisposeAsync();
        }
        if (routeMapModule is not null)
        {
            await routeMapModule.DisposeAsync();
        }        
    }
}

JavaScript 代码

export function addEmployeeMap() {
    const employeeLong = $('.employeeCoordinates').first().data('longitude')
    const employeeLat = $('.employeeCoordinates').first().data('latitude')
    const mapEndpoint = $('#mapEndpoint').data('endpoint');

    let featuresToAdd = [];
    const vectorSource = new ol.source.Vector({ features: [] });


    $.each($(".employeeCoordinates"), function (i, obj) {
        const employeeId = obj.value;
        const long = obj.dataset.longitude;
        const lat = obj.dataset.latitude;
        const displayText = obj.dataset.displaytext;

        const iconFeature = new ol.Feature({
            geometry: new ol.geom.Point(ol.proj.fromLonLat([long, lat])),
            name: employeeId
        });

        const iconStyle = new ol.style.Style(
            {
                text: new ol.style.Text({
                    text: displayText,
                    font: 'bold 12px Arial',
                    fill: new ol.style.Fill({ color: 'blue', }),
                    stroke: new ol.style.Stroke({ color: 'white', width: 2, }),
                    textBaseline: 'hanging',
                    offsetY: 15
                })
            });

        const pointStyle = new ol.style.Style({
            image: new ol.style.Circle({
                radius: 9,
                fill: new ol.style.Fill({ color: 'green' }),
                stroke: new ol.style.Stroke({ color: 'white', width: 2 })
            })
        });

        iconFeature.setStyle([pointStyle, iconStyle]);
        featuresToAdd.push(iconFeature);
    });


    vectorSource.addFeatures(featuresToAdd);

    const vectorLayer = new ol.layer.Vector({ source: vectorSource });
    const mapImageLayer = new ol.layer.Tile({
        source: new ol.source.XYZ({
            url: mapEndpoint
        })
    });


    const view = new ol.View({
        center: ol.proj.fromLonLat([employeeLong, employeeLat]),
        projection: 'EPSG:3857',
        extent: ol.proj.transformExtent([-8.77, 49.61, 1.95, 60.93], 'EPSG:4326', 'EPSG:3857'),
        minZoom: 7,
        maxZoom: 16,
        zoom: 12
    });

    const map = new ol.Map({
        layers: [mapImageLayer, vectorLayer],        
        target: 'map',
        view: view
    });

    // Center on point when dropdown is changed
    const center = document.getElementById('centerOn');
    center.addEventListener(
        'change',
        function () {
            if ($('#centerOn :selected').text()) {
                let centerLong = $('#centerOn :selected').data('longitude');
                let centerLat = $('#centerOn :selected').data('latitude');

                let coordinate = ol.proj.fromLonLat([centerLong, centerLat]);

                view.setCenter(coordinate);
            };
        },
        false
    );
}

问题原因与解决方法

核心原因

你的猜测正确,问题出在Blazor与JS的执行时序:

  1. OnAfterRenderAsync仅保证组件已渲染,但DOM布局可能未完全完成(比如父元素异步渲染、CSS布局延迟计算),导致OpenLayers初始化时#map容器宽高仍为0。
  2. 依赖ID选择器获取DOM元素,存在元素未就绪的风险。

具体修改方案

1. 强制容器宽高生效

在CSS中给#map添加明确的宽高规则,避免依赖父元素布局:

#map {
    width: 100%;
    height: 600px;
    min-height: 400px;
    display: block;
}

2. 延迟+检查容器状态后初始化地图

修改JS函数,等待容器宽高满足条件后再初始化:

export function addEmployeeMap() {
    function waitForContainer() {
        const mapContainer = document.getElementById('map');
        const rect = mapContainer.getBoundingClientRect();
        if (rect.width > 0 && rect.height > 0) {
            initMap();
        } else {
            setTimeout(waitForContainer, 50);
        }
    }

    function initMap() {
        // 原地图初始化代码完全移到这里
        const employeeLong = $('.employeeCoordinates').first().data('longitude')
        const employeeLat = $('.employeeCoordinates').first().data('latitude')
        const mapEndpoint = $('#mapEndpoint').data('endpoint');

        let featuresToAdd = [];
        const vectorSource = new ol.source.Vector({ features: [] });

        $.each($(".employeeCoordinates"), function (i, obj) {
            const employeeId = obj.value;
            const long = obj.dataset.longitude;
            const lat = obj.dataset.latitude;
            const displayText = obj.dataset.displaytext;

            const iconFeature = new ol.Feature({
                geometry: new ol.geom.Point(ol.proj.fromLonLat([long, lat])),
                name: employeeId
            });

            const iconStyle = new ol.style.Style(
                {
                    text: new ol.style.Text({
                        text: displayText,
                        font: 'bold 12px Arial',
                        fill: new ol.style.Fill({ color: 'blue', }),
                        stroke: new ol.style.Stroke({ color: 'white', width: 2, }),
                        textBaseline: 'hanging',
                        offsetY: 15
                    })
                });

            const pointStyle = new ol.style.Style({
                image: new ol.style.Circle({
                    radius: 9,
                    fill: new ol.style.Fill({ color: 'green' }),
                    stroke: new ol.style.Stroke({ color: 'white', width: 2 })
                })
            });

            iconFeature.setStyle([pointStyle, iconStyle]);
            featuresToAdd.push(iconFeature);
        });

        vectorSource.addFeatures(featuresToAdd);

        const vectorLayer = new ol.layer.Vector({ source: vectorSource });
        const mapImageLayer = new ol.layer.Tile({
            source: new ol.source.XYZ({
                url: mapEndpoint
            })
        });

        const view = new ol.View({
            center: ol.proj.fromLonLat([employeeLong, employeeLat]),
            projection: 'EPSG:3857',
            extent: ol.proj.transformExtent([-8.77, 49.61, 1.95, 60.93], 'EPSG:4326', 'EPSG:3857'),
            minZoom: 7,
            maxZoom: 16,
            zoom: 12
        });

        const map = new ol.Map({
            layers: [mapImageLayer, vectorLayer],        
            target: 'map',
            view: view
        });

        // 强制更新地图尺寸
        map.updateSize();

        // Center on point when dropdown is changed
        const center = document.getElementById('centerOn');
        center.addEventListener(
            'change',
            function () {
                if ($('#centerOn :selected').text()) {
                    let centerLong = $('#centerOn :selected').data('longitude');
                    let centerLat = $('#centerOn :selected').data('latitude');

                    let coordinate = ol.proj.fromLonLat([centerLong, centerLat]);

                    view.setCenter(coordinate);
                };
            },
            false
        );
    }

    waitForContainer();
}

3. 传递Blazor元素引用给JS(可选,更可靠)

修改Blazor的OnAfterRenderAsync,直接传递mapElement给JS:

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender && employeeCoordinates.Any())
    {
        olModule = await _jsRuntime.InvokeAsync<IJSObjectReference>("import", "./js/ol.js");
        routeMapModule = await _jsRuntime.InvokeAsync<IJSObjectReference>("import", "./js/employee-map.js");            
        await routeMapModule.InvokeVoidAsync("addEmployeeMap", mapElement);
    }
}

对应修改JS函数参数:

export function addEmployeeMap(mapElement) {
    function waitForContainer() {
        const rect = mapElement.getBoundingClientRect();
        if (rect.width > 0 && rect.height > 0) {
            initMap(mapElement);
        } else {
            setTimeout(waitForContainer, 50);
        }
    }

    function initMap(mapElement) {
        // ... 其余代码不变,仅修改地图target为mapElement
        const map = new ol.Map({
            layers: [mapImageLayer, vectorLayer],        
            target: mapElement,
            view: view
        });
        // ... 其余代码
    }

    waitForContainer();
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 21:37:01