《uni-app开发Harmony Next平台的App》第三篇:内置模块之地图与定位——集成腾讯地图

在这里插入图片描述

地图和定位,在鸿蒙上绕不开的坎

鸿蒙开发里,地图和定位是高频能力,几乎每个需要位置服务的App都绕不开。但很多人第一次接触这个能力时,会发现官方示例看起来很简单,但实际项目里跑起来并不顺利——地图加载不出来、定位始终返回错误码、页面跳转后经纬度全丢。

这个功能本身不复杂,真正难的是配置和调试环节。uni-app的跨平台特性虽然屏蔽了大量底层差异,但HarmonyOS NEXT的开发模式与Android/iOS有本质不同,地图组件通过WebView加载,协议也不是常见的http,这导致了很多开发者卡在配置环节。

下面直接展开讲,怎么在uni-app项目里把腾讯地图和定位功能跑通。

环境说明

HBuilderX 版本:HBuilderX 4.31 及以上
目标设备:HarmonyOS NEXT 真机(模拟器部分功能受限,建议真机测试)
地图服务:腾讯地图(当前HarmonyOS平台唯一支持的地图服务)

先搞懂它在解决什么问题

涉及哪些能力

功能 对应API/组件 适用场景 是否必须联网
展示地图 <map> 组件 位置展示、轨迹回放、区域标记
获取当前位置 uni.getLocation() 定位签到、附近搜索、获取经纬度
选择地点 uni.chooseLocation() 地址选择、收货地址填写
打开地图导航 uni.openLocation() 跳转到目的地、路线规划

为什么不支持高德或其他地图

这是HarmonyOS生态的一个现实限制。目前uni-app在鸿蒙平台只内置了腾讯地图的适配,高德等其他厂商还没有推出鸿蒙原生SDK。如果你想在鸿蒙上用高德的地图能力,那就得自己用Naitve API去对接鸿蒙的Map Kit,跟uni-app内置模块是两个路径。

配置腾讯地图Key

第一步:申请腾讯地图开发者Key

先去腾讯位置服务官网注册应用,申请Key。这一步跟Android/iOS上申请的Key不同之处在于:

  • HarmonyOS NEXT的页面加载使用的是file://https://混合协议
  • 域名白名单必须留空,否则地图组件无法正常加载

第二步:配置manifest.json

如果你是HBuilderX 4.31及以上版本,直接在可视化界面配置:

在这里插入图片描述

如果你习惯源码方式编辑,打开manifest.json,在app-plus -> distribute -> sdkConfigs下添加:

{
  "app-harmony": {
    "distribute": {
      "sdkConfigs": {
        "maps": {
          "qqmap": {
            "key": "你的腾讯地图Key"
          }
        }
      }
    }
  }
}

这里有个容易踩的坑:如果大版本不对,比如你用HBuilderX 4.26到4.30之间的版本,配置方式会不一样,必须按上面的方式手动添加。4.31之后直接在可视化界面操作就行。

第三步:配置HarmonyOS权限

harmony-configs/entry/src/main/module.json5里添加定位权限声明:

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.APPROXIMATELY_LOCATION",
        "reason": "用于获取模糊位置"
      },
      {
        "name": "ohos.permission.LOCATION",
        "reason": "用于获取精确位置",
        "usedScene": {
          "abilities": ["MainAbility"],
          "when": "foreground"
        }
      },
      {
        "name": "ohos.permission.LOCATION_IN_BACKGROUND",
        "reason": "后台定位需求(可选)",
        "usedScene": {
          "abilities": ["MainAbility"],
          "when": "always"
        }
      }
    ]
  }
}

权限这一块跟Android很相似,鸿蒙也分精确位置和模糊位置。如果不申请精确位置权限,uni.getLocationaltitude(海拔)数据会拿不到。

核心实现:完整页面示例

下面这个页面包含了三个核心功能:地图展示、获取当前位置、选择地点。

<!-- pages/map-demo.vue -->
<template>
  <view class="container">
    <!-- 地图组件 -->
    <map
      class="map"
      :longitude="centerLon"
      :latitude="centerLat"
      :markers="markers"
      :scale="scale"
      show-location
      @markertap="onMarkerTap"
    />
    
    <!-- 操作按钮区域 -->
    <view class="buttons">
      <button type="primary" @click="getMyLocation">获取当前位置</button>
      <button type="default" @click="openLocation">打开地图导航</button>
      <button type="warn" @click="chooseLocation">选择地点</button>
    </view>
    
    <!-- 位置信息展示 -->
    <view class="info" v-if="locationInfo">
      <text>经度:{{locationInfo.longitude}}</text>
      <text>纬度:{{locationInfo.latitude}}</text>
      <text v-if="locationInfo.name">地点名称:{{locationInfo.name}}</text>
    </view>
  </view>
</template>

<script setup>
import { ref } from 'vue'

