PAYATHON 2026

如何减少 Angular Web App(agm-core)的 Map calls?

数据阿明

结论

Angular 和 agm-core 不会自动把多个页面中的地图合并成一次 Map load。是否计费,主要取决于实际创建了多少个地图实例,而不是 Google Maps JavaScript API 脚本加载了多少次。

每当 <agm-map> 被销毁并重新创建时,agm-core 都会再次初始化底层的 google.maps.Map,通常也会产生一次新的 Dynamic Maps 地图加载。把地图尺寸设为 0 或移出屏幕,只能隐藏现有地图。如果切换路由时地图组件被销毁,这些 CSS 设置无法避免再次计费。

可以把地图放在所有子页面共用、且不会因路由切换而销毁的布局组件中。切换页面时,只替换 <router-outlet> 里的内容。需要隐藏地图时,应保留组件实例,不要使用 *ngIf、动态组件或会重建父布局的路由结构。

这种方式只能保证在同一次应用生命周期内复用一个地图实例。刷新浏览器、关闭后重新打开,或者离开并重新进入包含地图的应用区域,仍会创建新实例。因此,无法无条件保证整个用户 session 永远只计费一次。

为什么当前结构仍可能重复加载

组件是否处于”同一级别”,并不能说明它的实例会不会被保留。需要检查的是 Angular 组件树和路由树。

以下情况都会导致 <agm-map> 被重新创建:

  • 地图组件位于 <router-outlet> 内。
  • 每个页面模板都引用了一次地图组件。
  • 切换页面时,包含地图的父组件也被路由销毁。
  • 使用 *ngIf 隐藏地图,例如:
<app-map *ngIf="showMap"></app-map>
  • 使用 ngSwitch、动态组件或不同的布局路由重新创建地图。
  • 在代码中根据路由变化重新生成组件。
  • 页面导航触发了浏览器整页刷新,例如使用普通 URL 跳转,而不是 Angular Router。
  • 反复将 agm-core 的 <agm-map> 加入和移出 DOM。

设置 width: 0、height: 0 或 position: absolute; left: -99999px 不会主动销毁 Angular 组件。但如果这些样式应用在一个会被路由销毁的组件中,地图仍会重新创建。

推荐的组件和路由结构

可以建立一个长期存在的布局组件,例如 HomeComponent,并把地图和 <router-outlet> 都放进它的模板。Page 1 至 Page 5 则配置为它的子路由。

组件树可以采用以下结构:

AppComponent
└── HomeComponent
    ├── MapComponent
    └── RouterOutlet
        └── Page1Component | Page2Component | ... | Page5Component

切换 Page 1 至 Page 5 时,路由只替换 RouterOutlet 中的页面组件,不会销毁 HomeComponent 和 MapComponent。

路由配置

const routes: Routes = [
  {
    path: 'home',
    component: HomeComponent,
    children: [
      { path: 'page1', component: Page1Component },
      { path: 'page2', component: Page2Component },
      { path: 'page3', component: Page3Component },
      { path: 'page4', component: Page4Component },
      { path: 'page5', component: Page5Component },
      { path: '', redirectTo: 'page1', pathMatch: 'full' }
    ]
  }
];

HomeComponent 的模板可以这样写:

<div class="home-layout">
  <app-map
    class="shared-map"
    [class.shared-map--hidden]="!showMap">
  </app-map>

  <nav>
    <a routerLink="/home/page1">Page 1</a>
    <a routerLink="/home/page2">Page 2</a>
    <a routerLink="/home/page3">Page 3</a>
    <a routerLink="/home/page4">Page 4</a>
    <a routerLink="/home/page5">Page 5</a>
  </nav>

  <router-outlet></router-outlet>
</div>

不要在这里给 <app-map> 添加 *ngIf。

保留地图实例的隐藏方式

只改变地图是否可见,不改变组件是否存在:

.shared-map {
  display: block;
  width: 100%;
  height: 400px;
}

.shared-map--hidden {
  visibility: hidden;
  height: 0;
  overflow: hidden;
  pointer-events: none;
}

地图组件:

<agm-map
  [latitude]="latitude"
  [longitude]="longitude"
  [zoom]="zoom">
