戰(zhàn):定位選點(diǎn)與逆地理編碼)
簡介這份資源是面向微信小程序開發(fā)者的百度地圖 JavaScript API 接口包適合需要在輕量級小程序中嵌入地圖能力的初中級前端開發(fā)者用于解決地圖展示、定位、路徑規(guī)劃與地點(diǎn)檢索等常見需求。壓縮包共35個文件約28KB以js腳本、wxss樣式、json配置、wxml模板和png圖片為主其中bmap-wx.js與壓縮版構(gòu)成核心調(diào)用邏輯配套的app.json、app.js及pages目錄則給出可直接參考的示例工程結(jié)構(gòu)另有README.md說明文檔輔助上手。目前已有88人學(xué)習(xí)下載。借助包內(nèi)接口開發(fā)者可在小程序內(nèi)實(shí)現(xiàn)標(biāo)記點(diǎn)、線段、多邊形等覆蓋物繪制以及駕車導(dǎo)航、公交查詢、周邊檢索和實(shí)時位置分享等進(jìn)階功能配合官方文檔中的參數(shù)定義與調(diào)用示例能較快完成從環(huán)境引入到功能落地的過程為旅游、社交、打車等場景提供穩(wěn)定的地理位置服務(wù)支撐。1. 百度地圖微信小程序 JSAPI 接入從零跑通一個能定位、能選點(diǎn)的地圖頁很多團(tuán)隊(duì)第一次做微信小程序地圖都會卡在同一個地方小程序原生map組件能顯示地圖但一旦要做「拖動選點(diǎn)、逆地理編碼、周邊搜索、路線規(guī)劃」原生組件的能力就不夠用了。這時候百度地圖微信小程序 JSAPI 就成了一個繞不開的選項(xiàng)——它把 Web 端那套成熟的定位、檢索、算路能力封裝成小程序能直接調(diào)用的 SDK你不需要自己搭后端轉(zhuǎn)發(fā)也不用去啃各家坐標(biāo)系轉(zhuǎn)換的細(xì)節(jié)。這篇筆記講的就是怎么把這個 JSAPI 真正落地從申請密鑰、配置合法域名到封裝一個可復(fù)用的地圖工具模塊再到逆地理編碼、POI 檢索、路線規(guī)劃這幾個高頻場景的完整代碼。適合兩類人一類是剛接手小程序地圖需求、想快速跑通最小可用版本的開發(fā)者另一類是已經(jīng)用了原生組件、但被「選點(diǎn)精度」「地址回填」折磨過、想換一套更完整方案的人。下面所有代碼都是可以直接抄進(jìn)項(xiàng)目里改參數(shù)用的坑我也一并標(biāo)出來。2. 接入前的三件事密鑰、域名與坐標(biāo)系2.1 為什么不能直接在小程序里用 Web 版 JSAPI百度地圖 Web 端的 JSAPI 依賴window、document這些瀏覽器對象而微信小程序的邏輯層運(yùn)行在一個沒有 DOM 的 JS 引擎里兩者根本不是一套運(yùn)行環(huán)境。所以官方單獨(dú)出了一套小程序版 SDK它把地圖渲染交給小程序的map組件把定位、檢索、算路這些能力做成純 JS 接口通過request走 HTTPS 請求百度地圖的服務(wù)端。這里有個關(guān)鍵認(rèn)知小程序版 JSAPI 并不是「把地圖畫出來」的庫地圖渲染始終是小程序原生map組件干的活SDK 負(fù)責(zé)的是「數(shù)據(jù)」——坐標(biāo)、地址、POI、路線。理解這一點(diǎn)后面很多設(shè)計(jì)就順了你要在map組件上疊加標(biāo)記標(biāo)記的坐標(biāo)得由 SDK 算出來你要做選點(diǎn)得監(jiān)聽map的regionchange事件再把中心點(diǎn)坐標(biāo)丟給 SDK 做逆地理編碼。常見做法是新建一個libs目錄放 SDK 文件再建一個utils目錄放自己的封裝。目錄結(jié)構(gòu)大概長這樣project/ ├── libs/ │ └── bmap-wx.js // 百度地圖小程序 SDK ├── utils/ │ └── mapHelper.js // 自己的封裝 ├── pages/ │ └── map/ │ ├── map.js │ ├── map.wxml │ └── map.json2.2 申請密鑰與配置 request 合法域名第一步是去百度地圖開放平臺創(chuàng)建應(yīng)用應(yīng)用類型選「微信小程序」填上小程序的 AppID拿到一個 AKAccess Key。這個 AK 是后面所有接口的通行證泄露了別人就能拿你的額度刷請求所以別硬編碼在會提交到公開倉庫的文件里。拿到 AK 之后微信這邊還要放行請求域名。小程序的網(wǎng)絡(luò)請求有白名單機(jī)制沒配的域名一律請求失敗。百度地圖小程序 SDK 主要走這兩個域名用途域名主服務(wù)接口https://api.map.baidu.com部分靜態(tài)資源https://maponline0.bdimg.com配置位置在微信公眾平臺后臺「開發(fā)管理 → 開發(fā)設(shè)置 → 服務(wù)器域名 → request 合法域名」把上面兩個加進(jìn)去。注意必須用 HTTPS且不能帶路徑只填到域名。提示開發(fā)階段可以在微信開發(fā)者工具里勾選「不校驗(yàn)合法域名」但上線前一定要配好否則真機(jī)上直接請求失敗而且報錯信息很含糊容易誤判成 AK 問題。2.3 坐標(biāo)系這件事繞不過去國內(nèi)地圖服務(wù)涉及三套坐標(biāo)系GPS 原始坐標(biāo)WGS84、國測局加密坐標(biāo)GCJ02、百度自有坐標(biāo)BD09。微信小程序的wx.getLocation默認(rèn)返回的是 GCJ02而百度地圖 SDK 內(nèi)部用的是 BD09。如果你直接把wx.getLocation的經(jīng)緯度丟給百度 SDK 去標(biāo)點(diǎn)位置會偏幾百米——這就是很多人遇到的「定位點(diǎn)飄到馬路對面」的玄學(xué)問題。解決辦法有兩個一是調(diào)用 SDK 提供的坐標(biāo)轉(zhuǎn)換接口把 GCJ02 轉(zhuǎn)成 BD09二是讓 SDK 自己去做轉(zhuǎn)換。我一般會在封裝層統(tǒng)一處理所有外部傳入的坐標(biāo)先轉(zhuǎn)成 BD09 再進(jìn) SDK避免每個調(diào)用點(diǎn)都記一遍。轉(zhuǎn)換接口后面會給代碼。3. 封裝一個可復(fù)用的地圖工具模塊3.1 初始化 SDK 與全局單例SDK 初始化只需要一次重復(fù)初始化會浪費(fèi)內(nèi)存也可能導(dǎo)致回調(diào)錯亂。我習(xí)慣在mapHelper.js里做一個惰性單例第一次調(diào)用時才創(chuàng)建實(shí)例。// utils/mapHelper.js const BMap require(../libs/bmap-wx.js); let instance null; /** * 獲取 SDK 單例 * param {string} ak 百度地圖 AK * returns {object} SDK 實(shí)例 */ function getInstance(ak) { if (!instance) { instance new BMap.BMapWX({ ak: ak }); } return instance; } module.exports { getInstance };邏輯說明BMapWX是 SDK 暴露的構(gòu)造函數(shù)傳入ak后返回的實(shí)例上掛著geocoder、search、suggestion等能力對象。用單例是因?yàn)檫@些能力對象內(nèi)部會維護(hù)一些狀態(tài)多次 new 出來各自為政回調(diào)容易串。參數(shù)只有一個ak從配置里讀不要寫死。3.2 逆地理編碼把坐標(biāo)變成人能看懂的地址逆地理編碼是選點(diǎn)場景的核心用戶拖動地圖中心點(diǎn)坐標(biāo)一直在變你要實(shí)時把這個坐標(biāo)翻譯成「XX 路 XX 號」這樣的地址回填到輸入框。// utils/mapHelper.js 續(xù) const BMap require(../libs/bmap-wx.js); /** * 逆地理編碼坐標(biāo) - 地址 * param {object} sdk SDK 實(shí)例 * param {number} latitude 緯度BD09 * param {number} longitude 經(jīng)度BD09 * returns {Promiseobject} 地址信息 */ function reverseGeocode(sdk, latitude, longitude) { return new Promise((resolve, reject) { sdk.geocoder({ location: ${latitude},${longitude}, success(res) { // res 里包含 formatted_address、business 等字段 resolve(res); }, fail(err) { reject(err); } }); }); } module.exports { getInstance, reverseGeocode };邏輯說明geocoder接收的location是「緯度,經(jīng)度」的字符串順序不能反反了會返回空結(jié)果或者錯誤地址。success回調(diào)里的res結(jié)構(gòu)大致是{ formatted_address, business, addressComponent }其中formatted_address是完整地址addressComponent里有省市區(qū)街道分級信息做地址回填時通常用formatted_address就夠。參數(shù)上唯一要注意的是坐標(biāo)必須是 BD09。如果你手上是wx.getLocation拿到的 GCJ02得先轉(zhuǎn)。轉(zhuǎn)換接口在 SDK 里叫convertor用法是傳from、to和坐標(biāo)數(shù)組這里不展開思路是統(tǒng)一在調(diào)用reverseGeocode之前轉(zhuǎn)好。3.3 監(jiān)聽地圖拖動做防抖選點(diǎn)頁最怕的就是用戶每拖一下都發(fā)一次請求既費(fèi)額度又卡。正確做法是監(jiān)聽map組件的regionchange事件等用戶停下來再請求。// pages/map/map.js const mapHelper require(../../utils/mapHelper.js); const AK 你的AK; Page({ data: { latitude: 39.915, longitude: 116.404, address: , markers: [] }, onLoad() { this.sdk mapHelper.getInstance(AK); this.timer null; }, // 地圖區(qū)域變化 onRegionChange(e) { // 只在拖動結(jié)束時處理避免過程中頻繁觸發(fā) if (e.type ! end) return; // 防抖300ms 內(nèi)只發(fā)一次 clearTimeout(this.timer); this.timer setTimeout(() { this.fetchCenterAddress(); }, 300); }, // 獲取地圖中心點(diǎn)并逆地理編碼 fetchCenterAddress() { const mapCtx wx.createMapContext(myMap); mapCtx.getCenterLocation({ success: (res) { mapHelper.reverseGeocode(this.sdk, res.latitude, res.longitude) .then((data) { this.setData({ latitude: res.latitude, longitude: res.longitude, address: data.formatted_address }); }) .catch((err) { console.error(逆地理編碼失敗, err); }); } }); } });邏輯說明regionchange事件的type有begin和end兩種只在end時處理能省掉大量中間態(tài)請求。getCenterLocation是小程序原生MapContext的方法拿到的坐標(biāo)是 GCJ02嚴(yán)格來說這里應(yīng)該先轉(zhuǎn) BD09 再傳給reverseGeocode示例里為了突出主流程省略了轉(zhuǎn)換實(shí)際項(xiàng)目務(wù)必補(bǔ)上。防抖時間 300ms 是個經(jīng)驗(yàn)值太短用戶還沒停穩(wěn)就發(fā)請求太長地址回填有明顯延遲感。如果地圖上還要顯示標(biāo)記可以在setData里同步更新markers標(biāo)記坐標(biāo)用同一個中心點(diǎn)即可。4. POI 檢索與路線規(guī)劃把「附近」和「怎么走」做出來4.1 周邊檢索找附近的加油站、餐廳POI 檢索分兩種一種是以某個坐標(biāo)為中心搜周邊一種是按關(guān)鍵詞搜全城。選點(diǎn)場景常用前者。// utils/mapHelper.js 續(xù) /** * 周邊 POI 檢索 * param {object} sdk SDK 實(shí)例 * param {string} keyword 關(guān)鍵詞如「加油站」 * param {number} latitude 中心點(diǎn)緯度 * param {number} longitude 中心點(diǎn)經(jīng)度 * param {number} radius 搜索半徑單位米 * returns {PromiseArray} POI 列表 */ function searchNearby(sdk, keyword, latitude, longitude, radius 1000) { return new Promise((resolve, reject) { sdk.search({ query: keyword, location: ${latitude},${longitude}, radius: radius, success(res) { // res 里 originalData 才是真正的 POI 數(shù)組 resolve(res.originalData || []); }, fail(err) { reject(err); } }); }); }邏輯說明search的query是關(guān)鍵詞location同樣是「緯度,經(jīng)度」radius是半徑。返回結(jié)構(gòu)里有個坑res本身不是數(shù)組真正的 POI 列表在res.originalData里每一項(xiàng)包含title、address、location含lat、lng等字段。很多人第一次用直接遍歷res會拿到一堆 undefined就是沒注意這層包裝。半徑參數(shù)別設(shè)太大超過 5000 米結(jié)果會稀疏且不準(zhǔn)一般 1000 到 3000 米比較合適。如果要做分頁SDK 的search支持page和pageCapacity但小程序場景下用戶很少翻很多頁通常取前 20 條就夠。4.2 路線規(guī)劃駕車、步行、騎行路線規(guī)劃接口返回的是路徑點(diǎn)序列你要自己把它畫到map組件的polyline上。// utils/mapHelper.js 續(xù) /** * 駕車路線規(guī)劃 * param {object} sdk SDK 實(shí)例 * param {object} from {latitude, longitude} * param {object} to {latitude, longitude} * returns {Promiseobject} 路線數(shù)據(jù) */ function drivingRoute(sdk, from, to) { return new Promise((resolve, reject) { sdk.driving({ from: ${from.latitude},${from.longitude}, to: ${to.latitude},${to.longitude}, success(res) { resolve(res); }, fail(err) { reject(err); } }); }); }邏輯說明driving的from和to都是「緯度,經(jīng)度」字符串。返回的res里通常有routes數(shù)組每條路線包含steps每個 step 里有path點(diǎn)序列。要把路線畫出來需要把這些點(diǎn)抽出來拼成一個坐標(biāo)數(shù)組再轉(zhuǎn)成polyline需要的格式// 把路線數(shù)據(jù)轉(zhuǎn)成 polyline 需要的點(diǎn)數(shù)組 function routeToPolyline(routeData) { const points []; const routes routeData.routes || []; if (routes.length 0) return points; routes[0].steps.forEach((step) { step.path.forEach((p) { points.push({ latitude: p.lat, longitude: p.lng }); }); }); return points; }參數(shù)上driving還支持waypoints途經(jīng)點(diǎn)和policy算路策略比如避開高速。步行和騎行分別是walking和riding用法一致。注意路線規(guī)劃返回的點(diǎn)也是 BD09直接喂給polyline即可不用再轉(zhuǎn)。4.3 把檢索結(jié)果標(biāo)到地圖上拿到 POI 列表后要轉(zhuǎn)成map組件的markers格式// 把 POI 列表轉(zhuǎn)成 markers function poiToMarkers(poiList) { return poiList.map((poi, index) ({ id: index, latitude: poi.location.lat, longitude: poi.location.lng, title: poi.title, width: 24, height: 24, callout: { content: poi.title, display: BYCLICK, padding: 6, borderRadius: 4 } })); }邏輯說明markers每項(xiàng)必須有id、latitude、longitudeid要唯一用索引即可。callout是點(diǎn)擊標(biāo)記時彈出的氣泡display: BYCLICK表示點(diǎn)擊才顯示避免地圖上全是氣泡糊成一片。width和height控制圖標(biāo)大小不設(shè)的話默認(rèn)圖標(biāo)可能過大。5. 避坑與排查那些讓我返工三次的問題5.1 定位點(diǎn)偏移幾百米現(xiàn)象用wx.getLocation拿到的坐標(biāo)直接標(biāo)點(diǎn)位置明顯偏有時偏到河對岸。原因wx.getLocation返回 GCJ02百度 SDK 用 BD09兩套坐標(biāo)系不兼容。解決在調(diào)用任何百度接口前先用 SDK 的坐標(biāo)轉(zhuǎn)換能力把 GCJ02 轉(zhuǎn)成 BD09。如果懶得轉(zhuǎn)也可以在wx.getLocation里指定type: gcj02后自己寫轉(zhuǎn)換公式但更穩(wěn)的是用 SDK 自帶的轉(zhuǎn)換接口避免公式寫錯。5.2 逆地理編碼返回空地址現(xiàn)象geocoder的success回調(diào)里formatted_address是空字符串。原因八成是location的經(jīng)緯度順序?qū)懛戳嘶蛘邆髁?GCJ02 坐標(biāo)。百度接口對順序敏感緯度在前經(jīng)度在后。解決打印一下傳給geocoder的location字符串確認(rèn)是「緯度,經(jīng)度」且坐標(biāo)是 BD09。另外海上、荒漠等區(qū)域本身就沒有地址數(shù)據(jù)返回空是正常的要做兜底提示。5.3 真機(jī)上請求全部失敗開發(fā)者工具卻正?,F(xiàn)象開發(fā)者工具里一切正常真機(jī)預(yù)覽時所有地圖接口報錯。原因request 合法域名沒配或者配了但沒生效有緩存。解決去公眾平臺確認(rèn)api.map.baidu.com已加入白名單然后重新編譯、清緩存。真機(jī)調(diào)試時可以在vConsole里看具體報錯如果是url not in domain list就是域名問題。5.4 POI 檢索結(jié)果為空但關(guān)鍵詞沒錯現(xiàn)象搜「加油站」返回空數(shù)組換個詞又能搜到。原因radius設(shè)得太小或者中心點(diǎn)坐標(biāo)本身有問題比如傳了 GCJ02。解決先把radius調(diào)到 3000 試再確認(rèn)坐標(biāo)。另外百度對某些敏感或冷門關(guān)鍵詞會限流返回空不一定是代碼問題換個通用詞驗(yàn)證一下接口通不通。5.5 地圖拖動時頁面卡頓現(xiàn)象拖動地圖時掉幀地址回填延遲明顯。原因regionchange觸發(fā)太頻繁每次都setData大對象。解決只在type end時處理加防抖setData只更新變化的字段別整個data重設(shè)。如果標(biāo)記很多考慮用includePoints一次性調(diào)整視野而不是逐個更新。6. 進(jìn)階把地圖能力抽成獨(dú)立模塊的幾個技巧做到這里基本功能已經(jīng)能跑了。但如果你要在多個頁面復(fù)用或者團(tuán)隊(duì)里其他人也要用就得再往前走一步把地圖能力抽成一個不依賴具體頁面的模塊。我的習(xí)慣是定義一個MapService類把 AK、SDK 實(shí)例、常用方法都收進(jìn)去頁面只調(diào)方法不碰 SDK。這樣換 AK、加緩存、改超時都只改一個地方。下面是一個精簡版// utils/mapService.js const BMap require(../libs/bmap-wx.js); class MapService { constructor(ak) { this.sdk new BMap.BMapWX({ ak }); this.cache new Map(); // 簡單的地址緩存 } // 帶緩存的逆地理編碼 async geocode(lat, lng) { const key ${lat.toFixed(5)},${lng.toFixed(5)}; if (this.cache.has(key)) { return this.cache.get(key); } const result await new Promise((resolve, reject) { this.sdk.geocoder({ location: ${lat},${lng}, success: resolve, fail: reject }); }); this.cache.set(key, result); return result; } // 周邊搜索統(tǒng)一返回精簡結(jié)構(gòu) async nearby(keyword, lat, lng, radius 1000) { const res await new Promise((resolve, reject) { this.sdk.search({ query: keyword, location: ${lat},${lng}, radius, success: resolve, fail: reject }); }); return (res.originalData || []).map((poi) ({ title: poi.title, address: poi.address, latitude: poi.location.lat, longitude: poi.location.lng })); } } module.exports MapService;邏輯說明緩存用坐標(biāo)保留 5 位小數(shù)做 key因?yàn)樵倬_的差異對地址結(jié)果沒影響這樣能擋掉大量重復(fù)請求。nearby里把 SDK 的原始結(jié)構(gòu)轉(zhuǎn)成自定義的精簡結(jié)構(gòu)頁面就不用關(guān)心originalData這層包裝了。參數(shù)上radius給了默認(rèn)值調(diào)用方不傳也能工作。再補(bǔ)一個驗(yàn)證方法寫完模塊后別急著接頁面先在onLoad里跑一遍「定位 → 逆地理編碼 → 周邊搜索 → 算路」的完整鏈路把每步結(jié)果console.log出來。鏈路通了再往 UI 上接能省掉大量「到底是數(shù)據(jù)問題還是渲染問題」的排查時間。最后說個我踩過的坑早期我把 AK 直接寫在頁面 js 里后來要換 AK 得全局搜替換漏了一處就出詭異 bug?,F(xiàn)在一律走配置文件頁面只 import 模塊。這個習(xí)慣幫我省了不止一次后悔藥。地圖這塊接口多、坐標(biāo)系繞、真機(jī)環(huán)境又和工具不一樣能封一層就封一層別讓業(yè)務(wù)代碼直接裸奔在 SDK 上。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取