简介:面向微信小程序开发者的百度地图接口文件包,旨在解决在小程序环境中接入地图展示、地点检索、路径规划、实时定位等地理服务的问题。压缩包内共三十五个文件,主体为十个脚本接口文件,并配套六个页面结构文件、六个样式文件及六个配置文件,另含示例演示和说明文档,整体包体仅28KB,轻量易部署。文件包遵循微信小程序开发标准,开发者可依据文档快速实现覆盖物添加、缩放移动、路线导航及位置共享等常用功能,示例页面也为二次开发提供了可直接参考的交互模板。目前已有八十八人学习下载,适合希望为小程序快速添加地图能力的前端开发者或学生使用。与一般零散工具包不同,这一压缩包以主版本形式提供稳定代码,配合官方接口规范即可集成生产环境,省去自行封装底层接口的耗时。
1. 百度地图微信小程序 jsapi.zip:解压出来的是什么,怎么用得上
做小程序地图功能时,最省事的一步就是把百度地图微信小程序 jsapi.zip 拿到手。它里面封装的是百度地图官方为微信小程序场景裁剪过的 JavaScript API 库,常见文件是bmap-wx.min.js,附带示例页和说明文档。这个东西解决的是“小程序里怎么低成本接入地图能力”的落地问题:地图展示、定位、POI 搜索、路线规划,都能用几十行代码串起来,而不是自己在 WebView 里拼一套 H5 地图。适合正在做社区团购、家政服务这类带位置业务的小程序开发者,也适合从零开始接地图、不想被坐标系和底层渲染细节拖住的团队。下面我按接入、显示、检索、避坑和进阶的顺序,把整套流程拆开讲清楚。
2. 接入微信小程序:把 bmap-wx 放进工程,三处配置一次跑通
下载下来的 zip 是一个压缩包,不是直接解压就能跑的小程序完整工程。第一步要做的不是急着打开示例,而是理清楚哪些文件进代码库,哪些文件只是参考。
2.1 解压后该留哪个文件,别把整个包塞进项目
常见做法是在一个干净目录里解压,然后把bmap-wx.min.js复制到小程序的utils或lib目录。包里一般还有 README、示例页面和测试用的图片,这些都不需要进主工程。很多人在网上搜“zip解压”之后,直接整个目录拖进小程序,结果把示例页面、冗余图片一起打进了包,后面上传时提示超过 2MB 才回头来删,很被动。
还有人会搜“zip密码移除”,这里多提醒一句:官方渠道下载的百度地图小程序 JSAPI 压缩包通常没有密码,也不需要什么密码移除操作。如果解压时需要密码,说明这个 zip 可能是第三方二次转发的,路径不明,宁可重新去开放平台下载,也不要执行来路不明的解压工具。拿到的包尽量做一次文件列表检查,只保留跟地图库相关的文件,能省掉后面很多版本混乱的问题。
把bmap-wx.min.js放进工程后,最主要的工作是初始化。它不是直接 require 进来就能用的,需要你在 App 启动时创建一个 BMap 实例。
2.2 在 app.js 里初始化并挂到 globalData
看下面这段代码,这是最基础也最不容易出错的初始化方式:
// app.js const bmapLib = require('./utils/bmap-wx.min.js'); App({ globalData: { bmap: null }, onLaunch() { this.globalData.bmap = new bmapLib.BMap({ ak: '你在百度地图开放平台申请的密钥' }); } });这里bmapLib是 require 回来的模块对象,bmapLib.BMap才是构造函数。构造函数里唯一必填的就是ak,也就是百度地图开放平台里“微信小程序”类型应用的密钥。注意它和网页端 JSAPI 的 key 不是同一个东西,应用类型选错的话,后面所有请求都可能被拒。
把实例挂到globalData上的好处是全局只有一份,页面之间共用。不要在每个页面的onLoad里重新 new,这样既浪费内存,也容易让 key 的调用被限流。页面里需要用时写getApp().globalData.bmap或const bmap = getApp().globalData.bmap;就够了。
初始化只是第一步,真正让地图跑起来,还要处理三处配置:百度侧的 AppID 白名单、微信侧的服务器域名、小程序的定位权限声明。
2.3 三个硬性配置:appid、合法域名、定位权限声明
第一处:登录百度地图开放平台,找到这个应用的配置页,把微信小程序的 AppID 填进白名单。如果不填,真机上调用会报类似detail=jsapi has been banned的权限错误。
第二处:在微信公众平台的小程序后台,进入“开发管理-服务器域名”,把 request 合法域名加上。百度地图微信小程序 JSAPI 内部通过wx.request请求百度服务,常见域名是https://api.map.baidu.com。开发阶段可以在开发者工具里勾选“不校验合法域名、web-view、TLS 版本以及 HTTPS 证书”先跑通,但上线前必须把这一步补齐,否则真机上一片空白。
第三处:如果地图页要定位和展示当前位置,小程序需要在app.json里声明定位权限和隐私接口,否则真机上接口会静默失败。
{ "permission": { "scope.userLocation": { "desc": "用于在地图上显示你的位置和附近服务" } }, "requiredPrivateInfos": ["getLocation"] }这段配置里,desc是用户授权弹窗里展示的文案,尽量写得具体一点,比如“用于展示你当前的位置和附近门店”。如果后面还要用wx.chooseLocation选点,记得把chooseLocation也并进requiredPrivateInfos。这一步漏配置,往往是最难查的:开发者工具正常,真机一调用就fail,其实不是代码问题,而是权限声明没写。
这三个配置都完成之后,跑一个最简单的regeocode逆地理编码接口,一般就能通。如果还报错,先回来看ak是什么时候创建的、应用类型对不对,重生成一个 key 再试。
3. 地图显示与定位:map 组件和 JSAPI 各自负责什么
很多人会误以为装了百度地图 JSAPI,页面里就能直接出现百度底图。实际上在小程序生态里,底图渲染是微信原生<map>组件的活,百度 JSAPI 提供的是数据和计算服务。理解这个分工,后面才不会被层级和坐标系问题折腾。
3.1 页面里的 map 组件,数据来自 setData
先看一个最小可用的地图页面模板:
<map id="map" class="map" longitude="{{center.lng}}" latitude="{{center.lat}}" scale="{{scale}}" markers="{{markers}}" polyline="{{polyline}}" show-location="{{showLocation}}" ></map>组件的longitude和latitude控制中心点,scale是缩放级别,范围一般是 3 到 20,城市概览用 11 到 13,具体街区用 15 到 17。markers是标注点数组,polyline是路线折线,都能通过setData动态更新。
对应的页面数据长这样:
Page({ data: { center: { lng: 116.404, lat: 39.915 }, scale: 14, markers: [], polyline: [], showLocation: true }, onLoad() { const bmap = getApp().globalData.bmap; // 页面后续所有地图服务调用都基于这个实例 } });这组数据里没有直接使用bmap做渲染,bmap只负责去请求服务,比如反查地址、搜索 POI。页面拿到结果后,再通过setData把中心点、标记点、路线点喂给<map>。如果你发现地图空白但 JSAPI 请求都成功了,大概率是数据没有正确写给 map 组件,而不是地图服务没返回。
这里还要提一个高频问题:微信小程序顶部导航栏高度。地图页如果要全屏展示,不能直接把高度写成100vh,否则会被胶囊按钮、状态栏或底部 tabBar 挡掉一部分。常见做法是用wx.getWindowInfo()拿到整个窗口高度,再用wx.getMenuButtonBoundingClientRect()拿到右上角胶囊的位置,算出一个安全的地图高度。这样真机适配才不会出现地图底部被遮挡的情况。
3.2 用 regeocode 反查当前位置和城市
底图能显示以后,最常见的需求是“我在哪”。这个接口叫逆地理编码,作用是传入经纬度,返回地址描述和城市信息。
const bmap = getApp().globalData.bmap; function fetchAddress(lng, lat) { bmap.regeocode({ location: `${lng},${lat}`, success: (res) => { console.log('regeocode:', JSON.stringify(res)); this.setData({ address: res.address }); }, fail: (err) => { console.error('regeocode fail:', err); } }); }这里最关键的是location参数,它是字符串经度,纬度,逗号必须是英文逗号。很多人第一次调用时传对象{ lng: 116.404, lat: 39.915 },结果接口直接 fail。另外,百度地图不同版本返回结构不完全一样,旧教程里可能直接取res.address,但新版库里地址可能被包在res.result里。所以第一次跑通后,先在success里打印完整结果,确认字段再往下写。
逆地理编码经常和wx.getLocation配合:先拿定位坐标,再反查地址,然后在地图上方显示“当前城市:北京”。注意wx.getLocation需要用户授权,首次调用会弹窗,用户拒绝后要引导到设置页重新打开,否则后续页面一直拿不到坐标。
3.3 坐标系转换:为什么你看到的点“偏了”
这是百度地图接入里最绕、也最容易翻车的地方。微信小程序<map>组件默认用的是 GCJ-02 坐标系,也就是国内常见的“国测局坐标”。而百度地图的定位和地理编码结果,官方返回的是 BD-09 百度坐标系。两套坐标系之间存在偏移,直接混用,点位会偏几十米到几百米,具体取决于所在城市。
如果你是先通过wx.getLocation拿到坐标,这个坐标可以指定为type: 'gcj02',直接给<map>用没问题。但如果要把这个坐标传给百度 JSAPI 做逆地理编码或 POI 搜索,就需要把 GCJ-02 转成 BD-09。反过来,百度 JSAPI 返回的路线点和 POI 坐标,如果要展示到 map 组件上,又得从 BD-09 转回 GCJ-02。
下面这两个转换函数是经过检验的常用算法,我一般在项目里单独维护成一个utils/coord.js:
// utils/coord.js function gcj02ToBd09(lng, lat) { const xPi = (Math.PI * 3000) / 180; const x = lng; const y = lat; const z = Math.sqrt(x * x + y * y) + 0.00002 * Math.sin(y * xPi); const theta = Math.atan2(y, x) + 0.000003 * Math.cos(x * xPi); return { lng: z * Math.cos(theta) + 0.0065, lat: z * Math.sin(theta) + 0.006 }; } function bd09ToGcj02(lng, lat) { const xPi = (Math.PI * 3000) / 180; const x = lng - 0.0065; const y = lat - 0.006; const z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * xPi); const theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * xPi); return { lng: z * Math.cos(theta), lat: z * Math.sin(theta) }; } module.exports = { gcj02ToBd09, bd09ToGcj02 };建议把坐标转换收口到这一个文件里,所有页面都从这里取函数,不要在业务代码里各写一份近似公式。否则同一个坐标在列表页和地图页转换结果不一致,最后很难排查。我的血泪经验是:真机上 marker 偏移一条街,第一反应不是怀疑 GPS,而是先检查坐标系有没有统一。
4. POI 搜索与路线规划:把附近数据和路径画到地图上
底图、定位、坐标转换都通了,地图页基本能立起来。接下来真正有价值的业务功能,是“附近有什么”和“怎么去”。这两块分别对应 POI 搜索和路线规划。
4.1 search:附近 POI 搜索与 marker 渲染
做社区团购自提点、家政服务门店这类小程序时,搜索周边 POI 是核心功能。百度微信小程序 JSAPI 提供了search方法,基本写法如下:
const bmap = getApp().globalData.bmap; bmap.search({ query: '便利店', location: '116.404,39.915', radius: 3000, success: (res) => { console.log('search result:', JSON.stringify(res)); const pois = res.wxMarkerData || res.result || []; const markers = pois.map((item, index) => ({ id: index, latitude: item.latitude, longitude: item.longitude, title: item.title || item.name, iconPath: '/images/poi.png', width: 28, height: 28 })); this.setData({ markers }); }, fail: (err) => { console.error('search fail:', err); } });这里有几个参数值得单独说。query是搜索关键词,比如“便利店”“药店”“自提点”;location是中心点字符串,仍然是经度,纬度的格式;radius是搜索半径,单位是米,一般设 2000 到 5000 比较合适。搜索结果的返回结构在不同版本里不太一样,我见过wxMarkerData和result两种容器,所以上面做了一次兼容,但保险起见还是要先打印确认。
markers数组里的iconPath必须是小程序包内的本地路径,不能直接用网络图片。如果没做图标,map 组件会显示默认红钉,能用但不好看,最好准备一张 40x40 以内的 PNG,压缩一下再放进包。
点击 marker 展示详情,是小程序地图页几乎必做的交互。给<map>组件加一个事件绑定:
<map bindmarkertap="onMarkerTap" ...></map>onMarkerTap(e) { const id = e.detail.markerId; const target = this.data.markers.find((m) => m.id === id); if (target) { wx.showModal({ title: target.title, content: target.address || '暂无地址', showCancel: false }); } }事件回调里拿到的markerId就是我们在markers里设置的id,所以构造 marker 时不要偷懒,一定要给每个点设置唯一的自增 id。
4.2 路线规划:drivingRoute / walkingRoute 返回点抽稀
“怎么去”在代码层面分成两步:先用百度 JSAPI 算出路线折线点,再把这些点交给 map 组件的polyline画出来。驾车和步行的调用方式非常相近:
const bmap = getApp().globalData.bmap; bmap.drivingRoute({ origin: { lng: 116.404, lat: 39.915 }, destination: { lng: 116.414, lat: 39.925 }, success: (res) => { const routes = res.result && res.result.routes; if (!routes || !routes.length) { console.warn('no route found'); return; } const rawPoints = []; routes[0].steps.forEach((step) => { (step.points || []).forEach((p) => rawPoints.push(p)); }); this.setData({ polyline: [{ points: rawPoints.map((p) => bd09ToGcj02(p.lng, p.lat)), color: '#3388ff', width: 5, dottedLine: false }] }); }, fail: (err) => { console.error('route fail:', err); } });注意这里origin和destination是对象形式,而不是字符串,和前面search、regeocode的传参习惯不一样。路线规划返回的路线可能不止一条,默认取routes[0]即可。每个step里都有一组points,把这些点拼起来就是整条路线。
路线点往往非常密集,一条几公里的路线可能有好几百个点,直接全量传给polyline会造成渲染卡顿。我一般会做一次简单抽稀,每 3 到 5 个点取一个,既能保持路径形状,又能减少绘制压力:
function thinPoints(points, gap = 3) { const result = []; for (let i = 0; i < points.length; i += gap) { result.push(points[i]); } if (points.length % gap !== 0) { result.push(points[points.length - 1]); } return result; }抽稀后的点仍然需要先经过坐标转换。上面代码里用bd09ToGcj02把百度的 BD-09 坐标转成 GCJ-02,再交给polyline才能和底图对齐。如果忘记这一步,路线会和道路重叠不上,看起来像是在楼顶飞驰。
4.3 公交路线与“导航权限”的边界
公交路线用transitRoute也能调,但入参里通常要额外传城市信息,而且不同版本对城市字段的要求不一样,有的叫region,有的叫city。所以我建议第一次调用时先打印完整成功和失败回调,确认当前库到底认哪个字段,再写死。公交路线返回的换乘方案更复杂,展示到地图上时,分段渲染polyline的颜色也可以区分步行和公交路段。
关于导航,有一个很容易被热搜词带偏的误区:很多人搜“发起导航失败,请前往百度地图确认权限”,以为是微信小程序里可以直接唤起百度地图 App 导航。实际上,百度微信小程序 JSAPI 的重点是“地图数据和计算”,并不负责拉起百度地图 App。小程序里最稳妥的导航方案是wx.openLocation,它会在微信内置地图中展示目标点,用户自己选择用哪种方式导航。
wx.openLocation({ latitude: gcj02.lat, longitude: gcj02.lng, name: '目的地名称', address: '目的地地址', scale: 16 });这里传入的坐标也必须是 GCJ-02,也就是和 map 组件一致的坐标系。如果业务里出现“发起导航失败,请前往百度地图确认权限”一类的报错,除了检查ak权限,还要确认自己是不是用了与小程序不匹配的导航 SDK。我的习惯是:小程序端只负责把终点坐标算出来,调用wx.openLocation,把真正的导航交给微信自带能力,稳定也省心。
5. 避坑清单:接入 bmap-wx 最容易翻车的五个点
这套库整体不难,但接入过程中我见过太多人卡在同一批问题上。下面五个点按“现象、原因、解决”的顺序列出来,基本覆盖了 90% 的初次接入翻车场景。
5.1 真机地图空白,开发者工具正常
现象:开发者工具里地图和 marker 都正常,扫码到真机上一片空白,或者只有灰色网格。
原因:最常见的是合法域名没配。开发者工具里勾选了“不校验合法域名”后,真机调试不受这个开关控制,请求仍然会被拦截。其次是map组件的高度为 0,或者被父容器用overflow: hidden裁剪掉了。
解决:先到微信公众平台把百度地图请求域名加进 request 合法域名;再把 map 组件的高度设成明确的数值,比如600px或calc(100vh - 导航栏高度),不要依赖父容器自动撑开。真机预览时打开调试模式,确认 Network 面板里有地图瓦片请求,再往下排查。
5.2 点位偏移几十米到几百米
现象:POI 搜索出来的门店,在 map 组件上落到了马路对面,或者直接漂到隔壁街区。
原因:坐标系混用。百度接口返回的是 BD-09,map 组件用的是 GCJ-02。很多教程没有强调这一点,数据从百度接口拿回来直接塞进markers,自然偏移。
解决:在项目里统一维护utils/coord.js,所有从百度接口拿回来的坐标,在写入 map 组件前先调用bd09ToGcj02。反过来,如果要把wx.getLocation的坐标传给百度接口,先用gcj02ToBd09转一次。把转换逻辑收口到一个文件,不要在页面里各写一套。
5.3 报错 detail=jsapi has been banned / 10002
现象:调用search或regeocode时,fail 回调返回类似detail=jsapi has been banned的文本,或者错误码 10002。
原因:ak不合法,或者这个ak没有被授权使用微信小程序 JSAPI。常见误用是把网页端 JSAPI 的 key 直接拿过来,或者在百度开放平台忘记填小程序的 AppID 白名单。
解决:登录百度地图开放平台,确认应用类型是“微信小程序”,重新生成一个ak,替换app.js里的旧值。如果还报 10002,检查平台配置里填的 AppID 和当前小程序是不是同一个,然后等五到十分钟让配置生效再试。这种权限报错不是代码问题,改参数比重启项目有用得多。
5.4 source size 2612kb exceed max limit 2mb
现象:上传代码或真机预览时,控制台提示包体积超过 2MB,常见文案是source size 2612kb exceed max limit 2mb。
原因:bmap-wx 库本身并不大,很多人的主包里混进了 zip 里附带的示例页面、测试图片,或者整个解压目录都被复制进了项目。
解决:只保留bmap-wx.min.js,示例代码和图片一律不进主包。地图用到的 marker 图标压缩到几 KB,或者放到 CDN 用网络图片,注意网络图片在 marker 上不一定兼容,所以更稳妥的做法是本地放一张极小的图标。低频页面可以拆到分包,主包只放启动必要的代码。如果项目是用 uniapp 打包成微信小程序,也要检查打包配置有没有把不必要的静态资源打进了主包。
5.5 回调里拿不到想要的字段
现象:接口确实走了success,但res.wxMarkerData是undefined,或者res.address根本不存在。
原因:库版本和网上教程不一致,不同版本返回结构相差很大。网上很多代码是照着老版本写的,字段名带wxMarkerData,新版可能已经改成result或data。
解决:第一次调用任何接口,先在success回调里用console.log(JSON.stringify(res))把完整结构打印出来。这一步能破解掉回调结构这个“黑匣子”。另外注意回调里的this指向,用普通function嵌套会导致setData报错,建议统一用箭头函数,或者在外层先const that = this。替换库文件之前,先给当前能跑通的文件做个备份,这是给自己留的后悔药。
6. 进阶技巧:把 JSAPI 调用封装成 Promise 服务层
走到这一步,接入和排错基本没问题了。接下来值得做的事情,是把散落在各个页面的百度地图调用收拢到一个服务层里,让业务代码不再关心接口回调结构。
6.1 封装一个 bmap-service.js
我一般会在utils里建一个bmap-service.js,把常用接口包成 Promise:
// utils/bmap-service.js const getBMap = () => getApp().globalData.bmap; function regeocode(lng, lat) { return new Promise((resolve, reject) => { getBMap().regeocode({ location: `${lng},${lat}`, success: resolve, fail: reject }); }); } function searchPoi(query, lng, lat, radius = 2000) { return new Promise((resolve, reject) => { getBMap().search({ query, location: `${lng},${lat}`, radius, success: resolve, fail: reject }); }); } module.exports = { regeocode, searchPoi };页面里使用就变成:
const { regeocode, searchPoi } = require('../../utils/bmap-service.js'); Page({ async onLoad() { try { const res = await regeocode(116.404, 39.915); this.setData({ address: res.address }); } catch (err) { console.error('地址反查失败', err); } } });这样封装有几个直接的好处:页面代码不用每次处理回调风格;默认参数可以统一维护,比如radius只在一个地方改;Promise 的catch可以作为全项目的错误上报入口。注意getApp()不能在 App 实例初始化之前调用,但页面onLoad阶段已经晚于 ApponLaunch,所以没有问题。
6.2 真机验证三件套
封装完以后,最后讲一个我每次交付前都会做的验证流程。第一,真机调试时打开 vConsole,看 Network 面板里百度的请求是否命中、是否携带了正确的ak。第二,如果怀疑请求被拦截,用 Charles 抓包确认请求域名和返回码,能快速区分是域名配置问题还是密钥问题。第三,在展示层写一个小工具函数,所有传给<map>的经纬度都过一次坐标转换,并在开发环境打日志,一旦出现明显超范围的值就阻止渲染,避免黑屏。
地图功能看起来玄学,其实大部分问题都出在坐标系和权限配置上。我印象最深的一次是接了别人的项目,所有 marker 在真机上偏了一条街,排查到最后就是百度坐标直接塞给了 map 组件,改一个转换函数就好了。希望这份笔记能帮你把这块跑通,也少走这段冤枉路。
本文还有配套的精品资源,点击获取