本文是《React Native x HarmonyOS NEXT 创新能力接入方案》系列第 7 篇。
前面我们已经完成了 RNOH 环境搭建、TurboModule 最小实践、TTS、OCR、端侧 AI 商品图处理以及小艺智能体拉起。
本篇继续接入 HarmonyOS 典型系统级协同能力:碰一碰分享

源码:https://atomgit.com/huqi/RNHarmonySkuAssistant

1. 本篇要实现什么?

本篇我们要实现一个适合跨境电商工具场景的功能:

在 React Native 商品详情页中,点击“碰一碰分享 SKU”,将当前商品的 SKU 信息封装成分享内容,并通过 HarmonyOS 近场分享能力流转到另一台设备。

最终效果可以设计为:

RN 商品详情页
    ↓
点击“碰一碰分享”
    ↓
调用 RNOH TurboModule
    ↓
ArkTS 原生模块组装分享数据
    ↓
调用 HarmonyOS 碰一碰 / 近场分享能力
    ↓
接收端打开 SKU 详情或商品资料页面

2. 为什么选择“SKU 信息近场流转”作为场景?

原 UniApp 实践里,“碰一碰”分享更偏通用内容分享。
React Native 版我们可以做得更有业务差异化:

跨境电商场景下,SKU 信息经常需要快速流转

例如:

  • 运营把某个商品资料分享给主管;
  • 采购把供应商 SKU 信息分享给运营;
  • 美工拿到商品图和卖点后继续处理主图;
  • 线下会议中快速传递商品链接;
  • 多设备协同查看同一个商品资料。

传统方式通常是:

复制链接
    ↓
打开聊天工具
    ↓
发送给对方
    ↓
对方点击进入

而碰一碰分享的体验是:

打开商品详情
    ↓
两台设备靠近 / 触发近场交互
    ↓
接收端直接进入对应 SKU 页面

这类能力非常适合体现 HarmonyOS 的系统级协同价值。


3. 本篇技术方案

整体架构如下:

React Native 层
├── TapSharePage.tsx
├── 商品信息展示
├── 分享按钮
└── 分享状态展示

RNOH 桥接层
├── NativeTapShareModule.ts
└── TurboModule 调用声明

HarmonyOS 原生层
├── TapShareModule.ets
├── 分享参数校验
├── 分享内容构造
├── 分享结果
└── 真实碰一碰能力接入位置

调用流程:

TapSharePage.tsx
    ↓
NativeTapShareModule.shareSku(payload)
    ↓
TapShareModule.ets
    ↓
构造 HarmonyOS 分享内容
    ↓
调用系统分享能力
    ↓
Promise 返回 RN 页面

4. 运行截图

Screenshot_20260625003136640

Screenshot_20260625003200145

Screenshot_20260625003213802

Screenshot_20260625003223276

由于博主没有两台 nova 16,因此无法实际测试


5. 定义 React Native 侧 NativeModule

新建文件:

src/native/NativeTapShareModule.ts

代码如下:

import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';

export type TapShareSkuPayload = {
  skuId: string;
  spuId?: string;
  title: string;
  imageUrl?: string;
  price?: string;
  currency?: string;
  stockStatus?: 'normal' | 'low' | 'out_of_stock';
  targetRoute: string;
  sourceScene: 'sku_detail' | 'product_list' | 'inventory_warning';
};

export type TapShareResult = {
  code: number;
  message: string;
  shareId?: string;
  status: 'waiting' | 'success' | 'cancelled' | 'failed';
  targetRoute?: string;
};

export interface Spec extends TurboModule {
  shareSku(payload: TapShareSkuPayload): Promise<TapShareResult>;
  isTapShareAvailable(): Promise<boolean>;
}

export default TurboModuleRegistry.get<Spec>('TapShareModule');

这里我们定义了两个方法:

shareSku(payload)

用于发起 SKU 分享。

isTapShareAvailable()

用于判断当前设备环境是否支持碰一碰分享能力。