// 地图中心点(默认北京天安门)
const centerLat = ref(39.90923)
const centerLon = ref(116.397428)
const scale = ref(16)

// 标记点
const markers = ref([
  {
    id: 1,
    latitude: 39.90923,
    longitude: 116.397428,
    iconPath: '/static/marker.png',
    width: 30,
    height: 30,
    title: '天安门',
    label: {
      content: '出发地点',
      color: '#333',
      fontSize: 14
    }
  }
])

// 位置信息
const locationInfo = ref(null)

/**
 * 获取当前位置 - 核心方法
 * 
 * 注意:这里没有使用 type: 'gcj02' 会导致坐标偏移
 * 腾讯地图使用 gcj02 坐标系,不指定的话返回原始坐标
 */
const getMyLocation = () => {
  uni.getLocation({
    type: 'gcj02',
    isHighAccuracy: true,
    highAccuracyTimeout: 3000,
    success: (res) => {
      console.log('定位成功', JSON.stringify(res))
      
      // 更新地图中心到定位位置
      centerLat.value = res.latitude
      centerLon.value = res.longitude
      
      // 更新标记点到当前位置
      markers.value = [{
        id: new Date().getTime(),
        latitude: res.latitude,
        longitude: res.longitude,
        iconPath: '/static/marker.png',
        width: 30,
        height: 30,
        title: '当前位置'
      }]
      
      locationInfo.value = res
    },
    fail: (err) => {
      console.error('定位失败', JSON.stringify(err))
      uni.showToast({
        title: '定位失败,请检查权限',
        icon: 'none'
      })
    }
  })
}

/**
 * 打开地图导航 - 使用uni.openLocation
 * 
 * 这个API会调起系统的导航页面
 * 在鸿蒙上是一个WebView加载的腾讯地图页面
 */
const openLocation = () => {
  if (!locationInfo.value) {
    uni.showToast({
      title: '请先获取当前位置',
      icon: 'none'
    })
    return
  }
  
  uni.openLocation({
    latitude: locationInfo.value.latitude,
    longitude: locationInfo.value.longitude,
    name: locationInfo.value.name || '我的位置',
    address: locationInfo.value.address || '',
    scale: 18,
    success: () => {
      console.log('打开导航成功')
    },
    fail: (err) => {
      console.error('打开导航失败', JSON.stringify(err))
    }
  })
}

/**
 * 选择地点 - 使用uni.chooseLocation
 * 
 * 返回选择的经纬度和地址信息
 * 适合用来做:收货地址选择、活动地点选择
 */
const chooseLocation = () => {
  uni.chooseLocation({
    latitude: centerLat.value,
    longitude: centerLon.value,
    keyword: '', // 可选,搜索关键词
    success: (res) => {
      console.log('选择地点成功', JSON.stringify(res))
      
      // 更新地图中心到选择的地点
      centerLat.value = res.latitude
      centerLon.value = res.longitude
      
      // 添加新标记点
      markers.value = [{
        id: new Date().getTime(),
        latitude: res.latitude,
        longitude: res.longitude,
        iconPath: '/static/marker.png',
        width: 30,
        height: 30,
        title: res.name
      }]
      
      locationInfo.value = res
    },
    fail: (err) => {
      console.error('选择地点失败', JSON.stringify(err))
    }
  })
}

/**
 * 点击标记点事件
 */
const onMarkerTap = (e) => {
  console.log('点击了标记点', JSON.stringify(e))
  uni.showToast({
    title: `标记点:${e.markerId}`,
    icon: 'none'
  })
}
</script>

<style>
.container {
  display: flex;
  flex-direction: column;
  height: 100vh;
}

.map {
  flex: 1;
  width: 100%;
}

.buttons {
  display: flex;
  flex-direction: column;
  padding: 20rpx;
  gap: 12rpx;
}

.info {
  padding: 20rpx;
  background: #f5f5f5;
  font-size: 28rpx;
  line-height: 1.8;
}
</style>

核心代码解析

getMyLocation函数

  • 必须传递type: 'gcj02'参数。不传的话返回的是WGS84坐标系,在腾讯地图上会偏移几百米。这一点很容易被忽略。
  • isHighAccuracy: true启动高精度模式,通过GPS和基站辅助定位,定位速度更快。
  • 定位成功后会更新地图中心点和标记点,让用户看到自己的位置。

openLocation函数

  • 依赖之前定位或选择的地点数据。如果没获取到位置直接调用,会跳转到空白页面。
  • 在鸿蒙上,这个API底层也是通过WebView加载腾讯地图的导航页面,返回按钮不太一样,需要用户手动点击返回。

chooseLocation函数

  • 可以传入经纬度作为初始位置(用于定位到当前位置附近)。
  • keyword参数可以用来搜索特定地点,比如传“医院”会筛选出附近的医院。
  • 返回的数据包含name(名称)、address(地址)、latitude/longitude(经纬度)。

调试时的域名白名单配置

