uni-app开发Harmony Next平台的App——第九篇:实战项目——打造一个集地图、定位和WebView通讯的鸿蒙App

在这里插入图片描述

地图组件显示位置、内嵌H5页面通过JSBridge获取原生定位数据——这个场景在HarmonyOS NEXT开发里越来越常见。很多混合App都需要在地图上展示业务数据,同时内嵌一个H5运营页面,两者之间还要做数据交互。

这个功能本身不复杂,但真正麻烦的是三件事:地图组件的key配置、WebView和原生层的双向通讯、以及定位权限的生命周期管理。官方文档虽然提到了这些API,但没有解释实际使用中的限制和配合方式。

这篇直接用代码把这三个能力串起来,完成一个完整的示例:首页显示地图并获取用户位置,点击按钮后跳转到详情页,详情页内嵌H5页面并通过JSBridge把定位数据传给H5。

环境说明

DevEco Studio 版本:DevEco Studio 6.1.0 及以上
HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上
HBuilderX 版本:4.27 及以上
目标设备:HarmonyOS NEXT 真机 / 模拟器

先搞清楚这几件事

为什么需要WebView通讯

很多团队在迁移现有H5业务到Harmony App时,会遇到一个问题:H5页面想用原生的地图或定位能力,但H5自带的geolocation在WebView里不稳定。

方案对比:

方案 优缺点
H5直接请求浏览器定位 在WebView中精度差,部分设备不支持
原生定位后通过URL参数传值 只适用于启动时传参,动态交互无法实现
原生定位后通过JSBridge传值 实时通讯,支持双向调用,最稳定

实际项目里推荐用JSBridge方案。uni-app内置的WebView组件提供了evalJS方法和onMessage事件,刚好能做这个事。

项目功能拆解

  • 首页:使用map组件显示地图,调用getLocation获取当前经纬度
  • 跳转页:使用WebView组件加载本地H5页面
  • 通讯流程:点击WebView中的按钮→JSBridge触发postMessage→原生接收后通过evalJS传回定位数据

项目结构

pages/
├── index/           # 首页:地图 + 定位
│   └── index.vue
├── webview/         # WebView通讯页
│   └── webview.vue
hybrid/
└── html/
    └── map-demo.html   # WebView加载的H5页面

核心实现

第一步:配置腾讯地图key

在HarmonyOS NEXT上使用map组件,需要先配置腾讯地图key。这个很容易被忽略,直接贴配置代码:

编辑manifest.json,以源码方式打开,在app-plus节点下添加:

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

注意:HBuilderX 4.31及以上版本可以在可视化界面中配置,不需要手动改json。

申请key时,域名白名单留空,因为当前页面协议不是http。后续版本调整后会支持配置。

第二步:首页——地图组件与定位

pages/index/index.vue

<template>
  <view class="content">
    <map
      id="myMap"
      style="width: 100%; height: 500px;"
      :latitude="latitude"
      :longitude="longitude"
      :markers="markers"
      @markertap="handleMarkerTap"
    ></map>
    <view class="info">
      <text>当前位置:</text>
      <text>{{ latitude }}, {{ longitude }}</text>
    </view>
    <button type="primary" @click="getCurrentLocation">刷新定位</button>
    <button type="default" @click="goWebView">打开内嵌H5页面</button>
  </view>
</template>

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

const latitude = ref(39.908860)
const longitude = ref(116.397390)
const markers = ref([])

// 获取定位
async function getCurrentLocation() {
  try {
    const res = await uni.getLocation({
      type: 'gcj02',
      isHighAccuracy: true,
      highAccuracyExpireTime: 3000
    })
    latitude.value = res.latitude
    longitude.value = res.longitude
    // 更新标记点
    markers.value = [{
      id: 1,
      latitude: res.latitude,
      longitude: res.longitude,
      title: '当前位置',
      iconPath: '/static/location.png',
      width: 30,
      height: 30
    }]
  } catch (err) {
    uni.showToast({ title: '定位失败', icon: 'none' })
    console.error('getLocation err:', err)
  }
}

// 点击标记点事件
function handleMarkerTap(e) {
  uni.showToast({ title: `经纬度: ${e.detail.latitude}, ${e.detail.longitude}` })
}