6. 设计分享参数结构

不要只传一个字符串。

如果只传:

"SKU123"

后面扩展会很困难。

建议从一开始就设计成结构化对象:

const payload = {
  skuId: 'SKU-HM-001',
  spuId: 'SPU-HM-2026',
  title: '不锈钢厨房置物架',
  imageUrl: 'https://example.com/sku-hm-001.png',
  price: '19.99',
  currency: 'USD',
  stockStatus: 'low',
  targetRoute: '/sku/detail?skuId=SKU-HM-001',
  sourceScene: 'sku_detail',
};

这样接收端拿到数据后,可以决定:

  • 直接打开商品详情页;
  • 打开库存预警页;
  • 打开商品编辑页;
  • 打开图片处理页;
  • 打开运营分析页。

7. 编写 React Native 页面

新建文件:

src/pages/TapSharePage.tsx

代码如下:

import React, { useEffect, useState } from 'react';
import {
  ActivityIndicator,
  Alert,
  Pressable,
  SafeAreaView,
  ScrollView,
  StyleSheet,
  Text,
  View,
} from 'react-native';

import TapShareModule, {
  TapShareResult,
  TapShareSkuPayload,
} from '../native/NativeTapShareModule';

const skuPayload: TapShareSkuPayload = {
  skuId: 'SKU-HM-001',
  spuId: 'SPU-HM-2026',
  title: '不锈钢厨房置物架',
  imageUrl: 'https://example.com/sku-hm-001.png',
  price: '19.99',
  currency: 'USD',
  stockStatus: 'low',
  targetRoute: '/sku/detail?skuId=SKU-HM-001',
  sourceScene: 'sku_detail',
};

