欢迎光临
我们一直在努力

UniApp 中使用原生鸿蒙碰一碰分享技术详解

前言

随着鸿蒙(HarmonyOS)生态的快速发展,碰一碰分享作为一项创新的近场交互功能,为用户提供了更加便捷的内容分享体验。本文将详细介绍如何在 UniApp 项目中集成鸿蒙原生碰一碰分享功能,实现设备间的快速图片分享。本文将以我开发的APP《BabyOne》为例,感兴趣的朋友可以直接下载体验~

什么是碰一碰分享?

碰一碰分享是鸿蒙系统提供的一种基于近场通信的分享方式,用户只需将两台设备轻轻碰触,即可快速分享内容。相比传统的分享方式,碰一碰分享具有以下优势:

  • 🚀 操作简单:无需复杂的配对过程,轻碰即可分享
  • ⚡ 速度快:基于近场通信,传输速度快
  • 🔒 安全可靠:点对点传输,数据更安全
  • 📱 无需安装:接收方无需安装应用即可接收内容
  • 🎯 体验流畅:原生支持,用户体验优秀

应用场景

碰一碰分享适用于多种场景:

  • 👶 儿童成长记录:分享宝宝照片、成长卡片
  • 📸 照片分享:快速分享活动照片、合影
  • 🎨 创意作品:分享绘画作品、手工制作
  • 📇 名片交换:快速交换电子名片
  • 🎁 优惠券分享:分享优惠券、活动海报

jack-knock-share 插件介绍

为了简化在 UniApp 中使用鸿蒙碰一碰分享的开发流程,我封装了 jack-knock-share 插件。这是一个基于 UTS(UniApp TypeScript)开发的原生插件,完全开源。

平台支持

  • ✅ HarmonyOS:使用 @kit.ShareKit 原生 API