// 跳转到WebView页面,把定位数据作为参数传递
function goWebView() {
  const data = encodeURIComponent(JSON.stringify({
    latitude: latitude.value,
    longitude: longitude.value,
    timestamp: Date.now()
  }))
  uni.navigateTo({
    url: `/pages/webview/webview?location=${data}`
  })
}

// 页面加载时自动定位
getCurrentLocation()
</script>

关键点说明:

  • getLocationtype参数用gcj02,这是国内地图通用坐标系
  • isHighAccuracy设为true可以提升定位精度,但会增加耗电
  • highAccuracyExpireTime控制高精度模式超时时间,这里设了3秒
  • markers数组更新后地图会自动重绘,不需要手动调用

第三步:WebView页面——原生与H5通讯

pages/webview/webview.vue

<template>
  <view class="content">
    <web-view
      ref="webviewRef"
      src="/hybrid/html/map-demo.html"
      @message="handleMessage"
      @loaded="handleLoaded"
    ></web-view>
  </view>
</template>

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

const webviewRef = ref(null)
let locationData = null

// 接收首页传递的定位数据
onLoad((option) => {
  if (option.location) {
    try {
      locationData = JSON.parse(decodeURIComponent(option.location))
    } catch (e) {
      console.error('参数解析失败', e)
    }
  }
})

// WebView加载完成后,把定位数据传给H5
function handleLoaded() {
  if (locationData && webviewRef.value) {
    const jsCode = `window.setLocation(${JSON.stringify(locationData)})`
    webviewRef.value.evalJS(jsCode)
  }
}

// 接收H5传来的消息
function handleMessage(e) {
  const data = e.detail.data
  // data是数组,因为H5可能发送多条
  if (Array.isArray(data)) {
    data.forEach(msg => {
      console.log('收到H5消息:', msg)
      if (msg.action === 'getLocation') {
        // H5请求定位数据,将数据传回
        sendToWebView(locationData)
      } else if (msg.action === 'navigateBack') {
        uni.navigateBack()
      }
    })
  }
}

// 通过evalJS向H5发送数据
function sendToWebView(data) {
  if (webviewRef.value) {
    const jsCode = `window.receiveLocation(${JSON.stringify(data)})`
    webviewRef.value.evalJS(jsCode)
  }
}
</script>

这套通讯机制的核心是:

  • @message监听H5通过uni.postMessage发送的消息
  • evalJS执行H5端定义的全局函数,把数据传回去
  • 页面加载时通过URL参数传递初始定位数据,减少延迟

第四步:H5页面中的JSBridge实现