export default function TapSharePage() {
  const [available, setAvailable] = useState<boolean | null>(null);
  const [sharing, setSharing] = useState(false);
  const [result, setResult] = useState<TapShareResult | null>(null);

  useEffect(() => {
    checkAvailable();
  }, []);

  async function checkAvailable() {
    try {
      const supported = await TapShareModule.isTapShareAvailable();
      setAvailable(supported);
    } catch (error) {
      console.warn('check tap share available failed:', error);
      setAvailable(false);
    }
  }

  async function handleShareSku() {
    try {
      setSharing(true);
      setResult({
        code: 1001,
        message: '正在等待设备靠近,请使用支持碰一碰能力的设备进行分享',
        status: 'waiting',
      });

      const res = await TapShareModule.shareSku(skuPayload);
      setResult(res);

      if (res.status === 'success') {
        Alert.alert('分享成功', res.message);
      } else if (res.status === 'cancelled') {
        Alert.alert('已取消', res.message);
      } else {
        Alert.alert('分享失败', res.message);
      }
    } catch (error) {
      console.error('share sku failed:', error);
      setResult({
        code: -1,
        message: '调用碰一碰分享失败,请查看日志',
        status: 'failed',
      });
    } finally {
      setSharing(false);
    }
  }

  const stockText =
    skuPayload.stockStatus === 'low'
      ? '库存偏低'
      : skuPayload.stockStatus === 'out_of_stock'
        ? '已断货'
        : '库存正常';

  return (
    <SafeAreaView style={styles.safeArea}>
      <ScrollView contentContainerStyle={styles.container}>
        <Text style={styles.title}>SKU 碰一碰分享</Text>
        <Text style={styles.subtitle}>
          React Native x HarmonyOS 近场分享能力接入方案
        </Text>

        <View style={styles.card}>
          <Text style={styles.label}>商品标题</Text>
          <Text style={styles.productTitle}>{skuPayload.title}</Text>

          <View style={styles.row}>
            <Text style={styles.rowLabel}>SKU</Text>
            <Text style={styles.rowValue}>{skuPayload.skuId}</Text>
          </View>

          <View style={styles.row}>
            <Text style={styles.rowLabel}>售价</Text>
            <Text style={styles.rowValue}>
              {skuPayload.currency} {skuPayload.price}
            </Text>
          </View>

          <View style={styles.row}>
            <Text style={styles.rowLabel}>库存状态</Text>
            <Text style={styles.warningValue}>{stockText}</Text>
          </View>

          <View style={styles.row}>
            <Text style={styles.rowLabel}>目标路由</Text>
            <Text style={styles.routeValue}>{skuPayload.targetRoute}</Text>
          </View>
        </View>

        <View style={styles.statusCard}>
          <Text style={styles.statusTitle}>设备能力检测</Text>
          <Text style={styles.statusText}>
            {available === null
              ? '检测中...'
              : available
                ? '当前设备支持碰一碰分享'
                : '当前环境暂未检测到碰一碰能力,使用 Mock 分享链路'}
          </Text>
        </View>

        <Pressable
          style={[styles.button, sharing && styles.buttonDisabled]}
          disabled={sharing}
          onPress={handleShareSku}
        >
          {sharing ? (
            <ActivityIndicator />
          ) : (
            <Text style={styles.buttonText}>碰一碰分享 SKU</Text>
          )}
        </Pressable>

        {result ? (
          <View style={styles.resultCard}>
            <Text style={styles.resultTitle}>分享结果</Text>
            <Text style={styles.resultText}>状态:{result.status}</Text>
            <Text style={styles.resultText}>编码:{result.code}</Text>
            <Text style={styles.resultText}>说明:{result.message}</Text>
            {result.shareId ? (
              <Text style={styles.resultText}>Share ID:{result.shareId}</Text>
            ) : null}
          </View>
        ) : null}
      </ScrollView>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  safeArea: {
    flex: 1,
    backgroundColor: '#F7F8FA',
  },
  container: {
    padding: 20,
  },
  title: {
    fontSize: 26,
    fontWeight: '700',
    color: '#111827',
    marginTop: 12,
  },
  subtitle: {
    fontSize: 15,
    color: '#6B7280',
    marginTop: 8,
    lineHeight: 22,
  },
  card: {
    backgroundColor: '#FFFFFF',
    borderRadius: 18,
    padding: 18,
    marginTop: 24,
  },
  label: {
    fontSize: 13,
    color: '#6B7280',
  },
  productTitle: {
    fontSize: 20,
    color: '#111827',
    fontWeight: '700',
    marginTop: 6,
    marginBottom: 16,
  },
  row: {
    paddingVertical: 10,
    borderTopWidth: StyleSheet.hairlineWidth,
    borderTopColor: '#E5E7EB',
  },
  rowLabel: {
    fontSize: 13,
    color: '#6B7280',
    marginBottom: 4,
  },
  rowValue: {
    fontSize: 15,
    color: '#111827',
  },
  warningValue: {
    fontSize: 15,
    color: '#B45309',
    fontWeight: '600',
  },
  routeValue: {
    fontSize: 14,
    color: '#2563EB',
  },
  statusCard: {
    backgroundColor: '#FFFFFF',
    borderRadius: 18,
    padding: 18,
    marginTop: 16,
  },
  statusTitle: {
    fontSize: 16,
    color: '#111827',
    fontWeight: '600',
  },
  statusText: {
    fontSize: 14,
    color: '#4B5563',
    lineHeight: 21,
    marginTop: 8,
  },
  button: {
    marginTop: 20,
    backgroundColor: '#0A59F7',
    borderRadius: 16,
    paddingVertical: 14,
    alignItems: 'center',
  },
  buttonDisabled: {
    opacity: 0.6,
  },
  buttonText: {
    fontSize: 16,
    color: '#FFFFFF',
    fontWeight: '600',
  },
  resultCard: {
    backgroundColor: '#FFFFFF',
    borderRadius: 18,
    padding: 18,
    marginTop: 20,
  },
  resultTitle: {
    fontSize: 16,
    color: '#111827',
    fontWeight: '600',
    marginBottom: 10,
  },
  resultText: {
    fontSize: 14,
    color: '#374151',
    lineHeight: 22,
  },
});