</agm-map>

如果地图恢复显示后出现灰色区域、瓦片没有铺满或中心点偏移,通常是因为容器尺寸为零时,地图无法正确计算布局。恢复显示后,一般需要触发一次尺寸重算,并重新设置中心点。

具体调用方式取决于项目使用的 agm-core 版本。常见做法是通过 AgmMap 获取底层原生地图,再触发 resize。

示意代码如下:

import { Component, ViewChild } from '@angular/core';
import { AgmMap } from '@agm/core';

@Component({
  selector: 'app-map',
  templateUrl: './map.component.html'
})
export class MapComponent {
  @ViewChild(AgmMap) agmMap!: AgmMap;

  latitude = 31.2304;
  longitude = 121.4737;
  zoom = 12;

  refreshMapSize(): void {
    this.agmMap.triggerResize(true);
  }
}

不同版本的 agm-core 可能采用不同的 triggerResize 签名和行为,应以项目实际安装版本的类型定义为准。调用前还要确保容器已经恢复到有效尺寸,例如在样式更新后的下一个事件循环中执行。

如何确认地图是否真的被重建

不要只看 GCP 控制台里的聚合数字。可以先通过 Angular 生命周期记录地图组件的创建和销毁:

import { Component, OnDestroy, OnInit } from '@angular/core';

@Component({
  selector: 'app-map',
  templateUrl: './map.component.html'
})
export class MapComponent implements OnInit, OnDestroy {
  ngOnInit(): void {
    console.log('MapComponent created');
  }

  ngOnDestroy(): void {
    console.log('MapComponent destroyed');
  }
}

然后依次点击 Page 1 至 Page 5:

  • 如果只出现一次 created,而且没有出现 destroyed,说明 Angular 正在复用地图组件。
  • 如果每次导航都会出现 destroyed 和 created,说明地图仍处于会被路由替换的组件分支中。
  • 如果 Angular 组件没有重建,但计费仍在增加,需要检查地图组件内部是否反复创建 google.maps.Map,以及页面中是否还有其他 <agm-map> 实例。
  • 还要检查浏览器 Network 面板,确认导航没有触发整页文档重新加载。

也可以监听 AGM 的地图就绪事件:

<agm-map
  [latitude]="latitude"
  [longitude]="longitude"
  (mapReady)="onMapReady($event)">
</agm-map>
onMapReady(map: google.maps.Map): void {
  console.log('Native Google Map created', map);
}

在一次应用运行期间,正常情况下这条日志只会出现一次。

不应采用的方式

下面的写法会在每次显示地图时创建一个新实例:

<app-map *ngIf="showMap"></app-map>

应保留组件,只切换样式:

<app-map [class.shared-map--hidden]="!showMap"></app-map>

也不要在五个页面中分别放置:

<app-map></app-map>

即使各页面传入的中心点和缩放级别完全相同,它们仍是不同的地图实例。

关于”一次 session 只加载一次”的边界

前端通常无法仅凭 Angular 的 session 概念,保证地图永远只产生一次计费加载。以下操作都会创建新的地图实例:

  • 刷新浏览器。
  • 直接访问另一个 URL,导致应用重新启动。
  • 关闭标签页后重新打开。
  • 从 /home 跳转到另一个会销毁 HomeComponent 的顶级路由,再返回 /home。
  • 应用错误恢复或热更新导致组件树重新初始化。

如果业务要求用户离开 /home 后再返回时仍能复用同一个地图,最简单的办法是把共享地图提升到更高层级、长期存在的 AppComponent。另一种办法是实现自定义 RouteReuseStrategy 来缓存路由组件,但这会同时缓存页面状态和组件资源,复杂度高得多,通常不值得仅为复用地图而优先采用。

更准确的目标是:在页面不刷新的情况下,只要共享布局组件没有被销毁,同一次单页应用生命周期内就只创建一个 <agm-map> 实例。只要路由层级设置正确、不使用 *ngIf 删除地图,并确认导航不会触发整页刷新,就能避免在 Page 1 至 Page 5 之间切换时重复加载地图。

备注:内容仅供参考。