前言
之前在分享路线规划功能时,由于篇幅和展示限制,很多核心逻辑没有完整呈现。所以打算开一个专题:在 JavaScript 中究竟该如何优雅、高效且节省成本地调用 Google Maps API?
本文将作为“Google Maps 开发专题”的核心篇章,以技术分享的视角,深度剖析项目中的 src/utils/googleMaps.js 文件。我们将从初始化策略、按需加载、地址补全与会话管理到数据清洗,一步步讲解如何将其封装为一个健壮的公共工具库。
📚 官方文档参考
本文涉及的 API 均基于 Google Maps Platform 官方文档:
Place Class Data Fields
🚀 为什么要封装?
在直接查看代码之前,我们先思考一个问题:为什么不直接在 Vue 组件里写 new google.maps.Map() ?
如果在每个需要地图的 .vue 文件中都单独引入 API,你会面临以下灾难:
- 重复加载与性能损耗:
每个组件都试图向<head>注入<script>标签,不仅浪费网络资源(Google Maps SDK 并不小),还可能引发google is not defined或API loaded multiple times错误。 - 昂贵的计费陷阱:
如果不封装 Places Autocomplete 的 Session Token 逻辑,用户每一次键盘敲击(如输入 "New York" 的 "N", "Ne", "New"...)都会算作一次 API 调用。未优化的调用方式可能导致费用高出数十倍。 - 维护噩梦:
无论是更换 API Key、调整语言(zh-CNvsen)、还是修改版本号,分散在几十个文件中的硬编码将让维护变得极其痛苦。 - 异步地狱:
Google Maps API 是异步加载的。你需要一个统一的机制来确保:在 API 真正就绪前,任何地图操作都应处于“等待”状态,而不是直接报错。
因此,我们需要一个 src/utils/googleMaps.js,作为一个单例(Singleton) 和 适配器,统一管理所有与 Google 地图的交互。
🛠️ 核心代码深度解析
让我们逐块分析 src/utils/googleMaps.js 的实现思路。所有代码片段均取自该文件。
1. 单例初始化与动态加载 (The Loader)
为了防止地图脚本被重复加载,我们使用了一个闭包变量 initPromise 来缓存加载状态。这也是避免很多报错的关键 包括以上我们提到的 google is not defined、API loaded multiple times、Google map 的重复加载等。
/* src/utils/googleMaps.js */
import { setOptions, importLibrary } from "@googlemaps/js-api-loader"
let isInitialized = false
let initPromise = null
/**
* 核心初始化函数
* 确保 Google Maps SDK 只会被加载和配置一次
*/
export function initGoogleMaps(apiKey) {
// 1. 如果已经加载完成,直接返回 resolved promise
if (isInitialized) return Promise.resolve()
// 2. 如果正在加载中(Promise pending),返回同一个 Promise
// 这防止了多个组件同时挂载时触发多次网络请求
if (initPromise) return initPromise
// 3. 开始初始化
initPromise = (async () => {
try {
if (!isInitialized) {
// 设置全局配置,如 API Key 和版本
setOptions({
key: apiKey,
v: "weekly", // 建议使用 weekly 或 quarterly 以保持稳定
})
isInitialized = true
}
} catch (error) {
console.error("Failed to initialize Google Maps:", error)
initPromise = null // 出错重置,允许后续重试
throw error
}
})()
return initPromise
}
✨ 技术亮点:动态库加载 (Dynamic Library Import)
传统的做法是一次性加载所有功能。而现在的最佳实践是使用 importLibrary。
我们封装了 loadGoogleMapsLibrary,支持按需加载 maps、places、routes 等库。
- 首屏优化:如果用户只是在看静态页面,不需要加载庞大的
routes算法库。 - 代码分割:Google 在内部也做了大量的代码分割,按需加载能显著减少主线程阻塞。
export async function loadGoogleMapsLibrary(libraryName) {
return await importLibrary(libraryName)
}
2. 智能的地址补全(Autocomplete & Session Tokens)
这是最需要注意钱包的地方!💸
Google Places API 的计费机制中有一个概念叫 Session。
- 如果不使用 Session:用户输入 "Airport",敲击 7 次键盘,触发 7 次请求,计费 7 次。
- 使用 Session:从用户开始输入,到最终选中某个地点的整个过程(无论敲击多少次),只算作一次请求。
我们在封装中强制引入了 SessionToken 的概念:
/**
* 创建一个独立的会话 Token
* 在用户聚焦输入框时调用此方法
*/
export async function createAutocompleteSessionToken(apiKey) {
await initGoogleMaps(apiKey)
const { AutocompleteSessionToken } = await loadGoogleMapsLibrary("places")
return new AutocompleteSessionToken()
}
/**
* 获取建议列表
* 自动处理了库加载、Token 传递和空值校验
*/
export async function fetchAutocompleteSuggestions(options) {
const { input, sessionToken, ...rest } = options
if (!input || !input.trim()) return []
// 1. 加载 places 库
const { AutocompleteSuggestion } = await loadGoogleMapsLibrary("places")
// 2. 构建请求,务必带上 sessionToken
const request = {
input: input.trim(),
sessionToken: sessionToken, // ✅ 关键:传入 Token 节省费用
language: "zh-CN",
...rest,
}
try {
const { suggestions } =
await AutocompleteSuggestion.fetchAutocompleteSuggestions(request)
return suggestions || []
} catch (error) {
console.error("Autocomplete error:", error)
return []
}
}
3. 数据清洗 (Data Normalization)
Google Maps 返回的 Place 对象结构非常深且复杂(包含大量的 Getter 方法、类实例等)。如果直接把这个对象传给 Vue 的 template 或 state,可能会导致序列化问题,且使用起来非常繁琐(例如必须写 place.location.lat() 这种函数调用,而且有什么你还根本拿不到一些信息,比如你在智能的地址补全中,你要获取地址的简称,如果你不进行出来而是在vue的Script 中就直接使用返回的数据去获取,你会得到undefined。具体的原因有可能是vue的Proxy 陷阱,后面在单独一个文章讲解这个吧)。
为了让业务组件更专注,我们封装了 placeToLocationData 函数,作为适配器模式的体现:
/**
* 将复杂的 Google Place 对象转换为扁平的纯 JSON 对象
*/
export function placeToLocationData(place) {
return {
place_id: place.placeId || "",
name: place.displayName || "",
formatted_address: place.formattedAddress || "",
// 将经纬度方法转换为纯数字对象,方便存储和传递
geometry: {
location: place.location
? {
lat: place.location.lat(), // 转换 lat() 方法为属性
lng: place.location.lng(), // 转换 lng() 方法为属性
}
: null,
},
}
}
✨ 收益:
- 解耦:组件层不需要关心 Google Maps 的内部数据结构。即使 Google 升级了 API,我们也只需要修改这一个转换函数。
- 响应式友好:纯 JSON 对象在 Vue/React 的状态管理中性能更好。
4. 路线规划 (Routing)
路线规划通常涉及复杂的参数配置。即使代码看起来很简单,封装的好处在于我们可以预设一些默认行为,并统一错误处理。
export async function computeRoutes(apiKey, request) {
await initGoogleMaps(apiKey)
// 动态加载路由库,这是体积最大的库之一,只有在真正需要计算路线时才加载
const { Route } = await loadGoogleMapsLibrary("routes")
return await Route.computeRoutes(request)
}
💡 总结与最佳实践
回顾我们的 googleMaps.js,它并不仅仅是一个工具函数集合,它是防御性编程和工程化思维的体现:
- Loader 模式:通过 Promise 单例处理异步加载,彻底消灭
google is not defined。 - 按需加载:利用
importLibrary拆分体积,提升页面性能。 - 成本控制:通过
SessionToken的强制封装,在架构层面帮公司/个人省钱。 - 适配器模式:通过数据清洗,隔离第三方 SDK 的复杂性,保持业务逻辑纯净。
下一步建议
在实际项目中,还可以进一步扩展此文件,例如:
- 封装
Map实例的创建,统一设置地图样式(Dark Mode)但这往往需要你在控制台 开通对应的地图id。 - 添加
Markers的批量管理和聚合(Clustering)。
希望这篇文章能帮助大家构建更健壮的前端地图应用!