8. ArkTS 原生模块

用来验证 RNOH 桥接链路。

新建文件:

harmony/entry/src/main/ets/turbomodule/TapShareModule.ets

示例代码:

import { harmonyShare, systemShare } from '@kit.ShareKit';
import { uniformTypeDescriptor as utd } from '@kit.ArkData';
import { UITurboModule, UITurboModuleContext } from '@rnoh/react-native-openharmony';

interface TapShareSkuPayload {
  skuId: string;
  spuId?: string;
  title: string;
  imageUrl?: string;
  price?: string;
  currency?: string;
  stockStatus?: string;
  targetRoute: string;
  sourceScene: string;
}

interface TapShareResult {
  code: number;
  message: string;
  shareId?: string;
  status: string;
  targetRoute?: string;
}

export class TapShareModule extends UITurboModule {
  static readonly NAME = 'TapShareModule';
  private currentPayload: TapShareSkuPayload | null = null;
  private isRegistered: boolean = false;
  private readonly knockShareCallback = (sharableTarget: harmonyShare.SharableTarget): void => {
    this.handleKnockShare(sharableTarget).catch((err: Error) => {
      console.error(`[TapShareModule] knockShare failed: ${err.message}`);
    });
  };

  constructor(ctx: UITurboModuleContext) {
    super(ctx);
  }

  async isTapShareAvailable(): Promise<boolean> {
    return canIUse('SystemCapability.Collaboration.HarmonyShare');
  }

  async shareSku(payload: TapShareSkuPayload): Promise<TapShareResult> {
    if (!payload || !payload.skuId || !payload.title || !payload.targetRoute) {
      return {
        code: 4001,
        message: '分享参数不完整,请检查 skuId、title、targetRoute',
        status: 'failed',
      };
    }

    if (!await this.isTapShareAvailable()) {
      return {
        code: 3001,
        message: '当前系统不支持 Share Kit 碰一碰分享,请确认 HarmonyOS NEXT 版本与设备能力。',
        status: 'failed',
      };
    }

    this.currentPayload = payload;
    if (!this.isRegistered) {
      harmonyShare.on('knockShare', this.knockShareCallback);
      this.isRegistered = true;
      console.info(`[TapShareModule] knockShare registered: ${JSON.stringify(payload)}`);
      return {
        code: 1001,
        message: '已注册碰一碰分享,请将两台亮屏解锁手机顶部轻碰。',
        shareId: `knock_share_${Date.now()}`,
        status: 'waiting',
        targetRoute: payload.targetRoute,
      };
    }

    console.info(`[TapShareModule] knockShare payload updated: ${JSON.stringify(payload)}`);
    return {
      code: 1002,
      message: '已更新待分享 SKU,保持当前页面等待系统碰一碰回调即可。',
      shareId: `knock_share_${Date.now()}`,
      status: 'waiting',
      targetRoute: payload.targetRoute,
    };
  }

  async stopShare(): Promise<boolean> {
    if (!this.isRegistered) {
      this.currentPayload = null;
      return true;
    }

    harmonyShare.off('knockShare', this.knockShareCallback);
    this.currentPayload = null;
    this.isRegistered = false;
    console.info('[TapShareModule] knockShare unregistered');
    return true;
  }

  private async handleKnockShare(sharableTarget: harmonyShare.SharableTarget): Promise<void> {
    if (!this.currentPayload) {
      await sharableTarget.clarifyNonShare({ message: '当前页面暂无可分享 SKU,请返回支持碰一碰分享的界面再试。' });
      return;
    }

    const shareData = this.buildShareData(this.currentPayload);
    console.info('[TapShareModule] knockShare triggered');
    await sharableTarget.share(shareData);
  }

  private buildShareData(payload: TapShareSkuPayload): systemShare.SharedData {
    const record: systemShare.SharedRecord = {
      utd: utd.UniformDataType.HYPERLINK,
      content: this.buildShareLink(payload),
      title: payload.title,
      description: this.buildDescription(payload),
      extraData: {
        skuId: payload.skuId,
        spuId: payload.spuId ?? '',
        sourceScene: payload.sourceScene,
      },
    };

    return new systemShare.SharedData(record);
  }