核心特性

  • 简单易用:仅需两个 API,快速集成
  • 自动触发:注册后自动监听碰一碰事件
  • 完善的回调:支持成功、失败、完成回调
  • 生命周期管理:支持注册和注销监听
  • 开箱即用:本文提供完整源码,可直接使用
  • 为什么要封装这个插件?

    在开发儿童成长记录(BabyOne)应用时,我需要实现宝宝名片的快速分享功能。直接使用鸿蒙原生 API 存在以下问题:

  • API 复杂:需要处理 ShareKit、文件 URI 转换等
  • 回调管理困难:需要手动管理监听器的注册和注销
  • 错误处理繁琐:需要处理各种异常情况
  • 代码重复:多个页面使用时代码重复
  • 因此,我将功能封装成了 jack-knock-share 插件,大大简化了使用流程。

    技术架构

    插件目录结构

    uni_modules/jack-knock-share/
    ├── utssdk/
    │ ├── interface.uts # 接口定义
    │ └── app-harmony/ # 鸿蒙平台实现
    │ ├── index.uts # 主实现文件
    │ └── native.uts # 原生分享实现
    ├── package.json
    └── readme.md

    核心 API 设计

    插件提供了 2 个核心 API:

    API说明参数
    uni.registerKnockShare(options) 注册碰一碰分享监听 imagePath, success, fail, complete
    uni.unregisterKnockShare() 注销碰一碰分享监听

    插件完整源码

    为了方便大家使用,这里提供 jack-knock-share 插件的完整源代码,可以直接复制到你的项目中使用。

    1. utssdk/interface.uts(接口定义)

    /**
    * 碰一碰分享插件接口定义
    */

    /**
    * 注册碰一碰分享参数
    */

    export type RegisterKnockShareOptions = {
    /**
    * 要分享的图片路径(本地临时文件路径)
    */

    imagePath : string
    /**
    * 接口调用成功的回调函数
    * @defaultValue null
    */

    success ?: RegisterKnockShareSuccessCallback | null
    /**
    * 接口调用失败的回调函数
    * @defaultValue null
    */

    fail ?: RegisterKnockShareFailCallback | null
    /**
    * 接口调用结束的回调函数(调用成功、失败都会执行)
    * @defaultValue null
    */

    complete ?: RegisterKnockShareCompleteCallback | null
    }

    /**
    * 注册成功回调结果
    */

    export type RegisterKnockShareSuccess = {
    /**
    * 成功信息
    */

    errMsg : string
    }

    /**
    * 注册失败回调结果
    */

    export type RegisterKnockShareFail = {
    /**
    * 错误码
    */

    errCode : number
    /**
    * 错误信息
    */

    errMsg : string
    }

    /**
    * 注册完成回调结果
    */

    export type RegisterKnockShareComplete = {
    /**
    * 信息
    */

    errMsg : string
    }

    /**
    * 注册成功回调函数
    */

    export type RegisterKnockShareSuccessCallback = (result : RegisterKnockShareSuccess) => void

    /**
    * 注册失败回调函数
    */

    export type RegisterKnockShareFailCallback = (result : RegisterKnockShareFail) => void

    /**
    * 注册完成回调函数
    */

    export type RegisterKnockShareCompleteCallback = (result : RegisterKnockShareComplete) => void

    /**
    * 注册碰一碰分享函数类型
    */

    export type RegisterKnockShare = (options : RegisterKnockShareOptions) => void

    /**
    * 注销碰一碰分享函数类型
    */

    export type UnregisterKnockShare = () => void

    /**
    * 扩展 uni 全局对象
    */

    export interface Uni {
    /**
    * registerKnockShare()
    * @description
    * 注册碰一碰分享监听,当用户触发碰一碰时自动分享指定图片
    * @param {RegisterKnockShareOptions} options
    * @return {void}
    * @example
    ```typescript
    uni.registerKnockShare({
    imagePath: '/path/to/image.jpg',
    success: (res) => {
    console.log('注册成功:', res)
    },
    fail: (err) => {
    console.error('注册失败:', err)
    }
    });
    ```
    */

    registerKnockShare(options : RegisterKnockShareOptions) : void

    /**
    * unregisterKnockShare()
    * @description
    * 注销碰一碰分享监听
    * @return {void}
    * @example
    ```typescript
    uni.unregisterKnockShare();
    ```
    */

    unregisterKnockShare() : void
    }

    2. utssdk/app-harmony/index.uts(主实现文件)

    /**
    * 碰一碰分享插件 – HarmonyOS 实现
    */

    import {
    RegisterKnockShare,
    RegisterKnockShareOptions,
    RegisterKnockShareSuccess,
    RegisterKnockShareFail,
    RegisterKnockShareComplete,
    UnregisterKnockShare
    } from '../interface.uts'

    // 导出类型定义
    export {
    RegisterKnockShare,
    RegisterKnockShareOptions,
    RegisterKnockShareSuccess,
    RegisterKnockShareFail,
    RegisterKnockShareComplete,
    UnregisterKnockShare
    }

    // 导入 HarmonyOS 原生 API
    import { harmonyShare } from '@kit.ShareKit'
    import { BusinessError } from '@kit.BasicServicesKit'
    import { shareImageNative, ShareResult } from './native.uts'

    /**
    * 碰一碰回调函数引用
    */

    let knockShareCallback : ((target : harmonyShare.SharableTarget) => void) | null = null

    /**
    * 当前要分享的图片路径
    */

    let currentImagePath : string = ''

    /**
    * 当前注册的选项(用于回调)
    */

    let currentOptions : RegisterKnockShareOptions | null = null

    /**
    * 注册碰一碰分享
    */

    export const registerKnockShare : RegisterKnockShare = function (options : RegisterKnockShareOptions) {
    try {
    console.log('[KnockShare] 开始注册碰一碰分享')
    console.log('[KnockShare] 图片路径:', options.imagePath)

    // 保存图片路径和选项
    currentImagePath = options.imagePath
    currentOptions = options

    // 定义碰一碰回调函数
    knockShareCallback = (sharableTarget : harmonyShare.SharableTarget) => {
    // 调用原生分享函数
    const result : ShareResult = shareImageNative(sharableTarget, currentImagePath)

    if (result.success) {
    console.log('[KnockShare] 分享完成')
    if (currentOptions != null) {
    const successResult : RegisterKnockShareSuccess = {
    errMsg: '分享成功'
    }
    const completeResult : RegisterKnockShareComplete = {
    errMsg: '分享成功'
    }
    currentOptions.success?.(successResult)
    currentOptions.complete?.(completeResult)
    }
    } else {
    console.error('[KnockShare] 分享失败:', result.message)
    if (currentOptions != null) {
    const failResult : RegisterKnockShareFail = {
    errCode: result.code ?? 0,
    errMsg: result.message
    }
    const completeResult : RegisterKnockShareComplete = {
    errMsg: result.message
    }
    currentOptions.fail?.(failResult)
    currentOptions.complete?.(completeResult)
    }
    }
    }

    // 注册碰一碰监听
    harmonyShare.on('knockShare', knockShareCallback)

    console.log('[KnockShare] 碰一碰监听注册成功')

    // 调用成功回调(注册成功)
    const successResult : RegisterKnockShareSuccess = {
    errMsg: '注册成功'
    }
    const completeResult : RegisterKnockShareComplete = {
    errMsg: '注册成功'
    }
    options.success?.(successResult)
    options.complete?.(completeResult)

    } catch (error) {
    console.error('[KnockShare] 注册失败:', error)
    const err = error as BusinessError

    // 调用失败回调
    const failResult : RegisterKnockShareFail = {
    errCode: err.code,
    errMsg: err.message ?? '注册失败'
    }
    const completeResult : RegisterKnockShareComplete = {
    errMsg: err.message ?? '注册失败'
    }
    options.fail?.(failResult)
    options.complete?.(completeResult)
    }
    }

    /**
    * 注销碰一碰分享
    */

    export const unregisterKnockShare : UnregisterKnockShare = function () {
    try {
    console.log('[KnockShare] 开始注销碰一碰分享')

    if (knockShareCallback != null) {
    // 注销监听
    harmonyShare.off('knockShare', knockShareCallback)

    // 清理变量
    knockShareCallback = null
    currentImagePath = ''
    currentOptions = null

    console.log('[KnockShare] 碰一碰监听注销成功')
    } else {
    console.log('[KnockShare] 没有需要注销的监听')
    }

    } catch (error) {
    console.error('[KnockShare] 注销失败:', error)
    }
    }

    3. utssdk/app-harmony/native.uts(原生分享实现)

    /**
    * 原生分享实现
    */

    import { harmonyShare, systemShare } from '@kit.ShareKit'
    import { uniformTypeDescriptor as utd } from '@kit.ArkData'
    import { fileUri } from '@kit.CoreFileKit'
    import { BusinessError } from '@kit.BasicServicesKit'

    /**
    * 分享数据接口(匹配 SharedRecord)
    */

    interface ShareDataRecord {
    utd: string
    uri: string
    thumbnailUri?: string
    }

    /**
    * 分享结果接口
    */

    export interface ShareResult {
    success: boolean
    code?: number
    message: string
    }

    /**
    * 分享图片(原生实现)
    */

    export function shareImageNative(
    sharableTarget : harmonyShare.SharableTarget,
    imagePath : string
    ) : ShareResult {
    try {
    console.log('[Native] 碰一碰触发,开始分享')
    console.log('[Native] 分享图片:', imagePath)

    // 处理文件路径
    let filePath = imagePath
    if (filePath.startsWith('file://')) {
    filePath = filePath.substring(7)
    }

    console.log('[Native] 处理后的路径:', filePath)

    // 获取文件 URI
    const imageUri = fileUri.getUriFromPath(filePath)
    console.log('[Native] 文件 URI:', imageUri)

    // 构建分享数据对象
    const shareDataRecord : ShareDataRecord = {
    utd: utd.UniformDataType.IMAGE,
    uri: imageUri,
    thumbnailUri: imageUri
    }

    // 创建 SharedData
    const shareData = new systemShare.SharedData(shareDataRecord as systemShare.SharedRecord)

    console.log('[Native] 分享数据构建完成')

    // 执行分享
    sharableTarget.share(shareData)

    console.log('[Native] 分享成功')

    return {
    success: true,
    message: '分享成功'
    }

    } catch (error) {
    console.error('[Native] 分享失败:', error)
    const err = error as BusinessError
    return {
    success: false,
    code: err.code,
    message: err.message ?? '分享失败'
    }
    }
    }

    如何使用插件源码

  • 创建目录结构:在你的 UniApp 项目中创建 uni_modules/jack-knock-share 目录
  • 复制文件:按照上述目录结构,将代码复制到对应的文件中
  • 重启项目:重新编译运行项目,插件即可生效
  • 鸿蒙平台实现原理

    1. 引入鸿蒙 SDK

    import { harmonyShare, systemShare } from '@kit.ShareKit'
    import { uniformTypeDescriptor as utd } from '@kit.ArkData'
    import { fileUri } from '@kit.CoreFileKit'
    import { BusinessError } from '@kit.BasicServicesKit'

    2. 注册碰一碰监听

    鸿蒙平台使用 harmonyShare.on('knockShare', callback) 注册碰一碰事件监听:

    // 定义回调函数
    const knockShareCallback = (sharableTarget : harmonyShare.SharableTarget) => {
    // 当用户触发碰一碰时,这里会被调用
    // sharableTarget 是分享目标对象
    }

    // 注册监听
    harmonyShare.on('knockShare', knockShareCallback)

    3. 构建分享数据

    使用 systemShare.SharedData 构建分享数据:

    // 1. 处理文件路径
    let filePath = imagePath
    if (filePath.startsWith('file://')) {
    filePath = filePath.substring(7)
    }

    // 2. 获取文件 URI
    const imageUri = fileUri.getUriFromPath(filePath)

    // 3. 构建分享数据
    const shareDataRecord = {
    utd: utd.UniformDataType.IMAGE, // 数据类型:图片
    uri: imageUri, // 文件 URI
    thumbnailUri: imageUri // 缩略图 URI
    }

    // 4. 创建 SharedData
    const shareData = new systemShare.SharedData(shareDataRecord)

    4. 执行分享

    // 调用 share 方法执行分享
    sharableTarget.share(shareData)

    5. 注销监听

    // 注销碰一碰监听
    harmonyShare.off('knockShare', knockShareCallback)

    快速开始

    第一步:安装插件

    方式一:手动创建(推荐)

  • 在你的 UniApp 项目根目录下创建以下目录结构:

    uni_modules/jack-knock-share/
    ├── utssdk/
    │ ├── app-harmony/

  • 将上面"技术架构"章节中的完整源码,按照文件路径复制到对应位置

  • 确保所有文件都已创建:

    • uni_modules/jack-knock-share/utssdk/interface.uts
    • uni_modules/jack-knock-share/utssdk/app-harmony/index.uts
    • uni_modules/jack-knock-share/utssdk/app-harmony/native.uts
  • 方式二:通过 UniApp 插件市场安装

    访问 UniApp 插件市场,搜索 鸿蒙碰一碰分享插件 直接安装。

    第二步:在页面中使用

    1. 导入插件

    <script>
    // 使用条件编译,仅在鸿蒙平台导入
    // #ifdef APP-HARMONY
    import '@/uni_modules/jack-knock-share'
    // #endif

    export default {
    data() {
    return {
    shareImage: '',
    isSharing: false
    }
    }
    }
    </script>

    2. 生成要分享的图片

    使用 Canvas 生成图片:

    methods: {
    async generateShareImage() {
    return new Promise((resolve, reject) => {
    const ctx = uni.createCanvasContext('shareCanvas', this)

    // 绘制背景
    ctx.setFillStyle('#667eea')
    ctx.fillRect(0, 0, 375, 500)

    // 绘制文字
    ctx.setFillStyle('#ffffff')
    ctx.setFontSize(32)
    ctx.setTextAlign('center')
    ctx.fillText('宝宝成长卡片', 187.5, 250)

    // 导出图片
    ctx.draw(false, () => {
    setTimeout(() => {
    uni.canvasToTempFilePath({
    canvasId: 'shareCanvas',
    success: (res) => {
    resolve(res.tempFilePath)
    },
    fail: reject
    }, this)
    }, 500)
    })
    })
    }
    }

    3. 注册碰一碰分享

    async startKnockShare() {
    uni.showLoading({ title: '生成中…' })

    try {
    // 1. 生成图片
    const imagePath = await this.generateShareImage()
    this.shareImage = imagePath

    uni.hideLoading()

    // 2. 注册碰一碰分享
    // #ifdef APP-HARMONY
    uni.registerKnockShare({
    imagePath: imagePath,
    success: (res) => {
    console.log('注册成功:', res)
    this.isSharing = true
    uni.showToast({
    title: '请轻碰设备进行分享',
    icon: 'none',
    duration: 3000
    })
    },
    fail: (err) => {
    console.error('注册失败:', err)
    uni.showToast({
    title: '注册失败: ' + err.errMsg,
    icon: 'none'
    })
    },
    complete: (res) => {
    console.log('完成:', res)
    }
    })
    // #endif
    } catch (error) {
    uni.hideLoading()
    console.error('生成失败:', error)
    uni.showToast({
    title: '生成失败',
    icon: 'none'
    })
    }
    }

    4. 停止分享

    stopKnockShare() {
    // #ifdef APP-HARMONY
    uni.unregisterKnockShare()
    // #endif

    this.isSharing = false
    uni.showToast({
    title: '已关闭分享',
    icon: 'success'
    })
    }

    5. 页面生命周期管理

    onHide() {
    // 页面隐藏时自动关闭分享
    if (this.isSharing) {
    // #ifdef APP-HARMONY
    uni.unregisterKnockShare()
    // #endif
    this.isSharing = false
    }
    },

    onUnload() {
    // 页面卸载时关闭分享
    if (this.isSharing) {
    // #ifdef APP-HARMONY
    uni.unregisterKnockShare()
    // #endif
    }
    }

    完整示例:宝宝名片分享

    下面是一个完整的宝宝名片分享示例页面:

    <template>
    <view class="container">
    <!– 预览区域 –>
    <view class="preview-section">
    <view class="preview-card" v-if="shareImage">
    <image :src="shareImage" mode="aspectFit" class="preview-image"></image>
    </view>
    <view class="preview-placeholder" v-else>
    <text class="placeholder-text">点击下方按钮生成名片</text>
    </view>
    </view>

    <!– 操作按钮 –>
    <view class="action-section">
    <button
    @click="generateAndShare"
    class="btn-primary"
    :disabled="isSharing"
    >

    {{ isSharing ? '🔄 分享中…' : '✨ 生成并开启碰一碰分享' }}
    </button>

    <button
    @click="stopShare"
    class="btn-secondary"
    v-if="isSharing"
    >

    ❌ 关闭分享
    </button>
    </view>

    <!– 提示信息 –>
    <view class="tips-section" v-if="isSharing">
    <view class="tips-card">
    <text class="tips-icon">💡</text>
    <text class="tips-text">将设备轻碰另一台设备即可分享名片</text>
    </view>
    </view>

    <!– 隐藏的 Canvas 用于生成图片 –>
    <canvas canvas-id="shareCanvas" class="hidden-canvas"></canvas>
    </view>
    </template>

    <script>
    // 引入碰一碰分享插件
    // #ifdef APP-HARMONY
    import '@/uni_modules/jack-knock-share'
    // #endif

    export default {
    data() {
    return {
    shareImage: '',
    isSharing: false,
    babyInfo: {
    name: '小宝贝',
    age: '1岁3个月',
    birthday: '2023-10-15',
    weight: '10.5kg',
    height: '78cm'
    }
    }
    },

    methods: {
    /**
    * 生成并开启碰一碰分享
    */

    async generateAndShare() {
    uni.showLoading({ title: '生成中…' })

    try {
    // 1. 生成名片图片
    const imagePath = await this.generateBabyCard()
    this.shareImage = imagePath

    uni.hideLoading()

    // 2. 注册碰一碰分享
    // #ifdef APP-HARMONY
    uni.registerKnockShare({
    imagePath: imagePath,
    success: (res) => {
    console.log('[分享] 注册成功:', res)
    this.isSharing = true
    uni.showToast({
    title: '请轻碰设备进行分享',
    icon: 'none',
    duration: 3000
    })
    },
    fail: (err) => {
    console.error('[分享] 注册失败:', err)
    uni.showToast({
    title: '注册失败: ' + err.errMsg,
    icon: 'none'
    })
    },
    complete: (res) => {
    console.log('[分享] 完成:', res)
    }
    })
    // #endif

    // #ifndef APP-HARMONY
    uni.showToast({
    title: '当前平台不支持碰一碰分享',
    icon: 'none'
    })
    // #endif

    } catch (error) {
    uni.hideLoading()
    console.error('[分享] 生成失败:', error)
    uni.showToast({
    title: '生成失败',
    icon: 'none'
    })
    }
    },

    /**
    * 生成宝宝名片图片
    */

    async generateBabyCard() {
    return new Promise((resolve, reject) => {
    const ctx = uni.createCanvasContext('shareCanvas', this)
    const canvasWidth = 375
    const canvasHeight = 500

    // 绘制渐变背景
    const gradient = ctx.createLinearGradient(0, 0, canvasWidth, canvasHeight)
    gradient.addColorStop(0, '#667eea')
    gradient.addColorStop(1, '#764ba2')
    ctx.setFillStyle(gradient)
    ctx.fillRect(0, 0, canvasWidth, canvasHeight)

    // 绘制白色卡片
    ctx.setFillStyle('#ffffff')
    ctx.setShadow(0, 10, 30, 'rgba(0, 0, 0, 0.1)')
    ctx.fillRect(30, 80, canvasWidth 60, canvasHeight 160)

    // 绘制标题
    ctx.setFillStyle('#333333')
    ctx.setFontSize(36)
    ctx.setTextAlign('center')
    ctx.fillText('👶 宝宝成长卡片', canvasWidth / 2, 140)

    // 绘制分割线
    ctx.setStrokeStyle('#eeeeee')
    ctx.setLineWidth(2)
    ctx.beginPath()
    ctx.moveTo(60, 170)
    ctx.lineTo(canvasWidth 60, 170)
    ctx.stroke()

    // 绘制宝宝信息
    const info = this.babyInfo
    const startY = 220
    const lineHeight = 50

    ctx.setFillStyle('#666666')
    ctx.setFontSize(28)
    ctx.setTextAlign('left')

    ctx.fillText(`姓名: ${info.name}`, 70, startY)
    ctx.fillText(`年龄: ${info.age}`, 70, startY + lineHeight)
    ctx.fillText(`生日: ${info.birthday}`, 70, startY + lineHeight * 2)
    ctx.fillText(`体重: ${info.weight}`, 70, startY + lineHeight * 3)
    ctx.fillText(`身高: ${info.height}`, 70, startY + lineHeight * 4)

    // 绘制底部提示
    ctx.setFillStyle('#999999')
    ctx.setFontSize(20)
    ctx.setTextAlign('center')
    ctx.fillText('碰一碰分享给好友', canvasWidth / 2, canvasHeight 40)

    // 导出图片
    ctx.draw(false, () => {
    setTimeout(() => {
    uni.canvasToTempFilePath({
    canvasId: 'shareCanvas',
    success: (res) => {
    console.log('[Canvas] 图片生成成功:', res.tempFilePath)
    resolve(res.tempFilePath)
    },
    fail: (err) => {
    console.error('[Canvas] 图片生成失败:', err)
    reject(err)
    }
    }, this)
    }, 500)
    })
    })
    },

    /**
    * 停止分享
    */

    stopShare() {
    // #ifdef APP-HARMONY
    uni.unregisterKnockShare()
    console.log('[分享] 已注销监听')
    // #endif

    this.isSharing = false
    uni.showToast({
    title: '已关闭分享',
    icon: 'success'
    })
    }
    },

    onHide() {
    // 页面隐藏时自动关闭分享
    if (this.isSharing) {
    // #ifdef APP-HARMONY
    uni.unregisterKnockShare()
    // #endif
    this.isSharing = false
    console.log('[生命周期] 页面隐藏,已关闭分享')
    }
    },

    onUnload() {
    // 页面卸载时关闭分享
    if (this.isSharing) {
    // #ifdef APP-HARMONY
    uni.unregisterKnockShare()
    // #endif
    console.log('[生命周期] 页面卸载,已关闭分享')
    }
    }
    }
    </script>

    <style scoped>
    .container {
    min-height: 100vh;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    padding: 40rpx;
    }

    .preview-section {
    margin-bottom: 60rpx;
    }

    .preview-card {
    background: white;
    border-radius: 30rpx;
    overflow: hidden;
    box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.15);
    }

    .preview-image {
    width: 100%;
    height: 1000rpx;
    }

    .preview-placeholder {
    background: rgba(255, 255, 255, 0.2);
    border-radius: 30rpx;
    height: 1000rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 4rpx dashed rgba(255, 255, 255, 0.5);
    }

    .placeholder-text {
    color: white;
    font-size: 32rpx;
    }

    .action-section {
    display: flex;
    flex-direction: column;
    gap: 24rpx;
    }

    .btn-primary,
    .btn-secondary
    {
    width: 100%;
    height: 96rpx;
    border-radius: 48rpx;
    font-size: 32rpx;
    font-weight: bold;
    border: none;
    }

    .btn-primary {
    background: white;
    color: #667eea;
    box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.1);
    }

    .btn-primary[disabled] {
    opacity: 0.6;
    }

    .btn-secondary {
    background: rgba(255, 255, 255, 0.2);
    color: white;
    border: 2rpx solid white;
    }

    .tips-section {
    margin-top: 40rpx;
    }

    .tips-card {
    background: rgba(255, 255, 255, 0.95);
    border-radius: 20rpx;
    padding: 32rpx;
    display: flex;
    align-items: center;
    gap: 20rpx;
    box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.1);
    }

    .tips-icon {
    font-size: 48rpx;
    }

    .tips-text {
    flex: 1;
    font-size: 28rpx;
    color: #666;
    line-height: 40rpx;
    }

    .hidden-canvas {
    position: fixed;
    left: -9999rpx;
    top: -9999rpx;
    width: 750rpx;
    height: 1000rpx;
    }
    </style>

    核心技术要点

    1. 文件路径处理

    鸿蒙系统需要使用 fileUri.getUriFromPath() 将文件路径转换为 URI:

    // 处理 file:// 前缀
    let filePath = imagePath
    if (filePath.startsWith('file://')) {
    filePath = filePath.substring(7)
    }

    // 转换为 URI
    const imageUri = fileUri.getUriFromPath(filePath)

    2. 数据类型定义

    使用 uniformTypeDescriptor 定义分享数据类型:

    import { uniformTypeDescriptor as utd } from '@kit.ArkData'

    const shareDataRecord = {
    utd: utd.UniformDataType.IMAGE, // 图片类型
    uri: imageUri,
    thumbnailUri: imageUri
    }

    3. 监听器管理

    使用全局变量管理监听器,避免重复注册:

    let knockShareCallback : ((target : harmonyShare.SharableTarget) => void) | null = null

    // 注册前检查
    if (knockShareCallback != null) {
    // 先注销旧的监听器
    harmonyShare.off('knockShare', knockShareCallback)
    }

    // 注册新的监听器
    harmonyShare.on('knockShare', knockShareCallback)

    4. 错误处理

    完善的错误处理机制:

    try {
    // 执行分享逻辑
    sharableTarget.share(shareData)
    return {
    success: true,
    message: '分享成功'
    }
    } catch (error) {
    const err = error as BusinessError
    return {
    success: false,
    code: err.code,
    message: err.message ?? '分享失败'
    }
    }

    5. 生命周期管理

    在页面生命周期中管理分享状态:

    // 页面隐藏时注销
    onHide() {
    if (this.isSharing) {
    uni.unregisterKnockShare()
    this.isSharing = false
    }
    }

    // 页面卸载时注销
    onUnload() {
    if (this.isSharing) {
    uni.unregisterKnockShare()
    }
    }

    最佳实践

    1. 条件编译

    使用条件编译确保代码只在鸿蒙平台运行:

    // #ifdef APP-HARMONY
    uni.registerKnockShare({
    imagePath: imagePath,
    success: (res) => {
    console.log('注册成功')
    }
    })
    // #endif

    // #ifndef APP-HARMONY
    uni.showToast({
    title: '当前平台不支持碰一碰分享',
    icon: 'none'
    })
    // #endif

    2. 图片生成优化

    使用 Canvas 生成图片时的注意事项:

    // 1. 延迟导出,确保绘制完成
    ctx.draw(false, () => {
    setTimeout(() => {
    uni.canvasToTempFilePath({
    canvasId: 'shareCanvas',
    success: (res) => {
    resolve(res.tempFilePath)
    }
    }, this)
    }, 500) // 延迟 500ms
    })

    // 2. 使用高质量设置
    uni.canvasToTempFilePath({
    canvasId: 'shareCanvas',
    quality: 1, // 最高质量
    fileType: 'jpg', // 文件类型
    success: (res) => {
    // …
    }
    }, this)

    3. 用户体验优化

    提供清晰的视觉反馈:

    <template>
    <button
    @click="generateAndShare"
    :disabled="isSharing"
    :class="{ 'btn-loading': isSharing }"
    >

    {{ isSharing ? '🔄 分享中…' : '✨ 开启分享' }}
    </button>

    <view class="tips" v-if="isSharing">
    <text>💡 将设备轻碰另一台设备即可分享</text>
    </view>
    </template>

    <style>
    .btn-loading {
    animation: pulse 1.5s infinite;
    }

    @keyframes pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.7; }
    }
    </style>

    4. 错误提示

    提供友好的错误提示:

    uni.registerKnockShare({
    imagePath: imagePath,
    success: (res) => {
    uni.showToast({
    title: '请轻碰设备进行分享',
    icon: 'none',
    duration: 3000
    })
    },
    fail: (err) => {
    let errorMsg = '注册失败'
    if (err.errCode === 401) {
    errorMsg = '权限不足'
    } else if (err.errCode === 404) {
    errorMsg = '文件不存在'
    }

    uni.showToast({
    title: errorMsg,
    icon: 'none'
    })
    }
    })

    5. 状态管理

    使用状态标志管理分享状态:

    data() {
    return {
    shareState: {
    isGenerating: false, // 是否正在生成
    isSharing: false, // 是否正在分享
    shareImage: '', // 分享图片路径
    error: null // 错误信息
    }
    }
    },

    methods: {
    updateShareState(updates) {
    this.shareState = { this.shareState, updates }
    }
    }

    常见问题 FAQ

    Q1: 碰一碰没有触发分享?

    可能原因:

  • 设备不支持碰一碰功能
  • 未正确注册监听
  • 图片路径不正确
  • 系统版本过低
  • 解决方案:

    • 检查设备是否支持 HarmonyOS 碰一碰功能(需要 HarmonyOS 6.0+)
    • 查看控制台日志确认注册状态
    • 确保图片路径是本地临时文件路径
    • 检查 harmonyShare.on() 是否成功调用

    // 添加详细日志
    uni.registerKnockShare({
    imagePath: imagePath,
    success: (res) => {
    console.log('[调试] 注册成功:', res)
    },
    fail: (err) => {
    console.error('[调试] 注册失败:', err.errCode, err.errMsg)
    }
    })

    Q2: 分享失败,提示文件不存在?

    可能原因:

    • Canvas 生成图片失败
    • 图片路径格式错误
    • 文件已被清理

    解决方案:

    // 1. 检查图片是否生成成功
    const imagePath = await this.generateShareImage()
    console.log('生成的图片路径:', imagePath)

    // 2. 验证文件是否存在
    uni.getFileInfo({
    filePath: imagePath,
    success: (res) => {
    console.log('文件大小:', res.size)
    // 文件存在,可以分享
    },
    fail: (err) => {
    console.error('文件不存在:', err)
    }
    })

    // 3. 确保路径格式正确
    let filePath = imagePath
    if (filePath.startsWith('file://')) {
    filePath = filePath.substring(7)
    }

    Q3: 对方需要安装应用吗?

    答:不需要。 碰一碰分享的是图片文件,对方设备会直接接收图片到相册或文件管理器,无需安装应用。

    Q4: 可以分享其他类型的文件吗?

    答:目前插件只支持图片分享。 如需分享其他类型文件,需要修改 native.uts 中的数据类型:

    // 分享文本
    const shareDataRecord = {
    utd: utd.UniformDataType.TEXT,
    uri: textUri
    }

    // 分享视频
    const shareDataRecord = {
    utd: utd.UniformDataType.VIDEO,
    uri: videoUri
    }

    // 分享文件
    const shareDataRecord = {
    utd: utd.UniformDataType.FILE,
    uri: fileUri
    }

    Q5: 如何在多个页面使用?

    答:每个页面独立注册和注销。 建议封装成全局方法:

    // utils/knock-share.js
    export function startKnockShare(imagePath) {
    return new Promise((resolve, reject) => {
    // #ifdef APP-HARMONY
    uni.registerKnockShare({
    imagePath: imagePath,
    success: resolve,
    fail: reject
    })
    // #endif
    })
    }

    export function stopKnockShare() {
    // #ifdef APP-HARMONY
    uni.unregisterKnockShare()
    // #endif
    }

    // 在页面中使用
    import { startKnockShare, stopKnockShare } from '@/utils/knock-share.js'

    methods: {
    async handleShare() {
    try {
    await startKnockShare(this.imagePath)
    uni.showToast({ title: '请轻碰设备' })
    } catch (error) {
    uni.showToast({ title: '注册失败' })
    }
    }
    }

    Q6: 页面切换后分享还有效吗?

    答:有效,但不推荐。 建议在页面隐藏时注销监听:

    onHide() {
    // 页面隐藏时注销,避免在其他页面触发
    if (this.isSharing) {
    uni.unregisterKnockShare()
    this.isSharing = false
    }
    }

    Q7: 如何知道分享是否成功?

    答:通过回调函数。 在 registerKnockShare 的回调中处理:

    uni.registerKnockShare({
    imagePath: imagePath,
    success: (res) => {
    // 注册成功
    console.log('监听已注册')
    },
    fail: (err) => {
    // 注册失败
    console.error('注册失败:', err)
    }
    })

    // 注意:碰一碰触发后的分享结果在 native.uts 中处理
    // 如需获取分享结果,可以在 knockShareCallback 中添加回调

    Q8: Canvas 生成图片模糊怎么办?

    解决方案:

    // 1. 使用更大的 Canvas 尺寸
    const scale = 2 // 放大倍数
    const canvasWidth = 375 * scale
    const canvasHeight = 500 * scale

    // 2. 设置高质量导出
    uni.canvasToTempFilePath({
    canvasId: 'shareCanvas',
    quality: 1, // 最高质量
    destWidth: canvasWidth,
    destHeight: canvasHeight,
    success: (res) => {
    // …
    }
    }, this)

    // 3. 使用 Canvas 2D API (推荐)
    const canvas = await uni.createOffscreenCanvas({
    type: '2d',
    width: canvasWidth,
    height: canvasHeight
    })

    进阶技巧

    1. 动态生成不同类型的名片

    // 根据类型生成不同的名片
    async generateCard(type) {
    switch (type) {
    case 'baby':
    return await this.generateBabyCard()
    case 'family':
    return await this.generateFamilyCard()
    case 'event':
    return await this.generateEventCard()
    default:
    return await this.generateDefaultCard()
    }
    }

    2. 添加二维码

    // 使用第三方库生成二维码
    import QRCode from '@/utils/qrcode.js'

    methods: {
    async generateCardWithQRCode() {
    const ctx = uni.createCanvasContext('shareCanvas', this)

    // 绘制基础内容
    // …

    // 生成二维码
    const qrCodeData = QRCode.generate('https://example.com/baby/123')

    // 绘制二维码
    ctx.drawImage(qrCodeData, 250, 350, 100, 100)

    // 导出
    ctx.draw(false, () => {
    // …
    })
    }
    }

    3. 批量分享

    // 生成多张图片并依次分享
    async batchShare(imageList) {
    for (let i = 0; i < imageList.length; i++) {
    const imagePath = imageList[i]

    // 注册分享
    await new Promise((resolve) => {
    uni.registerKnockShare({
    imagePath: imagePath,
    success: () => {
    uni.showToast({
    title: `${i + 1}/${imageList.length}`,
    icon: 'none'
    })
    resolve()
    }
    })
    })

    // 等待用户碰一碰
    await this.waitForKnock()

    // 注销当前分享
    uni.unregisterKnockShare()
    }
    }

    4. 分享统计

    data() {
    return {
    shareStats: {
    totalShares: 0,
    successCount: 0,
    failCount: 0
    }
    }
    },

    methods: {
    trackShare(success) {
    this.shareStats.totalShares++
    if (success) {
    this.shareStats.successCount++
    } else {
    this.shareStats.failCount++
    }

    // 上报统计数据
    this.reportStats()
    },

    reportStats() {
    // 上报到服务器
    uni.request({
    url: 'https://api.example.com/stats',
    method: 'POST',
    data: this.shareStats
    })
    }
    }

    性能优化建议

    1. 图片缓存

    避免重复生成图片:

    data() {
    return {
    imageCache: new Map() // 缓存生成的图片
    }
    },

    methods: {
    async getOrGenerateImage(key, generator) {
    // 检查缓存
    if (this.imageCache.has(key)) {
    console.log('使用缓存图片')
    return this.imageCache.get(key)
    }

    // 生成新图片
    const imagePath = await generator()
    this.imageCache.set(key, imagePath)
    return imagePath
    },

    async shareWithCache() {
    const cacheKey = `baby_${this.babyInfo.id}`
    const imagePath = await this.getOrGenerateImage(
    cacheKey,
    () => this.generateBabyCard()
    )

    // 注册分享
    uni.registerKnockShare({ imagePath })
    }
    }

    2. 异步加载

    使用异步加载优化用户体验:

    async generateAndShare() {
    // 显示加载动画
    this.showLoading()

    try {
    // 使用 Promise.all 并行处理
    const [imagePath, userInfo] = await Promise.all([
    this.generateShareImage(),
    this.fetchUserInfo()
    ])

    // 注册分享
    await this.registerShare(imagePath)

    } catch (error) {
    this.handleError(error)
    } finally {
    this.hideLoading()
    }
    }

    3. 内存管理

    及时清理不需要的资源:

    onUnload() {
    // 注销监听
    if (this.isSharing) {
    uni.unregisterKnockShare()
    }

    // 清理缓存
    this.imageCache.clear()

    // 清理临时文件
    this.cleanTempFiles()
    },

    methods: {
    cleanTempFiles() {
    // 删除临时图片文件
    if (this.shareImage) {
    uni.removeSavedFile({
    filePath: this.shareImage,
    success: () => {
    console.log('临时文件已清理')
    }
    })
    }
    }
    }

    4. 防抖处理

    避免用户快速点击:

    methods: {
    generateAndShare: debounce(function() {
    // 分享逻辑
    }, 1000),

    // 或使用标志位
    async generateAndShare() {
    if (this.isProcessing) {
    return
    }

    this.isProcessing = true

    try {
    // 分享逻辑
    } finally {
    setTimeout(() => {
    this.isProcessing = false
    }, 1000)
    }
    }
    }

    源码解析:核心实现

    1. 接口设计

    插件使用 TypeScript 定义了清晰的接口:

    // 参数类型
    export type RegisterKnockShareOptions = {
    imagePath : string // 必填
    success ?: RegisterKnockShareSuccessCallback | null // 可选
    fail ?: RegisterKnockShareFailCallback | null
    complete ?: RegisterKnockShareCompleteCallback | null
    }

    // 回调类型
    export type RegisterKnockShareSuccessCallback = (result : RegisterKnockShareSuccess) => void

    这种设计的优势:

    • 类型安全:编译时检查参数类型
    • 可选回调:灵活的回调机制
    • 清晰的文档:类型即文档

    2. 状态管理

    使用模块级变量管理状态:

    let knockShareCallback : ((target : harmonyShare.SharableTarget) => void) | null = null
    let currentImagePath : string = ''
    let currentOptions : RegisterKnockShareOptions | null = null

    这种设计确保:

    • 单例模式:全局只有一个监听器
    • 状态隔离:避免多次注册冲突
    • 内存安全:注销时清理所有引用

    3. 错误处理

    完善的错误处理机制:

    try {
    // 执行操作
    harmonyShare.on('knockShare', knockShareCallback)

    // 成功回调
    options.success?.(successResult)
    options.complete?.(completeResult)

    } catch (error) {
    const err = error as BusinessError

    // 失败回调
    const failResult : RegisterKnockShareFail = {
    errCode: err.code,
    errMsg: err.message ?? '注册失败'
    }
    options.fail?.(failResult)
    options.complete?.(completeResult)
    }

    4. 回调链

    巧妙的回调链设计:

    // 1. 注册时的回调
    registerKnockShare({
    success: () => {
    // 注册成功
    }
    })

    // 2. 碰一碰触发时的回调
    knockShareCallback = (sharableTarget) => {
    const result = shareImageNative(sharableTarget, imagePath)

    if (result.success) {
    // 分享成功回调
    currentOptions.success?.(successResult)
    } else {
    // 分享失败回调
    currentOptions.fail?.(failResult)
    }
    }

    这种设计实现了:

    • 注册回调:告知注册是否成功
    • 分享回调:告知分享是否成功
    • 完整的生命周期:覆盖所有状态

    插件发布

    UniApp 插件市场

    jack-knock-share 插件已发布到 UniApp 插件市场,可以直接安装使用:

    插件地址: https://ext.dcloud.net.cn/plugin?name=jack-knock-share

    安装方式:

  • 访问插件市场页面
  • 点击"使用 HBuilderX 导入插件"
  • 选择项目导入
  • 重新编译运行
  • 插件特点:

    • ✅ 完全免费
    • ✅ 开源代码
    • ✅ 持续维护
    • ✅ 技术支持

    本地使用

    如果不想通过插件市场安装,也可以直接复制本文提供的完整源码到项目中使用。

    总结

    通过本文,我详细介绍了如何在 UniApp 项目中集成鸿蒙原生碰一碰分享功能。jack-knock-share 插件具有以下优势:

    ✅ 简单易用:仅需两个 API,快速集成 ✅ 自动触发:注册后自动监听碰一碰事件 ✅ 完善的回调:支持成功、失败、完成回调 ✅ 生命周期管理:支持注册和注销监听 ✅ 完全开源:本文提供完整源码 ✅ 插件市场发布:可直接安装使用

    碰一碰分享作为鸿蒙系统的特色功能,为用户提供了更加便捷的内容分享体验。无论是开发儿童成长记录应用、照片分享应用,还是社交类应用,jack-knock-share 都能满足你的需求。

    希望本文能帮助你快速掌握在 UniApp 中使用鸿蒙碰一碰分享的方法!

    参考资料

    • HarmonyOS ShareKit 官方文档
    • HarmonyOS 碰一碰分享指南
    • UniApp UTS 插件开发指南
    • UniApp 条件编译
    • UniApp 插件市场

    如有问题或建议,欢迎在评论区交流讨论!


    💡 提示:本文示例代码已在 HarmonyOS NEXT 上测试通过。如果你在使用过程中遇到问题,欢迎在评论区留言!

    ⭐ 如果本文对你有帮助,欢迎点赞、收藏、关注!也欢迎分享给更多需要的开发者!

    🔗 插件地址: https://ext.dcloud.net.cn/plugin?name=jack-knock-share

    赞(0)
    未经允许不得转载:171主机测评 » UniApp 中使用原生鸿蒙碰一碰分享技术详解
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址