数据从哪来
地图搜索端点返回的每个商家都带坐标字段:
latitude/longitude:纬度经度(可选字段,谷歌有时不给,要过滤)name、rating、address、position(本地排名)website_domain:用来标出「你的店」
技术选型:Leaflet
对比过几套方案后选了 Leaflet:体积小(几十 KB)、API 直白、社区插件多(聚合、热力都有)。底图用 OpenStreetMap 瓦片起步,不用申请 Key,开发阶段零门槛。
完整实现(React)
<span>"use client"</span>;
<span>import</span> { useEffect, useRef } <span>from</span> <span>"react"</span>;
<span>interface</span> <span>Place</span> {
<span>name</span>: <span>string</span>;
position?: <span>number</span>;
address?: <span>string</span>;
latitude?: <span>number</span>;
longitude?: <span>number</span>;
}
<span>export</span> <span>function</span> <span>PlacesMap</span>(<span>{ places }: { places: Place[] }</span>) {
<span>const</span> ref = useRef<<span>HTMLDivElement</span>>(<span>null</span>);
<span>useEffect</span>(<span>() =></span> {
<span>let</span> <span>map</span>: <span>any</span>;
(<span>async</span> () => {
<span>// Leaflet 依赖 window,必须动态引入(SSR 环境不能顶层 import)</span>
<span>const</span> L = (<span>await</span> <span>import</span>(<span>"leaflet"</span>)).<span>default</span>;
<span>await</span> <span>import</span>(<span>"leaflet/dist/leaflet.css"</span>);
<span>await</span> <span>import</span>(<span>"leaflet.markercluster"</span>);
<span>if</span> (!ref.<span>current</span>) <span>return</span>;
map = L.<span>map</span>(ref.<span>current</span>);
L.<span>tileLayer</span>(<span>"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"</span>, {
<span>attribution</span>: <span>"© OpenStreetMap contributors"</span>,
}).<span>addTo</span>(map);
<span>const</span> cluster = L.<span>markerClusterGroup</span>(); <span>// 点多了自动聚合</span>
<span>const</span> <span>points</span>: [<span>number</span>, <span>number</span>][] = [];
<span>for</span> (<span>const</span> p <span>of</span> places) {
<span>// 坐标是可选字段:没有就跳过,别画到 (0,0) 去</span>
<span>if</span> (p.<span>latitude</span> == <span>null</span> || p.<span>longitude</span> == <span>null</span>) <span>continue</span>;
<span>const</span> color = (p.<span>position</span> ?? <span>99</span>) <= <span>3</span> ? <span>"#16a34a"</span> : <span>"#f59e0b"</span>; <span>// 前3名绿色</span>
<span>const</span> marker = L.<span>circleMarker</span>([p.<span>latitude</span>, p.<span>longitude</span>], {
<span>radius</span>: <span>8</span>,
color,
<span>fillOpacity</span>: <span>0.8</span>,
});
marker.<span>bindPopup</span>(
<span>`<b>#<span>${p.position ?? <span>"-"</span>}</span> <span>${p.name}</span></b><br/><span>${p.address ?? <span>""</span>}</span>`</span>
);
cluster.<span>addLayer</span>(marker);
points.<span>push</span>([p.<span>latitude</span>, p.<span>longitude</span>]);
}
map.<span>addLayer</span>(cluster);
<span>if</span> (points.<span>length</span>) map.<span>fitBounds</span>(points, { <span>padding</span>: [<span>30</span>, <span>30</span>] });
})();
<span>return</span> <span>() =></span> map?.<span>remove</span>(); <span>// 卸载时销毁,防重复初始化</span>
}, [places]);
<span>return</span> <span><span><<span>div</span> <span>ref</span>=<span>{ref}</span> <span>style</span>=<span>{{</span> <span>height:</span> <span>480</span>, <span>borderRadius:</span> <span>8</span> }} /></span></span>;
}
三个设计点:动态引入(SSR 安全)、坐标过滤(可选字段)、前 3 名绿色(一眼分清你和竞品)。
三个进阶效果
1. 标记自己的店。 从数据里找 website_domain 等于自己域名的条目,换成大号红色标记 + 常开 tooltip:
<span>const</span> isMine = p.<span>website_domain</span> === <span>"your-site.com"</span>;
<span>const</span> marker = L.<span>circleMarker</span>([p.<span>latitude</span>!, p.<span>longitude</span>!], {
<span>radius</span>: isMine ? <span>12</span> : <span>8</span>,
<span>color</span>: isMine ? <span>"#dc2626"</span> : color,
});
2. 聚合开关。 上百个点时用 markerClusterGroup;想数密度时改用热力图插件(leaflet.heat),把 [lat, lng] 数组直接喂进去。
3. 按评分着色。 把 color 从「排名」换成「评分」档位(<4.0 红、4.0-4.5 黄、>4.5 绿),就是一张口碑分布图。
踩坑记录
坑 1:顶层 import L from "leaflet" 报 window is not defined。 Leaflet 依赖浏览器环境,Next.js 里要么 dynamic(() => import(...), { ssr: false }),要么像我上面一样在 useEffect 里动态 import。
坑 2:忘了引 leaflet.css。 结果瓦片错位、缩放按钮散架。Leaflet 的样式是必需的,不是可选装饰。
坑 3:没过滤空坐标。 latitude 缺失时传 undefined,marker 画到 (0,0)——几内亚湾上漂着一堆中国商家。过滤是硬要求。
坑 4:组件卸载没 map.remove()。 React 严格模式双执行下地图初始化两次,控制台警告 + 内存泄漏。清理函数里必须 remove。
坑 5:几百个点不开聚合。 页面卡顿、点叠成一坨。markerClusterGroup 一行接入。
坑 6:生产环境直接用 OSM 瓦片。 OSM 的公共瓦片有使用政策(限流量、要署名),上线前换商业瓦片服务或自建,别把 demo 配置带上生产。
工程清单
- Leaflet 动态引入,
leaflet.css必引 - 过滤空坐标,坐标字段是可选字段
- 用 circleMarker 按排名/评分上色,自己的店高亮
- 多点场景加 markercluster(或热力图)
- 卸载时
map.remove(),StrictMode 下别重复初始化 - 底图瓦片:开发用 OSM,生产换商业服务
地图可视化的收益,不在技术复杂度,而在「把数据变成决策」——同样一份商家数据,表格让人逐行读,地图让人一眼判断。做本地 SEO、渠道分析、门店选址,这张图值得花一个下午搭出来。
商家数据的坐标、排名、评分字段说明见 SerpBase 官方文档。你们的本地数据是怎么可视化的?表格、地图还是 Dashboard?评论区聊聊。
把表格里读不出的区域密度与竞争格局一眼看清,适合做本地 SEO、渠道分析与门店选址的同学。踩坑清单尤其省时间,一个下午即可落地。