笔记:UniApp 项目地图选型与跨端迁移方案
适用项目:基于 uni-app 的 App(Android/iOS/鸿蒙),需同时覆盖国内及海外地图服务。
1. 核心需求
- 国内地图 :需要满足商用免费,并支持地理编码、逆地理编码、搜索、定位、缩放、行政区划分级查询。
- 海外地图:覆盖 Google 地图。
- 跨端:App 需运行在 Android、iOS、鸿蒙系统上。
- 迁移基础:已实现基于 Web 端的天地图和 Google 地图开发。
2. 关键结论
| 问题 | 结论与选型 |
|---|---|
| 商用免费的国内地图选哪个? | 天地图是高德/百度/腾讯之外,唯一可满足复杂功能且商用免费的平台。 |
| Web 端地图如何迁移到 App 端? | 采用 WebView 嵌入方式,将现有 H5 地图页面嵌入 uni-app,改动最小,复用度最高。 |
| 鸿蒙系统是否支持? | 支持。WebView 是官方推荐的 Google 地图集成方案,天地图已有成熟实践。 |
| 坐标系偏移问题如何解决? | 天地图(WGS-84)与高德(GCJ-02)坐标互传会产生偏移。需统一坐标系,或使用 coordtransform 等库进行转换。 |
| Google 地图在鸿蒙上的限制? | HarmonyOS NEXT 不再支持 GMS,WebView 是绕开限制的主要方式。 |
| uni-app vs uni-app x 怎么选? | 当前选用 uni-app 即可满足需求。若未来追求极致性能,可考虑迁移至 uni-app x。 |
3. 功能与费用对比
| 地图平台 | 商用授权费用 | 关键说明 |
|---|---|---|
| 天地图 | 免费 | 官方平台,支持 Web API,App 端需通过 WebView 集成。 |
| 高德地图 | 约 5 万元/年 | 商业授权收费,需事先购买。 |
| 百度地图 | 约 5 万元/年 | 同上。 |
| 腾讯位置服务 | 约 5 万元/年 | 同上。 |
| Google Maps | 超免费额度后按量计费 | 每月有 10,000 次免费调用,超出后自动计费。 |
4. 技术实现路径
4.1 App 端地图集成方案(推荐)
- 方案 :使用 uni-app 的
<web-view>组件加载包含天地图或 Google 地图的 H5 页面。 - 优势 :
- 直接复用现有 Web 端地图代码,无需重写。
- 绕开鸿蒙系统不支持 GMS 的限制。
- 天地图已有成熟插件(如
@geek-fun/uni-map)采用该方式。
- 待解决问题 :
- 通信 :需实现 App 与 WebView 的双向通信。
- Web → App:使用
uni.postMessage+@message事件。 - App → Web:使用
evalJS或uni.createWebviewContext(鸿蒙端)。
- Web → App:使用
- 平台差异 :鸿蒙端不支持
plus.webview,需使用uni.createWebviewContext替代。 - 权限配置 :鸿蒙端需在配置中声明定位权限(如
ohos.permission.APPROXIMATELY_LOCATION)。
- 通信 :需实现 App 与 WebView 的双向通信。
4.2 坐标系转换
- 问题 :天地图/Google 使用 WGS-84 坐标,高德/百度使用 GCJ-02 坐标。
- 方案:若 App 内混用不同坐标系的地图,需在数据传递时进行转换。
- 工具 :可使用
coordtransform库进行坐标转换。
4.3 未来性能优化路径
- 若后续对 App 性能(如启动速度、地图交互流畅度)有更高要求,可考虑迁移至 uni-app x,将代码编译为各平台原生语言,直接调用原生地图能力。
5. 关键配置项(鸿蒙平台)
-
权限声明 :在鸿蒙配置文件中添加:
json{ "permissions": ["ohos.permission.APPROXIMATELY_LOCATION"] } -
WebView 设置 :确保开启
domStorageAccess(true)并声明INTERNET权限,避免页面白屏或无法加载。
6. 注意事项
- 坐标一致性 :尽量统一全系统使用 WGS-84 坐标存储,仅在展示时根据地图底图动态转换。
- 桥接文件版本 :鸿蒙端使用
uni.webview.js时,建议使用 1.5.7 或更高版本 ,以支持harmony平台标识。 - 付费问题:Google Maps 超出免费额度后会自动扣费,建议设置预算预警。
7. 参考资料与链接
8.代码模板
模板一:WebView 双向通信(App ↔ H5 地图页面)
1. App 端页面(pages/map/map.vue)
vue
<template>
<view class="map-container">
<!-- #ifdef APP-PLUS -->
<web-view
id="map-webview"
:src="webViewSrc"
@message="handleWebViewMessage"
></web-view>
<!-- #endif -->
<!-- #ifdef H5 -->
<!-- H5 端直接渲染地图(非 WebView 方式) -->
<div id="h5-map-container"></div>
<!-- #endif -->
</view>
</template>
<script>
export default {
data() {
return {
// WebView 加载的地图页面路径(放在 hybrid/html/ 目录下)
webViewSrc: '/hybrid/html/map.html'
}
},
onReady() {
// #ifdef APP-PLUS
// 获取 WebView 实例(用于 App → Web 通信)
this.webViewContext = plus.webview.currentWebview().children()[0]
// #endif
},
methods: {
/**
* 接收 WebView 发来的消息(Web → App)
*/
handleWebViewMessage(event) {
const data = event.detail.data
console.log('收到 WebView 消息:', data)
// 根据消息类型做不同处理
switch(data.type) {
case 'locationSelected':
// 用户在地图上选了点
this.handleLocationSelected(data.lat, data.lng)
break
case 'searchResult':
// 搜索结果
this.handleSearchResult(data.results)
break
case 'mapReady':
// 地图加载完成
uni.showToast({ title: '地图加载完成' })
break
default:
console.warn('未知消息类型:', data.type)
}
},
/**
* 向 WebView 发送指令(App → Web)
*/
sendToWebView(action, params = {}) {
// #ifdef APP-PLUS
if (this.webViewContext) {
const message = JSON.stringify({ action, params })
// 调用 WebView 内的全局函数 receiveFromApp
this.webViewContext.evalJS(`window.receiveFromApp(${message})`)
}
// #endif
},
/**
* 示例:从 App 定位后,让地图移动到指定位置
*/
moveMapTo(lat, lng) {
this.sendToWebView('moveTo', { lat, lng, zoom: 15 })
},
/**
* 示例:从 App 发起搜索
*/
searchMap(keyword) {
this.sendToWebView('search', { keyword })
},
handleLocationSelected(lat, lng) {
console.log('用户选点:', lat, lng)
// 可在此处进行后续业务处理
},
handleSearchResult(results) {
console.log('搜索结果:', results)
}
}
}
</script>
<style>
.map-container {
width: 100%;
height: 100vh;
}
</style>
2. WebView 内嵌地图页面(hybrid/html/map.html)
html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>地图</title>
<!-- ========== 引入地图 SDK(按需选择) ========== -->
<!-- 天地图(国内) -->
<!-- <script src="https://api.tianditu.gov.cn/api?v=4.0&tk=你的天地图key"></script> -->
<!-- Google Maps(海外) -->
<!-- <script src="https://maps.googleapis.com/maps/api/js?key=你的GoogleKey&callback=initMap" async defer></script> -->
<!-- ========== UniApp WebView 通信桥接(必须) ========== -->
<script src="https://webviewjs.dcloud.net.cn/uni.webview.1.5.7.js"></script>
<style>
* { margin: 0; padding: 0; }
html, body { width: 100%; height: 100%; overflow: hidden; }
#map-container { width: 100%; height: 100%; }
</style>
</head>
<body>
<div id="map-container"></div>
<script>
let map = null
let mapType = 'tianditu' // 'tianditu' 或 'google'
const TK = '你的天地图key' // 天地图 key
const GOOGLE_KEY = '你的GoogleKey' // Google key
/**
* ============================================
* 一、地图初始化
* ============================================
*/
function initMap() {
const container = document.getElementById('map-container')
if (mapType === 'tianditu') {
// ----- 天地图初始化 -----
// 天地图 4.0 版本
map = new T.Map(container, {
projection: 'EPSG:4326'
})
map.centerAndZoom(new T.LngLat(116.397, 39.908), 12)
// 添加控件
map.addControl(new T.Control.Zoom())
map.addControl(new T.Control.Scale())
} else if (mapType === 'google') {
// ----- Google Maps 初始化 -----
map = new google.maps.Map(container, {
center: { lat: 39.908, lng: 116.397 },
zoom: 12,
mapTypeId: 'roadmap'
})
}
// 地图加载完成后通知 App
sendToApp({ type: 'mapReady' })
// 绑定地图点击事件(示例:选点)
if (mapType === 'tianditu') {
map.addEventListener('click', function(e) {
const lngLat = e.lnglat
sendToApp({
type: 'locationSelected',
lat: lngLat.getLat(),
lng: lngLat.getLng()
})
})
} else if (mapType === 'google') {
map.addListener('click', function(e) {
sendToApp({
type: 'locationSelected',
lat: e.latLng.lat(),
lng: e.latLng.lng()
})
})
}
}
/**
* ============================================
* 二、Web → App:发送消息
* ============================================
*/
function sendToApp(data) {
// 使用 UniApp 官方桥接方法
if (window.uni && uni.postMessage) {
uni.postMessage({
data: data
})
} else {
// 降级方案:直接调用 App 注入的全局方法(某些场景)
console.log('sendToApp:', data)
}
}
/**
* ============================================
* 三、App → Web:接收指令
* ============================================
*/
window.receiveFromApp = function(message) {
console.log('收到 App 指令:', message)
try {
const data = typeof message === 'string' ? JSON.parse(message) : message
const { action, params } = data
switch(action) {
case 'moveTo':
moveMapTo(params.lat, params.lng, params.zoom)
break
case 'search':
searchMap(params.keyword)
break
case 'setZoom':
setZoom(params.level)
break
default:
console.warn('未知指令:', action)
}
} catch(e) {
console.error('解析 App 指令失败:', e)
}
}
/**
* ============================================
* 四、地图操作函数(App 可调用的能力)
* ============================================
*/
function moveMapTo(lat, lng, zoom = 14) {
if (!map) return
if (mapType === 'tianditu') {
map.panTo(new T.LngLat(lng, lat))
if (zoom) map.setZoom(zoom)
} else if (mapType === 'google') {
map.panTo({ lat, lng })
if (zoom) map.setZoom(zoom)
}
}
function searchMap(keyword) {
if (!keyword) return
// 这里调用地图的搜索 API(示例仅做占位)
// 实际开发中可调用天地图/Google 的 POI 搜索接口
sendToApp({
type: 'searchResult',
keyword: keyword,
results: [] // 实际搜索结果
})
}
function setZoom(level) {
if (!map) return
if (mapType === 'tianditu') {
map.setZoom(level)
} else if (mapType === 'google') {
map.setZoom(level)
}
}
/**
* ============================================
* 五、启动地图
* ============================================
*/
// 根据环境加载对应地图 SDK(如需动态加载)
function loadMapSDK() {
// 判断当前环境(通过 UA 或 URL 参数)
const ua = navigator.userAgent
// 简单判断:如果 UA 包含 'Android' 或 'iPhone' 且在 App WebView 内
// 实际可通过 URL 参数 ?platform=app 来区分
// 默认使用天地图
mapType = 'tianditu'
// 加载完毕后初始化
if (document.readyState === 'complete') {
initMap()
} else {
window.addEventListener('load', initMap)
}
}
// 启动
loadMapSDK()
</script>
</body>
</html>
模板二:条件编译配置(天地图 ↔ Google 地图)
1. 使用条件编译区分平台(pages/map/map.vue 中)
vue
<template>
<view class="map-wrapper">
<!-- ========== H5 端:直接渲染 ========== -->
<!-- #ifdef H5 -->
<view id="h5-map" style="width:100%;height:100%;"></view>
<!-- #endif -->
<!-- ========== App 端:使用 WebView 加载 ========== -->
<!-- #ifdef APP-PLUS -->
<web-view
id="map-webview"
:src="appMapUrl"
@message="handleMapMessage"
></web-view>
<!-- #endif -->
<!-- ========== 小程序端(如有需要) ========== -->
<!-- #ifdef MP-WEIXIN -->
<map
:latitude="latitude"
:longitude="longitude"
scale="14"
></map>
<!-- #endif -->
</view>
</template>
<script>
export default {
data() {
return {
// #ifdef APP-PLUS
// App 端:根据国内/海外动态选择 WebView 加载的页面
appMapUrl: '/hybrid/html/map.html',
// #endif
// #ifdef H5
mapInstance: null,
// #endif
latitude: 39.908,
longitude: 116.397,
// 判断国内/海外(示例:可根据用户 IP 或 App 定位国家码)
isDomestic: true
}
},
mounted() {
// #ifdef H5
this.initH5Map()
// #endif
// #ifdef APP-PLUS
// App 端通过 URL 参数传递环境标识给 WebView
this.appMapUrl = `/hybrid/html/map.html?env=${this.isDomestic ? 'domestic' : 'overseas'}`
// #endif
},
methods: {
// ========== H5 端地图初始化(直接使用 Web API) ==========
// #ifdef H5
initH5Map() {
// 天地图或 Google Maps 的 Web API 初始化
// 与普通 Web 开发一致,此处略
console.log('H5 端初始化地图')
},
// #endif
// ========== App 端 WebView 消息处理 ==========
// #ifdef APP-PLUS
handleMapMessage(event) {
const data = event.detail.data
console.log('App 收到地图消息:', data)
// 处理地图回传的数据...
},
// #endif
// ========== 切换国内/海外地图(示例) ==========
switchMapRegion(isDomestic) {
this.isDomestic = isDomestic
// #ifdef APP-PLUS
// 重新加载 WebView(简单方式)
this.appMapUrl = `/hybrid/html/map.html?env=${isDomestic ? 'domestic' : 'overseas'}`
// #endif
// #ifdef H5
// H5 端重新初始化地图...
// #endif
}
}
}
</script>
<style>
.map-wrapper {
width: 100%;
height: 100vh;
}
</style>
2. 在 hybrid/html/map.html 中根据参数切换地图
html
<!-- hybrid/html/map.html 中增加以下逻辑 -->
<script>
// 解析 URL 参数,判断使用哪种地图
function getUrlParam(name) {
const params = new URLSearchParams(window.location.search)
return params.get(name)
}
const env = getUrlParam('env') || 'domestic' // 默认国内
if (env === 'domestic') {
// 使用天地图
mapType = 'tianditu'
loadTianDiTu()
} else {
// 使用 Google Maps
mapType = 'google'
loadGoogleMaps()
}
function loadTianDiTu() {
// 动态加载天地图 SDK
const script = document.createElement('script')
script.src = `https://api.tianditu.gov.cn/api?v=4.0&tk=${TK}`
script.onload = initMap
document.head.appendChild(script)
}
function loadGoogleMaps() {
// 动态加载 Google Maps SDK
const script = document.createElement('script')
script.src = `https://maps.googleapis.com/maps/api/js?key=${GOOGLE_KEY}&callback=initMap`
script.async = true
script.defer = true
document.head.appendChild(script)
}
</script>
使用说明
| 文件 | 位置 | 作用 |
|---|---|---|
map.vue |
pages/map/map.vue |
App 端主页面,承载 WebView 并处理通信 |
map.html |
hybrid/html/map.html |
WebView 内嵌的地图页面,包含天地图/Google 地图逻辑 |
| 条件编译 | 在 map.vue 中使用 #ifdef H5 / #ifdef APP-PLUS |
区分 H5 和 App 端的不同实现 |
关键点提醒
- 鸿蒙平台通信 :使用
uni.createWebviewContext替代plus.webview(模板中已预留) - 坐标统一 :WebView 内使用的坐标建议统一为 WGS-84,与天地图/Google 保持一致
- 安全性 :地图 Key 建议放在服务端或使用环境变量,不要写死在 HTML 中