简介:这份压缩包面向正在学习百度地图JavaScript API的Web开发者与Java初学者,提供可直接导入Eclipse运行的网页地图示例,帮助理解地图初始化、中心点设置、标记添加、拖拽、获取位置及路径规划等常见交互。包内共53个文件,以37个JSP页面为主体,配合XML工程配置、JPG图片素材和少量JS脚本,整体约86KB,工程目录与Web配置相对完整,便于按模块阅读、运行和调试。已有553人学习,说明其作为入门练习材料具有一定参考价值。读者可从源码中观察地图API的调用方式、页面与配置文件的组织关系,并在此基础上修改参数、扩展图层或定位功能,适合课堂示例、自学练手和项目原型参考。对于需要理解百度地图服务如何嵌入传统Java Web工程的学习者,这类示例比零散代码片段更利于建立整体认识。
1. 一个后台系统临时加地图,别从啃文档开始
接手一个门店后台,产品临时加了一屏「门店分布图」:地图上打点、点击弹卡片、左边按城市筛选联动。翻开放平台的文档,几十个类、上百个方法,光「覆盖物」就分了七八种,硬啃两天也未必能上线。这种时候最快的路径不是通读文档,而是先拿一份能跑起来的地图 API学习示例源码——纯JavaScript,不依赖任何框架,跑起来就能看到点位、聚合、热力图、路线规划长什么样,再按业务往里改。
这份示例包适合两类人:第一次在地图API上做业务功能、需要一份能直接抄骨架的前端;以及手上有个老地图页面、想搞清楚标注和覆盖物到底该怎么管的中级开发。下面按我实际跑通的顺序拆:先认目录、再点亮地图,然后逐个拆标注与覆盖物模块,最后说二次开发怎么改。代码里的MapGL是示例包统一封装的命名空间,你接哪家平台的 SDK 就换成对应的全局对象,其余写法不变。
2. 跑通第一屏:密钥、脚本加载与目录结构
拿到一个示例包,最怕的是上来就改代码,改完发现连地图都不出来。我的习惯是先花十分钟把目录认清楚,再把加载链路理顺,最后才动业务逻辑。这一章按这个顺序走,每一步都给出可抄的代码。
2.1 目录结构:先认路再动手
示例包是典型的「多页面 + 公共 JS」结构,没有构建工具,也不需要 npm install,直接用静态服务器起一个端口就能跑。目录大致是这样:
map-demo/ ├── index.html # 总入口,列出所有示例的导航 ├── pages/ │ ├── marker.html # 标注点 + 信息窗口 │ ├── cluster.html # 点聚合 │ ├── heatmap.html # 热力图 │ ├── polyline.html # 折线 / 轨迹回放 │ ├── geocoder.html # 地址与经纬度互转 │ └── route.html # 路线规划 ├── js/ │ ├── loader.js # SDK 异步加载,全局只加载一次 │ ├── map-init.js # 地图实例创建 / 销毁 │ ├── markers.js # 标注渲染与信息窗口 │ ├── overlay.js # 折线、多边形、热力图 │ └── utils/coord.js # 坐标系转换 ├── css/map.css # 容器高度等必要样式 ├── data/ │ ├── shops.json # 门店示例数据(约 300 条) │ └── track.json # 轨迹点示例 └── README.md # 密钥申请与运行说明这个结构里有两个细节值得注意。第一,pages/下每个 HTML 只引自己需要的 JS,不做全局打包,好处是单个示例可以独立拷走,坏处是公共逻辑必须在js/里收口,否则六个页面会各写一份加载代码。第二,data/放的是静态 JSON 而不是写死在 JS 里,这是刻意为之——它逼着你把「取数据」和「画数据」分开,后面接真实接口时只要换fetch地址,渲染层几乎不用动。
提示:这类示例包通常要求走 http 协议打开,直接双击
file://会踩到跨域和相对路径两个坑,后文排查章节会细说。
2.2 脚本异步加载与密钥配置
地图 SDK 一般是几百 KB 的脚本,同步引入会阻塞首屏渲染,所以示例包用的是「动态插 script + callback 回调」的异步方案。loader.js干的事就三件:只加载一次、并发去重、超时兜底。
// js/loader.js // 统一入口:业务代码不关心 script 标签怎么插,只等一个 Promise const SDK_SRC = 'https://webapi.example.com/map/api'; // 换成密钥申请页给你的脚本地址 let loading = null; export function loadMapSDK({ ak, version = '3.0', timeout = 8000 } = {}) { if (window.MapGL) return Promise.resolve(window.MapGL); // 已加载,直接复用 if (loading) return loading; // 并发调用共用同一个 Promise loading = new Promise((resolve, reject) => { const cbName = '__mapReady_' + Date.now(); // SDK 通过 callback 参数回调一个全局函数,所以必须挂在 window 上 window[cbName] = () => { delete window[cbName]; // 用完即删,避免污染全局 window.MapGL ? resolve(window.MapGL) : reject(new Error('脚本执行完成但命名空间不存在,检查 version')); }; const script = document.createElement('script'); script.src = `${SDK_SRC}?v=${version}&ak=${ak}&callback=${cbName}`; script.async = true; script.onerror = () => reject(new Error('SDK 脚本请求失败,先看 Network 面板')); document.head.appendChild(script); // 兜底:网络被墙在中间态时,Promise 会一直 pending,页面像卡死 setTimeout(() => reject(new Error('SDK 加载超时')), timeout); }); return loading; }逻辑说明:loading这个变量是「并发去重」的关键。一个页面里可能有地图模块、路线模块、地理编码模块同时调用loadMapSDK,没有它就会插入三个 script 标签,触发三次回调,白浪费流量还可能互相覆盖回调名。cbName加时间戳是为了支持多次实例化时回调名不冲突。
参数说明:ak是你在开放平台申请的浏览器端密钥,必须走前端传参而不是写死在 HTML 里,方便多环境切换;version决定用 2D 还是 3D 版本,示例包里 3D 版本支持倾斜和旋转,但低端安卓机上掉帧明显,我一般按设备能力做降级;timeout默认 8 秒,弱网环境下可以放到 12 秒,但不要不设——没有超时兜底的 Promise 是最难排查的黑匣子。
2.3 最小地图实例:从 new 到 destroy 的完整生命周期
加载完之后,创建一个地图实例只需要一行,但真正能上线的实例需要处理三件事:容器尺寸变化时的重绘、页面离开时的销毁、以及初始化失败的兜底。
// js/map-init.js import { loadMapSDK } from './loader.js'; export async function createMap(containerId, ak) { await loadMapSDK({ ak }); const map = new window.MapGL.Map(containerId, { center: new window.MapGL.Point(116.404, 39.915), // 注意:经度在前,纬度在后 zoom: 12, // 常用 11~15,室内图可到 20 minZoom: 4, maxZoom: 19, enableScrollWheelZoom: true, enableDragging: true, }); // 容器尺寸变化(侧边栏收起、弹窗打开)后必须手动触发,否则瓦片错位 const ro = new ResizeObserver(() => map.resize()); ro.observe(document.getElementById(containerId)); // 返回句柄和销毁函数,交给调用方管理生命周期 return { map, destroy() { ro.disconnect(); map.clearOverlays(); // 先清覆盖物,再销毁实例 map.destroy && map.destroy(); }, }; }逻辑说明:ResizeObserver那段是这个示例包里少数「超出文档、但实战必须有」的代码。地图容器在 flex 布局里被侧边栏挤压时,SDK 不会自动感知尺寸变化,表现就是地图只渲染左半边、右边一片灰,很多人以为是瓦片没加载出来,其实是容器宽高变了没通知地图。destroy里先clearOverlays再destroy也是经验:直接销毁实例,挂在上面的标注和事件监听不一定会被回收,反复进出页面后内存会持续上涨。
参数说明:center的Point构造是「经度、纬度」顺序,和 GeoJSON、高德系的部分接口相反,这是最高频的低级错误,后文排查章节单独讲。zoom的下限设 4 是为了防止用户滚轮缩到世界地图级别,视觉上很难看;enableScrollWheelZoom在移动端建议关掉,否则会和页面滚动抢手势。
3. 核心模块拆解:标注、覆盖物与事件的渲染链路
地图能显示只是第一步,真正的业务量集中在「往地图上堆东西」和「点了之后干什么」。这一章拆三个模块:点位渲染、海量数据、事件回调。每一段都是示例包里能直接跑通、也能直接抄进项目的写法。
3.1 Marker 与信息窗口:几百个点位怎么画才不卡
点位渲染的朴素写法是「一条数据 new 一个 Marker」,一百条以内没问题,三百条开始掉帧,一千条直接卡死。示例包的做法是复用单个信息窗口 + 批量添加 + 统一的点击处理器:
// js/markers.js export function renderMarkers(map, list) { // 关键:整个页面只创建一个信息窗口,点击时只换内容,不重复 new const infoWindow = new window.MapGL.InfoWindow('', { width: 260, height: 0, // 0 表示高度自适应内容 enableAutoPan: true, // 弹窗贴边时自动平移地图,避免被裁掉 }); const markers = list.map((item) => { const marker = new window.MapGL.Marker( new window.MapGL.Point(item.lng, item.lat), // 经度在前 { title: item.name, enableMassClear: true } // title 让鼠标悬停有原生提示 ); marker.addEventListener('click', function () { infoWindow.setContent( `<div class="poi-card"><b>${item.name}</b><p>${item.addr}</p></div>` ); map.openInfoWindow(infoWindow, this.getPosition()); // this 就是被点的 marker }); map.addOverlay(marker); return marker; }); return markers; // 返回数组,筛选时逐个 remove,不要用 clearOverlays 全清 }逻辑说明:信息窗口是这一段的重点。每个 Marker 各 new 一个 InfoWindow 时,SDK 会为每个窗口创建独立的 DOM 节点和事件监听,三百个点位就是三百个隐藏的浮层,滚动地图时这些浮层的定位计算全在主线程上跑,卡顿来源就在这里。复用一个窗口,DOM 只有一份,切换时只改 innerHTML 和位置,实际测试三百点位的地图拖动帧率能稳定在 55 以上。
参数说明:width固定 260 是为了让卡片内文字换行一致,height: 0让高度随内容撑开,但要注意内容区必须给 CSS 内边距,否则贴边很难看。enableAutoPan建议开着,用户点边缘点位时地图会自动平移把卡片露出来,体验差别很大。enableMassClear设 true 后,页面切换时map.clearOverlays()才会把这个 Marker 一起清掉,漏设就会出现「换了页面标注还在」的玄学问题。
3.2 聚合、海量点与热力图:数据量上去之后的三种解法
三百条以上就得换方案。示例包里给了三条路,选哪条看数据量和业务诉求:
// js/overlay.js // 方案一:点聚合,适合「几百到几千」且需要看单点详情的场景 export function renderCluster(map, list) { const markers = list.map( (i) => new window.MapGL.Marker(new window.MapGL.Point(i.lng, i.lat)) ); return new window.MapGL.MarkerClusterer(map, { markers, gridSize: 60, // 聚合网格像素,越大越容易聚成一坨 maxZoom: 16, // 超过该级别不再聚合,直接显示单点 minClusterSize: 2, // 少于 2 个不聚合 }); } // 方案二:海量点,适合「上万条」且只关心分布密度的场景 export function renderMass(map, list) { const points = list.map((i) => new window.MapGL.Point(i.lng, i.lat)); return new window.MapGL.PointCollection(points, { size: 3, shape: 'circle' }); } // 方案三:热力图,适合看「区域热度」而不是具体点位 export function renderHeat(map, list) { return new window.MapGL.HeatmapOverlay({ radius: 24, // 影响半径,像素 opacity: 0.6, data: list.map((i) => ({ lng: i.lng, lat: i.lat, count: i.count || 1 })), }); }逻辑说明:这三种覆盖物的清理方式不一样,这是示例包里最容易被忽略的一处。聚合器返回的MarkerClusterer必须调用它自己的清理方法,光调map.clearOverlays()清不掉聚合图标,会出现「筛完之后旧的聚合数字还浮在地图上」的翻车现场。海量点和热力图则是整体作为一个覆盖物挂在 map 上,addOverlay之后就按普通覆盖物管理。
参数说明:gridSize是聚合的核心参数,60 意味着 60 像素内的点合并成一个气泡,手机上建议给 70~80,因为手指点击精度低;maxZoom: 16要结合业务,做门店导航时我会调到 18,用户放大到街道级就该看到具体门店而不是数字。热力图的radius和opacity要一起调,半径大而透明度高会出现整片糊住底图的情况,看不出路网。
注意:三种方案不要在同一个地图实例上叠加使用,尤其是聚合 + 海量点,覆盖物层级和高亮状态会互相干扰,业务上真需要就先分组、再按缩放级别切换渲染策略。
3.3 事件绑定里的 this:一个箭头函数引起的血案
地图 SDK 的事件回调是典型的「用this传上下文」的设计,而现代前端习惯写箭头函数,这一撞就出事:
// 正确:普通函数,this 指向触发事件的对象 marker.addEventListener('click', function (e) { console.log(this.getPosition()); // 拿到被点 Marker 的坐标 console.log(e.point); // 事件对象里也有坐标 console.log(e.target); // 指向被点的覆盖物 }); // 翻车:箭头函数没有自己的 this,模块作用域下是 undefined marker.addEventListener('click', (e) => { // this.getPosition() 会直接 TypeError // 只能用 e.target.getPosition() });逻辑说明:这不是 SDK 设计得怪,而是它要支持「一个回调处理多个对象」的写法,用this才能知道是哪个标注被点了。示例包里两种写法都保留了,但我的习惯统一用普通函数,因为e.target在部分覆盖物(比如折线、多边形)上返回的是整个覆盖物对象,和this语义不完全一致,混用容易在调试时绕晕。
除了click,示例包里还演示了mouseover/mouseout做悬浮高亮、dragend做点位拖拽拾取、zoomend做分级渲染。这几个事件的触发频率差别很大:zoomend只在缩放结束后触发一次,可以放心在里面做数据请求;mouseover在密集点位下会高频触发,必须加节流,否则鼠标划过一片点位时页面直接卡住。
参数说明:事件回调的e里通常包含point(地理坐标)、pixel(屏幕像素)、target(触发对象)、type(事件名)。做自定义弹窗定位时用pixel比point方便,因为它直接对应容器的 CSS 坐标,不需要再做一次投影换算。
4. 数据能力实战:地理编码、路线规划与坐标转换
地图的渲染层搞明白之后,真正吃时间的往往不是画图,而是数据对不上。这一章讲三个最容易出问题的数据能力:地址转坐标、路线规划、坐标系偏移。
4.1 地理编码与逆地理编码:把地址和经纬度互相翻译
业务数据里通常只有「XX 路 XX 号」这种文本地址,要打点必须先转成经纬度。SDK 提供的是回调式接口,直接拿来用会写出一堆嵌套,示例包里统一包成了 Promise,并加了并发控制:
// js/utils/geocode.js function geocodeOnce(address, city) { const gc = new window.MapGL.Geocoder(); return new Promise((resolve, reject) => { gc.getPoint( address, (point) => { if (point) resolve({ lng: point.lng, lat: point.lat }); else reject(new Error('解析失败:' + address)); }, city // 传城市名能显著提高命中率,尤其是「人民路」这种重名街道 ); }); } // 批量解析:每批 3 个,避免触发服务端 QPS 限制 export async function geocodeBatch(list, concurrency = 3) { const results = []; for (let i = 0; i < list.length; i += concurrency) { const batch = list.slice(i, i + concurrency); const settled = await Promise.allSettled( batch.map((it) => geocodeOnce(it.address, it.city)) ); results.push(...settled); } return results; // 每项是 {status:'fulfilled'|'rejected'},失败的单独重试 }逻辑说明:用Promise.allSettled而不是Promise.all是刻意的,一批里有一条地址解析不出来,all会整体 reject,前面成功的全白跑。分批是为了避开 QPS 限制,实际项目里我一般把并发压到 3,每批之间再等 100 毫秒,五百条地址大概十几秒跑完,比一次性并发被限速重试要快。
参数说明:city这个参数千万别省。不带城市时「中山路」可能解析到任意一个城市,带城市后命中率能提高一大截。逆地理编码反过来用getLocation(point),返回结构里包含省市区、街道、门牌号,做「拖拽地图拾取地址」功能时非常方便,但要注意它返回的精度只到街道级,门牌号经常是空的。
4.2 路线规划:驾车、步行、公交三种策略的参数差异
路线规划在示例包里是独立页面,三种出行方式各有自己的类,共用一套renderOptions:
// js/route.js const ROUTE_CLASS = { driving: 'DrivingRoute', walking: 'WalkingRoute', transit: 'TransitRoute', }; export function planRoute(map, origin, dest, mode = 'driving') { const Ctor = window.MapGL[ROUTE_CLASS[mode]]; const route = new Ctor(map, { renderOptions: { map, autoViewport: true, // 自动缩放到能看完整条路线 panel: 'route-panel', // 文字导航渲染到侧边栏容器 }, policy: mode === 'driving' ? 0 : undefined, // 0 表示默认(时间优先)策略 onSearchComplete: (res) => { if (!route.getResults() || route.getResults().getCurrentNumPois() === 0) { console.warn('未规划出路线,检查起终点是否在路网覆盖范围内'); return; } const plan = route.getResults().getPlan(0); // 第 0 条方案 const distance = plan.getDistance(true); // true 返回带单位的字符串 const duration = plan.getDuration(true); console.log(`全程 ${distance},预计 ${duration}`); }, }); route.search(origin, dest); // origin/dest 可以是 Point,也可以是字符串地址 return route; }逻辑说明:route.search的入参可以是坐标也可以是地址文本,传地址时 SDK 会内部先做一次地理编码,方便但慢,而且地址解析失败时错误信息很不友好。生产环境我一般先自己转坐标再传,这样失败点能精确定位到是哪一步出的问题。
参数说明:autoViewport: true是必须开的,否则路线画出来了但视野还停在原中心点,用户看不到。panel是文字导航的容器 id,不做侧边栏可以不传。policy只对驾车有效,表示策略(时间优先、距离优先、避开高速等),不同平台的枚举值不一样,改之前查一下自己的 SDK 文档,用错值不会报错,只会默默按默认策略返回——这种静默失败最耗时间。
4.3 坐标偏移:为什么你的点和底图对不上
后端数据库里存的如果是 GPS 设备采集的原始坐标,直接打到底图上会整体偏移几百米,看起来像「点在隔壁小区」。原因是底图用了加密后的坐标系,而 GPS 原始坐标是另一套,中间必须做一次转换:
// js/utils/coord.js // GPS 原始坐标 -> 国内加密坐标(GCJ-02),核心是两次非线性偏移 const PI = Math.PI; const A = 6378245.0; const EE = 0.00669342162296594323; function transformLat(x, y) { let ret = -100 + 2 * x + 3 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x)); ret += ((20 * Math.sin(6 * x * PI) + 20 * Math.sin(2 * x * PI)) * 2) / 3; ret += ((20 * Math.sin(y * PI) + 40 * Math.sin((y / 3) * PI)) * 2) / 3; return ret; } function transformLng(x, y) { let ret = 300 + x + 2 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x)); ret += ((20 * Math.sin(6 * x * PI) + 20 * Math.sin(2 * x * PI)) * 2) / 3; ret += ((20 * Math.sin(x * PI) + 40 * Math.sin((x / 3) * PI)) * 2) / 3; return ret; } export function wgs84ToGcj02(lng, lat) { let dLat = transformLat(lng - 105, lat - 35); let dLng = transformLng(lng - 105, lat - 35); const radLat = (lat / 180) * PI; let magic = Math.sin(radLat); magic = 1 - EE * magic * magic; const sqrtMagic = Math.sqrt(magic); dLat = (dLat * 180) / (((A * (1 - EE)) / (magic * sqrtMagic)) * PI); dLng = (dLng * 180) / ((A / sqrtMagic) * Math.cos(radLat) * PI); return { lng: lng + dLng, lat: lat + dLat }; }逻辑说明:这段算法是公开的通用实现,偏移量在中国境外基本为 0,境外坐标可以原样使用。但只做这一步还不够——很多底图用的是在这套加密坐标之上再叠了一层偏移的坐标系,所以要打点准确,需要「GPS → 加密坐标 → 底图坐标」两步。示例包的coord.js里两步都写了,我建议直接把目标坐标系的转换放到服务端做,前端只负责渲染,避免每换一个底图就全量重算。
参数说明:函数入参的lng/lat顺序不要弄反,反了之后结果是能算出来的(不会报错),但点位会偏到海里去,这也是个典型的静默失败。转换的精度在城市级够用,做车道级导航不行,那种场景必须用底图厂商提供的官方转换接口。加不加偏移,判断标准很简单:拿一个已知地标的真实经纬度打点,看它落在地标的哪一侧,偏了就是没转。
5. 排查手册:本地跑地图示例最容易翻车的五个地方
地图这块的报错有个共同特点:控制台经常是干净的,问题全在视觉上。下面五个是我自己在跑示例包和接项目时踩过的,按「现象 → 原因 → 解决」写,遇到问题照单排查基本能定位。
5.1 页面白屏,控制台干干净净
现象:地图容器里一片空白,Network 里脚本 200,Console 无报错,甚至能看到 canvas 元素存在。
原因:九成是容器高度为 0。地图容器是绝对定位于父元素的,父元素没给高度(比如放在 flex 布局里没设flex: 1或height),SDK 初始化时读到的高度就是 0,瓦片没有可视区域,自然什么都不画。剩下的一成是初始化时机太早,DOM 还没挂载就new Map。
解决:给容器一个明确高度,#map { width: 100%; height: 100%; }加上父级链路每一层都检查是否有高度;初始化放进DOMContentLoaded或ResizeObserver的首轮回调里。我一般会加一个断言,高度小于 100 就先console.error提醒,比事后猜快得多。
5.2 点位整体偏移几百米
现象:所有标注都在地图上,但统一往某个方向偏了三四百米,缩放级别越高偏得越明显。
原因:两种可能。一是Point参数顺序写反了,纬度当经度传;二是坐标系没转换,GPS 原始坐标直接打到了加密底图上。
解决:先打印第一个点的经纬度,看数值范围——国内经度在 73~135 之间,纬度在 3~53 之间,数值明显错位就是顺序问题。数值范围正常但整体偏移,就是坐标系问题,按 4.3 节做转换。我吃过一次亏:转换函数写对了,但只对新增数据做了转换,历史数据没刷,导致新旧点位一半对齐一半偏移,排查了半天以为是地图 SDK 的问题。
5.3 密钥报错与域名白名单
现象:地图不出来,控制台提示密钥校验失败、referer 不合法、或者返回一个错误码但页面没任何提示。
原因:浏览器端密钥通常绑定了域名白名单,localhost或127.0.0.1没加进去就报错;另外部分平台的密钥区分「浏览器端」和「服务端」,用反了也会失败;还有一种情况是本机开了代理工具改了请求头。
解决:到控制台把localhost、127.0.0.1和真实域名都加进白名单,注意端口是否需要单独配置;核对密钥类型;检查是否有插件修改了请求。密钥不要写死在 HTML 里,用环境变量注入,否则切环境时一定会漏改。
5.4 反复切换筛选后地图越来越卡
现象:按城市筛选几次之后,地图拖动明显掉帧,最后甚至卡死。
原因:每次筛选都重新new Marker但没清理旧对象;或者清理时只调了map.clearOverlays(),而聚合器的图标是独立管理的,没被清掉,越积越多。
解决:维护一个markers数组,切换时逐个removeOverlay;聚合场景下必须调用聚合器自己的清理方法再重新创建。判断有没有内存泄漏很简单:在 DevTools 的 Memory 面板打两个快照,看 Marker 对象数量是不是只增不减。从那以后我养成了一个习惯——任何「重新渲染」的函数,第一行先写清理。
5.5 移动端点击标注没反应
现象:PC 上点点位弹窗正常,真机上点十次中一次。
原因:默认图标尺寸偏小,手指接触面积不够;另外地图容器本身在监听拖拽手势,快速点击会被判定为拖拽的起点,事件被吞掉。
解决:把图标尺寸放大到至少 32×32,并用anchor把锚点对准图标底部中心,视觉上才不会偏;点击热区可以通过在 Marker 上叠加一个透明的大图标实现;手势冲突则要在容器上做区分,短按和拖拽用时间阈值判断。真机调试这块没有捷径,必须上手机点,模拟器的触摸事件和真机差别很大。
6. 二次开发:把示例改成能吃业务数据的地图组件
示例包能跑通不等于能进项目。业务页面关心的是「给一批数据,地图上出现对应的点,切数据就换掉」,而不是new Marker写在哪。我的做法是在示例之上封一层MapView,把生命周期、渲染策略、清理逻辑全收进去,页面里只留一行setData:
// js/map-view.js export class MapView { constructor(el, { ak, center, zoom = 12, mode = 'marker' } = {}) { this.el = el; this.options = { ak, center, zoom, mode }; // mode: marker | cluster | heat this.overlays = []; } async init() { const { map, destroy } = await createMap(this.el, this.options.ak); this.map = map; this._destroy = destroy; return this; } setData(list) { this.clear(); // 数据驱动:先清后画 const { mode } = this.options; this.overlays = mode === 'cluster' ? [renderCluster(this.map, list)] : renderMarkers(this.map, list); return this; // 支持链式调用 } clear() { this.overlays.forEach((o) => o.clearMarkers && o.clearMarkers()); // 聚合器 this.map.clearOverlays(); this.overlays = []; } destroy() { this.clear(); this._destroy(); } }这段封装的三个设计点值得说一下。第一,mode用配置而不是让调用方自己判断,页面不需要知道聚合和海量点的差别,后面加新的渲染策略只改这一个文件。第二,clear里先判断clearMarkers再统一清覆盖物,把聚合器的特殊性藏在了组件内部,调用方永远不用记这件事。第三,destroy里先clear再销毁,杜绝了「页面切走后标注还挂在内存里」的情况。
改造时最容易过头的两件事,一是过早引入框架封装,把地图包成 React/Vue 组件后,useEffect的依赖数组和地图实例的生命周期很难对齐,我见过一个项目因为依赖写错,每次输入框变化都重建一次地图;二是把所有交互都做成配置项,最后 options 有二十几个字段,维护成本比直接写业务还高。我的经验是先只抽「数据进、画面出」这一层,交互类需求(拖拽拾取、框选、测距)等真的出现第二次再抽。
验证改造是否成功,我一般跑一张固定清单:筛选切换十次后 Memory 快照里 Marker 数量是否回落、进出页面五次后地图实例是否只有一个、移动端点击命中率是否稳定、容器宽度从 300 拉到 1200 时瓦片是否跟随重绘。这四项过了,再谈样式和交互细节。
这套东西我是被坑出来的——早几年做一个轨迹回放页面,每次切换设备都重建地图,测试同学连续点了二十次之后页面直接白屏,最后定位到是实例没销毁。从那以后我每次封地图组件,第一件事就是写好destroy,并且在页面卸载时强制走一遍,不管看起来有没有必要。希望帮到你。
本文还有配套的精品资源,点击获取