这个问题是很多人在真机调试时遇到的:地图一片空白,控制台也没有任何错误。原因在于:

HarmonyOS NEXT上的页面目前不是通过http/https加载的,uni-app在鸿蒙上使用了一种特殊的本地协议。腾讯地图的JavaScript SDK默认检查当前页面的域名,如果不是白名单内的域名,就会拒绝加载地图。

解决方案

腾讯位置服务控制台的Key管理中,把域名白名单留空。这意味着这个Key可以在任何域名下使用,虽然不太安全,但在当前鸿蒙这类特殊环境下是唯一可行的方案。

在这里插入图片描述

后续uni-app官方表示会调整成以http方式加载页面,届时就可以像Android和iOS一样正常配置域名白名单了。

常见问题

问题1:地图一直显示“正在加载”,无法渲染

现象:页面能打开,但地图区域一直显示灰色或加载中。

原因

  1. Key配置不生效(manifest.json写错位置)。
  2. 域名白名单未正确配置。
  3. 腾讯地图服务端域名未在HarmonyOS网络配置中允许。

解决方案

  1. 检查manifest.json中maps.qqmap.key是否在正确的层级。
  2. 确认腾讯地图控制台的域名白名单为空。
  3. 在HarmonyOS的网络配置中,允许*.map.qq.com*.lbs.qq.com等域名(通常不需要额外配置,但如果企业网络环境有防火墙限制则需要)。

问题2:getLocation返回错误码,定位失败

现象:点击获取位置按钮,fail回调执行。

原因

  1. 权限未授予。
  2. 定位模块初始化失败(鸿蒙的定位服务需要LiteOS级别支持)。
  3. 定位超时(默认超时时间较短)。

解决方案

  1. 检查module.json5中是否声明了ohos.permission.LOCATION
  2. 在手机设置中确认应用定位权限已打开(鸿蒙的权限管理比Android更严格)。
  3. 增加highAccuracyTimeout参数到5秒以上,有些设备首次定位需要更长时间。

问题3:chooseLocation选择地点后返回空数据

现象:调起选择页面后,选择了一个地点,但返回的数据是空的。

原因

  1. 页面生命周期问题:返回数据时页面已经被销毁。
  2. 回调函数未正确绑定。
  3. 鸿蒙WebView的postMessage机制存在延迟。

解决方案

  1. success回调中立即保存数据到本地存储(uni.setStorageSync),避免页面销毁丢失数据。
  2. 不要在该回调中执行耗时操作,否则返回数据可能被“丢弃”。
  3. 考虑使用Promise封装:
const chooseLocationAsync = () => {
  return new Promise((resolve, reject) => {
    uni.chooseLocation({
      success: resolve,
      fail: reject
    })
  })
}

// 使用
const location = await chooseLocationAsync()

最佳实践

1. 定位请求加防抖,避免连续触发

用户如果快速点击“获取位置”按钮两次,两次请求会并行执行,可能导致定位模块异常。推荐:

let isLocating = false

const getMyLocation = () => {
  if (isLocating) {
    return
  }
  isLocating = true
  uni.getLocation({
    // ...
    complete: () => {
      isLocating = false
    }
  })
}

2. 每次定位都更新地图标记点

如果不更新标记点,用户定位成功后地图上还是之前的标记点,视觉上会让人困惑。就像上面示例代码里的处理。

3. 使用高精度模式但设置合理超时

isHighAccuracy: true结合highAccuracyTimeout: 5000是推荐组合。太短(如1000ms)导致频繁超时;太长(如10000ms)用户体验差。

FAQ

Q:为什么真机定位正常,模拟器上getLocation一直失败?

模拟器通常没有真实的GPS模块,也没有基站辅助定位。鸿蒙模拟器对位置模拟的支持还不太完善。建议所有定位相关功能都在真机上验证。

Q:页面返回后,之前选择的地点信息丢了,怎么处理?

这是页实例销毁后状态丢失的问题。建议在chooseLocation成功回调中把数据存到uni.setStorageSync或者全局状态管理(如Pinia/Vuex)中,返回页面时再从存储中读取。

// 选择地点时
uni.setStorageSync('selectedLocation', res)

// 页面onShow时
onShow(() => {
  const savedLocation = uni.getStorageSync('selectedLocation')
  if (savedLocation) {
    centerLat.value = savedLocation.latitude
    // ...其他处理
  }
})

Q:为什么需要设置type为gcj02?不设置会怎样?

不设置type或设置为wgs84时,返回的是GPS原始坐标。腾讯地图使用gcj02坐标系统,偏移量可能达到几百米,在地图上标记点会明显不在实际位置上。Android上也有这个区别,但很多应用会自动转换,鸿蒙上需要开发者手动处理。


示例代码地址:GitHub 项目地址(包含完整配置和演示页面)

如果你也遇到类似问题,可以重点检查Key配置和权限声明这两个环节。官方文档对这部分行为描述得比较简单,但实际开发中配置出错是最高频的问题。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