  private buildShareLink(payload: TapShareSkuPayload): string {
    const params: Array<string> = [
      `skuId=${encodeURIComponent(payload.skuId)}`,
      `title=${encodeURIComponent(payload.title)}`,
      `targetRoute=${encodeURIComponent(payload.targetRoute)}`,
      `sourceScene=${encodeURIComponent(payload.sourceScene)}`,
    ];

    if (payload.spuId) {
      params.push(`spuId=${encodeURIComponent(payload.spuId)}`);
    }
    if (payload.imageUrl) {
      params.push(`imageUrl=${encodeURIComponent(payload.imageUrl)}`);
    }
    if (payload.price) {
      params.push(`price=${encodeURIComponent(payload.price)}`);
    }
    if (payload.currency) {
      params.push(`currency=${encodeURIComponent(payload.currency)}`);
    }
    if (payload.stockStatus) {
      params.push(`stockStatus=${encodeURIComponent(payload.stockStatus)}`);
    }

    return `skuassistant://demo/sku/detail?${params.join('&')}`;
  }

  private buildDescription(payload: TapShareSkuPayload): string {
    const priceText = payload.price ? `${payload.currency ?? 'USD'} ${payload.price}` : '价格待定';
    return `${priceText} · ${payload.stockStatus ?? 'normal'} · ${payload.targetRoute}`;
  }

  __onDestroy__(): void {
    this.stopShare().catch(() => undefined);
    super.__onDestroy__();
  }
}


9. 注册 TurboModule(RNOH 0.84.1 实装)

第一层在 harmony/entry/src/main/ets/GeneratedPackage.ets 注册 ArkTS UITurboModule

import { TapShareModule } from './turbomodule/TapShareModule';

return new Map([
  [TapShareModule.NAME, (ctx) => new TapShareModule(ctx)],
]);

本工程的自定义 TurboModule 采用 ArkTS 实现 + C++ JSI 元数据 双层注册。只注册 ArkTS 时,编译能够通过,但真机运行会报 Couldn't find Turbo Module on the ArkTs side

第二层由 TapShareModule.cpp 声明 JS 可调用方法:

methodMap_ = {
    ARK_ASYNC_METHOD_METADATA(shareSku, 1),
    ARK_ASYNC_METHOD_METADATA(isTapShareAvailable, 0),
};

TapSharePackage.h 创建 C++ TurboModule,最后在 PackageProvider.cpp 加入:

packages.push_back(std::make_shared<TapSharePackage>(ctx));

对应 .cpp 还必须加入 CMakeLists.txt。RN spec、C++ name 和 ArkTS NAME 必须全部保持为 TapShareModule


10. 真实 HarmonyOS 碰一碰能力接入位置

真实能力替换的位置在 ArkTS 侧:

async shareSku(payload: TapShareSkuPayload): Promise<TapShareResult> {
  // 1. 校验 payload
  // 2. 构造分享内容
  // 3. 调用 HarmonyOS 碰一碰 / 近场分享 / 系统协同能力
  // 4. 监听分享状态
  // 5. 将结果返回 RN
}

可以把分享内容抽象成:

type HarmonyShareContent = {
  title: string
  description: string
  thumbnailUri?: string
  deepLink: string
  extraData: Record<string, string>
}

例如:

const content = {
  title: payload.title,
  description: `SKU:${payload.skuId},库存状态:${payload.stockStatus}`,
  thumbnailUri: payload.imageUrl,
  deepLink: `rnharmony://sku/detail?skuId=${payload.skuId}`,
  extraData: {
    skuId: payload.skuId,
    spuId: payload.spuId ?? '',
    sourceScene: payload.sourceScene
  }
}

接收端拿到内容后,可以根据 deepLink 打开对应页面。


