用Claude Code把Google Maps接入Web应用:Next.js实战指南
用Claude Code安全接入Google Maps,覆盖Advanced Markers、地理编码、门店搜索、Mapbox取舍和上线陷阱。
先设计地图能力,再写地图组件
让Claude Code加一个地图组件很容易。真正影响线上质量的是另一组问题:API密钥怎样限制,地址搜索放在浏览器还是服务端,账单是否有告警,手机端列表和地图怎样共存,用户拒绝定位时页面是否还能用。
Masa在做门店搜索原型时踩过的坑不是标记点,而是地址搜索。Geocoding,也就是地理编码,指把地址转换成经纬度。它看起来只是一次API调用,但同一个地名可能有多个结果,调用次数会产生费用,返回内容也受Google Maps Platform政策约束。如果把所有逻辑都塞进浏览器,开发阶段能跑,生产阶段却可能留下安全和成本问题。
本文以Next.js App Router为例,演示怎样用Claude Code生成可维护的Google Maps集成:Maps JavaScript API、Advanced Marker、Geocoding API、门店搜索UI,以及什么时候改用Mapbox GL JS。API密钥和权限部分可以和Claude Code安全审计一起看,性能问题可以参考Claude Code性能优化,地理数据展示也可以延伸到Claude Code数据可视化。
给Claude Code的实现说明
不要只写“帮我接入Google Maps”。地图功能会影响产品、费用和合规,所以指令要写清楚运行约束。
请在Next.js App Router中实现门店搜索页面。
要求:
- 使用Google Maps JavaScript API
- 使用AdvancedMarkerElement,不使用旧Marker类
- 浏览器密钥从NEXT_PUBLIC_GOOGLE_MAPS_API_KEY读取
- 默认已配置HTTP referrer限制和API限制
- Geocoding API只能从服务端路由调用,使用GOOGLE_MAPS_SERVER_KEY
- 服务端密钥不能进入浏览器bundle
- 地址搜索、门店列表、标记点点击、选中状态要同步
- SSR期间不能读取window或google
- 处理loading、error、空结果、定位被拒绝、移动端布局
- 最后输出API限制、预算告警、政策检查清单
架构可以拆成下面几块。这样Claude Code生成代码后,review时能快速判断职责有没有混在一起。
flowchart LR
User["用户"]
Page["门店搜索页"]
Map["Google Maps JS API"]
Route["/api/geocode"]
Google["Geocoding API"]
Store["门店数据"]
Alerts["账单告警和日志"]
User --> Page
Page --> Map
Page --> Store
Page --> Route
Route --> Google
Route --> Alerts
给非工程成员说明时,也要把术语说成人话。Geocoding是把地址变成坐标,reverse geocoding是把坐标推回地址,map ID是Google Cloud里用于地图样式和Advanced Marker的标识。这个翻译动作能减少评审中的误解。
在Next.js中安全加载Google Maps
先安装加载器和类型定义。类型定义能帮助Claude Code发现过时示例,比如误用旧的Marker接口或在服务端访问google对象。
npm i @googlemaps/js-api-loader
npm i -D @types/google.maps
Google当前的Advanced Marker文档要求使用map ID。开发时可以用DEMO_MAP_ID做最小验证,生产环境应在Google Cloud Console里创建自己的map ID。Maps JavaScript API的浏览器密钥会暴露在前端,这是产品形态决定的;真正要做的是按照Google Maps Platform安全指南设置HTTP referrer限制和API限制。
// src/lib/google-maps-loader.ts
import { Loader } from "@googlemaps/js-api-loader";
let googleMapsPromise: Promise<typeof google> | null = null;
export function loadGoogleMaps() {
const apiKey = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY;
if (!apiKey) {
throw new Error("NEXT_PUBLIC_GOOGLE_MAPS_API_KEY is missing");
}
if (!googleMapsPromise) {
const loader = new Loader({
apiKey,
version: "weekly",
libraries: ["marker", "places"],
});
googleMapsPromise = loader.load();
}
return googleMapsPromise;
}
下面的组件只在客户端运行,避免SSR期间访问window或google。它也会在组件卸载时清理标记点,避免页面切换后残留监听器。
// src/components/GoogleBusinessMap.tsx
"use client";
import { useEffect, useRef } from "react";
import { loadGoogleMaps } from "@/lib/google-maps-loader";
export type MapPoint = {
id: string;
title: string;
lat: number;
lng: number;
category?: "store" | "warehouse" | "property";
};
type Props = {
points: MapPoint[];
center: google.maps.LatLngLiteral;
zoom?: number;
onSelect?: (point: MapPoint) => void;
};
export function GoogleBusinessMap({ points, center, zoom = 13, onSelect }: Props) {
const mapRef = useRef<HTMLDivElement>(null);
useEffect(() => {
let cancelled = false;
let markers: google.maps.marker.AdvancedMarkerElement[] = [];
async function renderMap() {
await loadGoogleMaps();
if (!mapRef.current || cancelled) return;
const { Map } = (await google.maps.importLibrary("maps")) as google.maps.MapsLibrary;
const { AdvancedMarkerElement, PinElement } = (await google.maps.importLibrary(
"marker",
)) as google.maps.MarkerLibrary;
const map = new Map(mapRef.current, {
center,
zoom,
mapId: process.env.NEXT_PUBLIC_GOOGLE_MAPS_MAP_ID ?? "DEMO_MAP_ID",
fullscreenControl: false,
gestureHandling: "cooperative",
});
markers = points.map((point, index) => {
const pin = new PinElement({
glyph: String(index + 1),
background: point.category === "warehouse" ? "#0f766e" : "#2563eb",
borderColor: "#ffffff",
glyphColor: "#ffffff",
});
const marker = new AdvancedMarkerElement({
map,
position: { lat: point.lat, lng: point.lng },
title: point.title,
content: pin.element,
});
marker.addListener("click", () => onSelect?.(point));
return marker;
});
}
renderMap().catch((error) => console.error("Failed to render Google Map", error));
return () => {
cancelled = true;
markers.forEach((marker) => {
marker.map = null;
});
};
}, [center.lat, center.lng, points, zoom, onSelect]);
return <div ref={mapRef} className="h-[420px] w-full rounded-lg border" />;
}
这里故意没有把用户输入拼接成HTML再放进InfoWindow。门店名、地址、备注都可能来自CMS或管理后台,应该用文本节点或受控组件展示。
把地址搜索放到服务端路由
浏览器用的Maps JavaScript API密钥和服务端调用Geocoding API的密钥要分开。前者用HTTP referrer限制,后者根据部署环境使用IP限制或其他服务器端限制。Geocoding的状态码和响应结构可以看官方请求与响应文档。
// src/app/api/geocode/route.ts
import { NextResponse } from "next/server";
type GeocodeResponse = {
status: string;
error_message?: string;
results: Array<{
formatted_address: string;
place_id: string;
geometry: { location: { lat: number; lng: number } };
}>;
};
const endpoint = "https://maps.googleapis.com/maps/api/geocode/json";
export async function GET(request: Request) {
const key = process.env.GOOGLE_MAPS_SERVER_KEY;
const { searchParams } = new URL(request.url);
const address = searchParams.get("address")?.trim();
if (!key) return NextResponse.json({ error: "Server key is missing" }, { status: 500 });
if (!address || address.length > 180) {
return NextResponse.json({ error: "Address is required" }, { status: 400 });
}
const params = new URLSearchParams({ address, key, language: "zh-CN", region: "cn" });
const response = await fetch(`${endpoint}?${params}`, { cache: "no-store" });
const data = (await response.json()) as GeocodeResponse;
const first = data.results[0];
if (!response.ok || data.status !== "OK" || !first) {
return NextResponse.json(
{ error: data.error_message ?? data.status },
{ status: data.status === "ZERO_RESULTS" ? 404 : 502 },
);
}
return NextResponse.json({
formattedAddress: first.formatted_address,
placeId: first.place_id,
location: first.geometry.location,
});
}
不要因为担心费用就随意长期缓存Geocoding结果。哪些内容可以保存、保存多久、能否和非Google地图一起显示,都要按Google Maps Platform的条款和具体产品政策判断。门店自己的坐标最好作为自有数据维护,用户输入的临时查询则按最小必要原则处理。
门店搜索、房源和运营地图的用法
第一个典型场景是门店搜索。分店、诊所、课程教室、展厅、活动会场都需要地址搜索、当前定位、营业时间、电话和预约按钮。Masa的原型一开始把手机端地图做得太高,结果列表被挤到下面。更好的做法是移动端先展示列表,用户点选门店后再移动地图中心。
第二个场景是房源或住宿搜索。价格、可租状态、面积、步行距离、筛选条件和地图边界必须同步。不要一次性展示全部标记点,而是根据当前viewport请求数据,密集区域使用聚合,列表排序也要和地图范围一致。
第三个场景是配送、外勤销售、设备巡检。这里地图是运营界面,不是装饰元素。要先决定位置更新频率、谁能查看、保存多久、用户拒绝定位时怎么处理。如果要做路线优化,还要提前确认Routes API或Directions API的费用模型。
第四个场景是内容地图,例如旅行攻略、城市指南、活动报道。地图能提高探索感,但不能替代正文。SEO和读者体验仍然需要交通方式、现场判断、注意事项和推荐理由。
何时选择Mapbox
Google Maps适合门店搜索、地址搜索、Place数据和用户熟悉的地图体验。Mapbox GL JS更适合大量自有地理数据、自定义样式、WebGL图层和可视化动画。Mapbox的核心概念可以从Mapbox GL JS指南开始。
| 判断点 | Google Maps | Mapbox GL JS |
|---|---|---|
| 门店搜索 | 和Geocoding、Places组合方便 | 自有数据为主时也可用 |
| 视觉自由度 | Cloud Styling足够多数业务 | 样式和图层控制更强 |
| 学习成本 | 常见Web应用更容易上手 | 需要理解source、layer和style |
| 运营风险 | 必须做密钥限制和账单告警 | 必须做token限制和归属显示 |
// src/components/MapboxPreview.tsx
"use client";
import { useEffect, useRef } from "react";
import mapboxgl from "mapbox-gl";
import "mapbox-gl/dist/mapbox-gl.css";
export function MapboxPreview() {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const token = process.env.NEXT_PUBLIC_MAPBOX_TOKEN;
if (!containerRef.current || !token) return;
mapboxgl.accessToken = token;
const map = new mapboxgl.Map({
container: containerRef.current,
style: "mapbox://styles/mapbox/streets-v12",
center: [121.4737, 31.2304],
zoom: 12,
});
map.addControl(new mapboxgl.NavigationControl(), "top-right");
return () => map.remove();
}, []);
return <div ref={containerRef} className="h-[420px] w-full rounded-lg border" />;
}
给Claude Code的指令应该是按角色拆分,而不是盲目“双实现”。例如:地址搜索和门店发现用Google Maps,自有数据热力图用Mapbox。这样架构更容易维护。
上线前要抓的坑
第一是未限制的API密钥。浏览器密钥虽然会公开,但必须限制来源和可用API。服务端密钥不能进入前端bundle。
第二是旧Marker示例。Google官方已经推荐Advanced Markers,Advanced Markers指南展示了AdvancedMarkerElement的用法。Claude Code如果生成旧代码,要要求它升级。
第三是SSR错误。在Next.js中顶层访问google.maps.Map会导致google is not defined。地图组件要使用"use client",并在useEffect里加载API。
第四是地址歧义。短地名、商圈名、车站名可能返回多个结果。应使用语言、区域、国家、结构化输入或候选列表,不要把原始地址字符串当作数据库主键。
第五是性能。数百个标记点和列表项同时渲染会拖慢页面。超过100个点时应考虑聚合、按视口查询、分页或服务器搜索。
第六是隐私。当前位置需要用户授权。拒绝授权时页面不能崩溃;如果保存位置信息,必须说明目的和保留期限。
结论和实际检查结果
用Claude Code接入Google Maps时,关键不是让地图出现,而是把地图渲染、地址搜索、密钥管理、费用监控、隐私和移动端体验拆开。Google Maps适合门店和地址工作流,Mapbox适合自有数据图层和视觉表达。
本文更新时,在不接入真实API密钥的范围内检查了Next.js职责拆分、SSR安全加载、错误分支和代码块格式。接入真实密钥前,请在Google Cloud Console配置Maps JavaScript API、Geocoding API、HTTP referrer限制、服务端密钥限制和预算告警。先用3条门店数据验证搜索、标记点点击、手机端布局和账单指标,再扩大到生产数据。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
把 Claude Code 变成真正能带来结果的工作流
先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。
相关文章
从Obsidian到CLAUDE.md的Claude Code流程:不再反复解释上下文
把 Obsidian 工作笔记整理成 CLAUDE.md 运行说明,让 Claude Code 每次都带着正确上下文开始。
Claude Code 收入 CTA 路由:从文章分流到 PDF、Gumroad 与咨询
用 Claude Code 按读者意图把文章流量分到免费 PDF、Gumroad 教材或咨询入口。
Claude Code 团队交接规则: 把审查证据、权限、回滚和收入路径一起交付
面向团队的 Claude Code 交接格式: 证据、权限、回滚、免费 PDF、Gumroad 与咨询路径都要可审查。