hybrid/html/map-demo.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>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font-family: -apple-system, sans-serif; padding: 20px; }
    .info { padding: 20px; background: #f5f5f5; border-radius: 8px; margin-bottom: 20px; }
    .btn { padding: 12px 20px; background: #07c160; color: #fff; border: none; border-radius: 6px; font-size: 16px; width: 100%; }
    .btn:active { opacity: 0.8; }
    #locationDisplay { font-size: 14px; color: #666; margin-top: 10px; }
    #mapContainer { width: 100%; height: 300px; background: #eee; border-radius: 8px; margin-bottom: 20px; }
  </style>
</head>
<body>
  <h3>内嵌H5页面</h3>
  <div id="mapContainer">
    <p style="padding: 20px; text-align: center; color: #999;">地图加载中...</p>
  </div>
  <div class="info">
    <p>定位数据:</p>
    <p id="locationDisplay">等待获取...<p>
  </div>
  <button class="btn" onclick="requestLocation()">请求原生定位数据</button>
  <button class="btn" style="background: #999; margin-top: 10px;" onclick="goBack()">返回上一页</button>

  <script>
    // 存储当前位置数据
    let currentLocation = null

    // 由原生端调用,设置初始定位数据
    window.setLocation = function(data) {
      currentLocation = data
      updateDisplay(data)
      console.log('setLocation from native:', data)
    }

    // 由原生端调用,接收实时定位数据
    window.receiveLocation = function(data) {
      currentLocation = data
      updateDisplay(data)
      console.log('receiveLocation from native:', data)
    }

    // 更新页面显示
    function updateDisplay(data) {
      if (data) {
        document.getElementById('locationDisplay').innerHTML = 
          `纬度: ${data.latitude}<br>经度: ${data.longitude}<br>时间戳: ${new Date(data.timestamp).toLocaleString()}`
      }
    }

    // H5主动请求定位数据(通过JSBridge)
    function requestLocation() {
      // 使用uni-app WebView的JSBridge协议
      if (window.uni && typeof window.uni.postMessage === 'function') {
        window.uni.postMessage({
          action: 'getLocation',
          data: {}
        })
      } else {
        // 兜底:如果原生定好了直接显示
        if (currentLocation) {
          updateDisplay(currentLocation)
        } else {
          alert('定位数据尚未获取到')
        }
      }
    }

    // 返回原生页面
    function goBack() {
      if (window.uni && typeof window.uni.postMessage === 'function') {
        window.uni.postMessage({
          action: 'navigateBack',
          data: {}
        })
      }
    }

    // 页面加载时检查是否有缓存定位数据
    if (currentLocation) {
      updateDisplay(currentLocation)
    }
  </script>
</body>
</html>

这里有一个隐藏细节:window.uni.postMessage是uni-app WebView注入的JSBridge对象,需要通过它发送消息到原生层。直接使用uni.postMessage就可以,不需要额外引入SDK。

踩坑记录

坑1:WebView的src路径问题

现象src="/hybrid/html/map-demo.html"在HarmonyOS真机上无法加载,页面空白。

原因:HarmonyOS NEXT平台的WebView不支持以/开头的绝对路径。需要换成相对路径或使用@/

解决方案

<web-view
  src="/hybrid/html/map-demo.html"  <!-- 错误写法 -->
  src="../../hybrid/html/map-demo.html"  <!-- 正确写法:相对路径 -->
></web-view>

坑2:postMessage接收数据格式

现象handleMessage方法中e.detail.data返回的是数组,但很多人误以为直接是对象。

原因:uni-app WebView的@message事件规定,H5发送的数据会封装为数组,每条消息一个元素。

解决方案:判断是数组后再遍历处理,不能直接使用e.detail.data.xxx

// 正确做法
function handleMessage(e) {
  const dataList = e.detail.data
  if (Array.isArray(dataList)) {
    dataList.forEach(msg => {
      if (msg.action === 'xxx') { ... }
    })
  }
}

坑3:定位权限弹窗只触发一次

现象:第一次运行会弹权限框,同意后关闭App再打开,getLocation仍然成功。但如果系统设置里关闭了定位,重新打开App不会再次弹框。

原因:鸿蒙的权限管理机制中,用户拒绝后不会自动重弹,需要主动引导用户去系统设置开启。

解决方案:在定位失败时判断错误码,如果是权限拒绝,通过uni.openAppAuthorizeSetting打开系统设置页。

async function getCurrentLocation() {
  try {
    // 定位逻辑...
  } catch (err) {
    if (err.errCode === 12) {
      uni.showModal({
        title: '定位权限未开启',
        content: '请在设置中开启定位权限后重试',
        success: (res) => {
          if (res.confirm) {
            uni.openAppAuthorizeSetting()
          }
        }
      })
    }
  }
}

最佳实践

1. 不要在onLoad里直接调用evalJS

原因:WebView组件可能有加载延迟,onLoad触发时页面可能还没渲染完毕。建议等@loaded事件触发后再执行JS。

2. 统一JSBridge消息格式

所有原生和H5之间的消息建议统一为{ action: string, data: any }格式,方便扩展和调试。

3. 定位数据传到WebView时做脱敏

正式项目里定位数据不应该直接暴露给H5,建议只传{ lat, lng }不传时间戳等信息。如果H5需要高频获取位置,可以在原生端做节流,避免频繁调用定位API。

FAQ

Q:为什么真机定位正常,模拟器获取不到位置?

A:HarmonyOS模拟器的定位功能是模拟的,默认返回北京天安门坐标(39.908860, 116.397390)。如果需要模拟其他位置,可以在模拟器设置中手动调整。

Q:WebView中postMessage发送的消息,原生收不到怎么办?

A:先确认H5页面中window.uni.postMessage是否可用。在WebView的@loaded事件触发后,可以通过evalJS执行一段脚本检测typeof window.uni是否存在。如果不存在,检查web-view组件的src路径和是否在子域名环境下。

Q:页面返回后,WebView的状态丢失了?

A:当前实现中,返回后WebView会销毁。如果需要保持状态,可以考虑用pageskeep-alive或者将定位数据存入全局状态(pinia/vuex),下次进入时自动恢复。

Logo

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

更多推荐