11. Deep Link 与接收端页面恢复

碰一碰分享不只是把一段文本发出去,更重要的是接收端能恢复业务页面。

建议设计统一路由:

rnharmony://sku/detail?skuId=SKU-HM-001

接收端处理逻辑:

收到分享内容
    ↓
解析 deepLink
    ↓
读取 skuId
    ↓
请求商品详情
    ↓
打开 SKU 详情页

React Native 侧可以使用 Linking 处理:

import { Linking } from 'react-native';

useEffect(() => {
  const subscription = Linking.addEventListener('url', event => {
    console.log('received deep link:', event.url);
    // parse url and navigate
  });

  return () => {
    subscription.remove();
  };
}, []);

如果你使用 React Navigation,可以在路由配置中加入:

const linking = {
  prefixes: ['rnharmony://'],
  config: {
    screens: {
      SkuDetail: 'sku/detail',
    },
  },
};

12. 错误码设计

建议不要只返回 true / false
分享类能力容易出现多种失败原因,应该从一开始设计错误码。

code status 含义
0 success 分享成功
1001 waiting 等待设备靠近
2001 cancelled 用户取消分享
3001 failed 当前设备不支持
3002 failed 系统分享服务不可用
4001 failed 参数不完整
5001 failed 原生能力调用异常

RN 页面可以根据错误码展示不同提示:

if (res.code === 3001) {
  Alert.alert('当前设备暂不支持碰一碰分享');
}

13. 常见问题

问题 1:RN 调用后没有反应

优先检查:

1. NativeTapShareModule.ts 中模块名是否为 TapShareModule
2. ArkTS 注册模块名是否一致
3. Codegen 是否重新生成
4. DevEco Studio 是否重新编译
5. Metro 是否加载了最新 JS

问题 2:真实设备没有出现碰一碰入口

优先检查:

1. 当前设备是否支持相关近场分享能力
2. 系统版本是否满足要求
3. 权限是否已申请
4. 是否需要在 AGC 或配置文件中开通对应能力
5. 是否在真机上验证,而不是仅在模拟器上验证

问题 3:接收端无法打开页面

优先检查:

1. deepLink scheme 是否配置正确
2. RN Linking 是否监听成功
3. React Navigation linking 配置是否正确
4. 接收端是否安装了应用
5. 分享内容中的 targetRoute 是否完整

问题 4:分享内容太大

近场分享不建议直接传完整商品详情。

推荐只传:

skuId
spuId
title
thumbnail
deepLink
sourceScene

商品详情由接收端通过 skuId 再请求。


14. 本篇小结

仓库验证结果(2026-06-24)

npm test -- --runInBand
npm run lint
npx tsc --noEmit
cd harmony && /Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw \
  --mode module -p module=entry@default -p product=default assembleHap --no-daemon

自动化测试 4/4 通过,HAP 编译、打包、签名和真机安装成功。nova 16 真机验证了页面加载、结构化 payload、实现成功结果与 Share ID。真实碰一碰、接收端设备发现和系统弹窗仍不在本次实装范围内。

本篇完成了 React Native 调用 HarmonyOS 碰一碰分享能力的基础设计:

RN 商品详情页
    ↓
NativeTapShareModule.shareSku()
    ↓
RNOH TurboModule
    ↓
ArkTS TapShareModule
    ↓
碰一碰能力
    ↓
结果返回 RN

更重要的是,我们把分享对象从普通文本升级成了业务结构化数据:

SKU 信息
商品标题
库存状态
目标路由
Deep Link
来源场景

这样接收端不是简单“收到一段文字”,而是可以直接恢复业务页面。


15. 下一篇预告

下一篇继续接入 HarmonyOS 交互协同能力:

RNOH x HarmonyOS 智感握姿:原生事件驱动 RN UI 自适应

下一篇会实现:

ArkTS 监听握姿状态
    ↓
通过事件发送给 RN
    ↓
RN 根据左右手握持状态调整悬浮按钮位置
Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