概述
随着头部地图平台纷纷收费,5 万一年的入场券额外增加了中小型企业的负担。最近在开发中遇到微信小程序关于地图更换的问题。
我们发现,微信小程序目前仅支持自家的 <map> 组件,使用它展示默认样式地图------底图加载、缩放、拖拽、markers 标记、polyline 路线、circle 圆形区域、显示用户当前定位红点、自定义 marker 图标、文字 label------这些基础渲染功能是不收费的 。真正收费的是逆地址解析 和 POI 搜索等 LBS 数据服务。如果已经购买了其他家 LBS 服务,再为平台的 API 额外付费显然不划算。
周末琢磨了一下,在 uni-app 平台上,完全可以把 map 组件只当作渲染层,LBS 数据服务全部走已经购买的服务商 API。
本文记录基于我们已购买的维智定位服务,实现的一套代码跑通 App、微信小程序、H5 三端的完整过程。
环境
- 框架:uni-app(Vue 2)
- 地图渲染 :App 端 / H5 端使用维智Web地图,小程序端使用微信原生
<map> - 定位 & LBS 服务:维智定位 SDK(逆地理编码、正地理编码、POI 搜索、附近搜索)
核心思路:地图是壳,LBS 是内核
arduino
┌──────────────────────────────────────┐
│ index.vue │
│ 按钮定义、业务逻辑(三端共用) │
└──────────┬───────────────────────────┘
│
┌───────┼───────┐
▼ ▼ ▼
App端 小程序 H5
│ │ │
│ <map> aimap-gl ← 只负责渲染(免费的)
│ 组件 SDK
│
└── 维智 SDK(逆地址/Poi/定位) ← LBS 数据(已购买)
关键认知:map 组件只是一个"画布",你在上面画 markers、画路线,它不收费。收费的是"这个坐标对应什么地址"、"搜索附近的餐厅"这类数据查询。把数据查询换成自己买的 LBS 供应商即可。
实现过程
1. 地图控制器抽象层
三端的地图操作方式完全不同------App 端通过 web-view 内的 evalJS 调用,小程序通过 uni-app 数据绑定,H5 直接操作 SDK 实例。如果不加抽象,业务代码里会充满条件编译。
我们设计一个统一的 MapController 接口:
js
// utils/map.js
export function createMapController(platform, options) {
switch (platform) {
case 'app-plus': return new AppMapController(options);
case 'mp-weixin': return new MiniMapController(options);
case 'h5': return new H5MapController(options);
}
}
三端实现同一套 API:
| 方法 | App端 | 小程序端 | H5端 |
|---|---|---|---|
getCenter() |
evalJS → postMessage | 读 data 属性 | map.getCenter() |
flyTo(center, zoom) |
evalJS | 修改绑定数据 | map.flyTo() |
addMarker(point) |
evalJS | push 到 markers 数组 | 直接调用 SDK |
setStyle(style) |
evalJS | 不支持 | map.setStyle() |
这样业务代码只跟 mapController 打交道,不关心平台差异。
2. App 端踩坑:web-view 原生层级问题
这是整个开发中耗时最长的一个坑。
现象:App 端(Android)上,所有 Vue 层写的按钮、提示框全部不显示,但地图正常。
尝试过的无效方案:
z-index: 9999--- 没用position: fixed--- 没用cover-view包裹 --- 没用(cover-view 能覆盖 video/map,覆盖不了 web-view)subNVue子窗口 --- 仍然被 web-view 盖住
根因 :uni-app 的 <web-view> 是原生子窗口(plus.webview.create),绘制层级是系统级别的,高于所有前端渲染层。任何 CSS 手段都无法跨越这个层级鸿沟。
最终方案 :放弃在 Vue 层写 UI,全部 UI 以 HTML 字符串形式通过 evalJS 注入到 web-view 内部去渲染。
js
// Vue 侧:构建 UI 并注入
_renderAppUI() {
var html = '<style>' + this._appStyles() + '</style>'
+ '<div class="location-info">...</div>'
+ '<div class="btn-group">...</div>';
this._appEvalJS("__mapSetUI('" + html.replace(/'/g, "\\'") + "')");
}
html
<!-- map_view.html:接收注入 -->
<div id="uiContainer" onclick="handleUIClick(event)"></div>
<script>
function __mapSetUI(html) {
document.getElementById('uiContainer').innerHTML = html;
}
// 事件代理,按钮点击回传给 Vue
function handleUIClick(e) {
var btn = e.target.closest('[data-id]');
if (btn) uni.postMessage({type:'buttonClick', id: btn.getAttribute('data-id')});
}
</script>
另一个小坑是 rpx 单位在 web-view 的普通 HTML 环境中不生效,需要手动转换:
js
_rpx(val) {
const info = uni.getSystemInfoSync();
return Math.round(val * info.screenWidth / 750);
}
3. 按钮统一定义
App 端按钮在 web-view 内渲染,小程序/H5 端通过 v-for 渲染。为了避免两套按钮定义,共用一份 buttons 数组:
js
buttons: [
{ id: 'singleClick', text: '单次定位' },
{ id: 'conitueClick', text: '连续定位' },
{ id: 'stopLocation', text: '停止定位' },
{ id: 'getAddress', text: '获取地址' },
{ id: 'getGeoCode', text: '正地理' },
{ id: 'poiSearch', text: 'poi 搜索' },
{ id: 'poiNearBySearch',text: 'poi 附近搜索' },
{ id: 'getMapCenter', text: '获取中心点' },
{ id: 'getMapBounds', text: '获取视野' },
{ id: 'toggleStyle', text: '暗黑地图' }
]
点击分发也只用一份 methodMap:
js
handleBtnClick(id) {
const methodMap = {
singleClick: this.singleClick,
conitueClick: this.conitueClick,
// ...
};
if (methodMap[id]) methodMap[id]();
}
App 端从 web-view 的 postMessage 回调走到同一个 handleBtnClick,小程序/H5 端从 @click 直接调用。
4. 逆地址解析
拿到坐标后,调用维智 SDK 的逆地理编码接口获取地址描述:
js
import * as wzLocation from '@/utils/wz_sdk_uniapp.js';
getAddress() {
wzLocation.setAk(LOCATION_KEY);
wzLocation.getReverseCode(107.191693, 27.945061, 'wgs84').then(
(res) => {
this.wzLocation.address.name = res.address.name;
this.mapController.updateLocation(this.wzLocation);
}
);
}
小程序端使用 wgs84 坐标系,与微信 map 组件一致,无需额外转换。
5. POI 搜索功能
关键词搜索和附近搜索同样走维智 SDK:
js
// 关键词搜索
poiSearch() {
wzLocation.setAk(POI_KEY);
wzLocation.poiSearch("泛悦城T2", '武汉市', 2, 20).then(res => {
// 跳转到第一个搜索结果
this.wzLocation.longitude = res[0].geoPoint.split(',')[0];
this.wzLocation.latitude = res[0].geoPoint.split(',')[1];
this.mapController.flyTo(this.wzLocation);
});
}
// 附近搜索
poiNearBySearch() {
wzLocation.setAk(POI_KEY);
wzLocation.poiNearbySearch("乒乓球", "体育场馆", '120.59986,31.25979').then(res => {
// 同上处理
});
}
搜索结果通过 mapController.addMarker 在地图上打点,位置也可以通过 mapController.flyTo 跳转。
6. H5 端特殊处理
H5 的导航栏默认会遮挡页面,在 pages.json 中配置隐藏:
json
{
"pages": [{
"path": "pages/index/index",
"style": {
"h5": { "navigationStyle": "custom" }
}
}]
}
另外移动端浏览器的地址栏会影响 100vh 的计算,需要配合 position: fixed 和安全区适配:
css
/* #ifdef H5 */
.content { position: fixed !important; top: 0; left: 0; width: 100%; height: 100%; }
.location-info { top: calc(30rpx + env(safe-area-inset-top)) !important; }
/* #endif */
项目地址
后话
我们买的是维智的LBS服务。如果你用的是高德或百度的LBS服务,把上面逆地址解析和POI搜索换成对应平台的API 即可------比如高德的 GET /v3/geocode/regeo 做逆地址,GET /v3/place/text 做 POI 搜索。核心思路不变:map 组件只负责渲染,LBS数据走自己的供应商。
改造过程中有些小坑值得注意:
- web-view 原生层级是最大的坑,App 端 UI 必须注入到 web-view 内部
- rpx 单位在 web-view 的 HTML 环境里无效,需要手动转 px
- evalJS 时序不可靠,web-view 内部初始化逻辑要自给自足,evalJS 只做增强不做依赖
- 微信 map 不支持运行时切换暗黑主题,需要提前在
pages.json用条件编译处理
有问题欢迎留言交流。