Use Cases (更新: 2026/6/2)

用Claude Code把Google Maps接入Web应用:Next.js实战指南

用Claude Code安全接入Google Maps,覆盖Advanced Markers、地理编码、门店搜索、Mapbox取舍和上线陷阱。

用Claude Code把Google Maps接入Web应用:Next.js实战指南

先设计地图能力,再写地图组件

让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期间访问windowgoogle。它也会在组件卸载时清理标记点,避免页面切换后残留监听器。

// 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 MapsMapbox 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条门店数据验证搜索、标记点点击、手机端布局和账单指标,再扩大到生产数据。

#Claude Code #Google Maps #Mapbox #地图应用 #位置服务
免费

免费 PDF: Claude Code 速查表

输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。

我们会妥善保护你的信息,不发送垃圾邮件。

把 Claude Code 变成真正能带来结果的工作流

先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。

Masa

关于作者

Masa

专注 Claude Code 实务流程、团队导入和内容转化的工程师。