如何减少 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 之间切换时重复加载地图。
备注:内容仅供参考。
