把地图数据画出来:Leaflet + 搜索 API 做商家分布图

文章来源声明: 原文作者:解不开的开; 来源站点:掘金; 原文链接:https://juejin.cn/post/7689258467468935211; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

把表格里读不出的区域密度与竞争格局一眼看清,适合做本地 SEO、渠道分析与门店选址的同学。踩坑清单尤其省时间,一个下午即可落地。

做本地商家调研时,我拿到过一份 200 行的表格:商家名、评分、地址、排名。表格里看半天,看不出「哪片区域密、哪片区域空、我的店被谁围着」。后来把这些点画到地图上,问题十秒内一目了然——这就是地图可视化的价值:**坐标数据放在表里是数字,放在地图上是格局**。这篇用 Leaflet 把地图搜索端点的商家数据画成分布图。

数据从哪来

地图搜索端点返回的每个商家都带坐标字段:

  • 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 配置带上生产。

工程清单

  1. Leaflet 动态引入,leaflet.css 必引
  2. 过滤空坐标,坐标字段是可选字段
  3. 用 circleMarker 按排名/评分上色,自己的店高亮
  4. 多点场景加 markercluster(或热力图)
  5. 卸载时 map.remove(),StrictMode 下别重复初始化
  6. 底图瓦片:开发用 OSM,生产换商业服务

地图可视化的收益,不在技术复杂度,而在「把数据变成决策」——同样一份商家数据,表格让人逐行读,地图让人一眼判断。做本地 SEO、渠道分析、门店选址,这张图值得花一个下午搭出来。

商家数据的坐标、排名、评分字段说明见 SerpBase 官方文档。你们的本地数据是怎么可视化的?表格、地图还是 Dashboard?评论区聊聊。