小白的鸿蒙适配之旅:让 react-native-share 跑在 HarmonyOS 上

写给和我一样刚开始接触鸿蒙适配的同学。这篇文章会从「我什么都不懂」开始,一步一步带你完成 react-native-share 这个 React Native 分享库的鸿蒙化。不跳步,不省略,所有踩过的坑都会写出来。


第零章 · 开始之前:你需要知道的概念

如果你已经是老手,可以跳过这章直接看第一章。

0.1 什么是 React Native?

React Native(简称 RN)是 Facebook 出的一个框架,让你用 JavaScript 写代码,就能同时跑在 Android 和 iOS 上。比如你写了一个「分享到微信」的功能,Android 和 iOS 都能用同一套 JS 代码调用。

0.2 什么是「三方库」?

别人写好的、可以拿来用的代码包。react-native-share 就是一个三方库——它帮你在 RN 里一行代码搞定分享到 Facebook、WhatsApp 等社交平台。

0.3 什么是 HarmonyOS / 鸿蒙?

华为自己做的操作系统。最新的叫 HarmonyOS 6,和 Android 不兼容了,需要用华为自己的语言(ArkTS/仓颉)来写原生代码。

0.4 什么是「适配」?

原来的 react-native-share 只能在 Android/iOS 上跑。我们要让它也能在鸿蒙上跑。这个过程就叫适配。

打个比方:你有一个充电器(三方库),以前只能插 Android 和 iPhone 的接口。现在华为换了新接口,你需要做一个转接头(适配层),让充电器也能给华为手机充电。

0.5 几个关键词速查

术语 一句话解释
TurboModule RN 新架构中 JS 调用原生代码的方式,你可以理解为一个「桥梁」
Want 鸿蒙里用来启动其他应用的对象,类似 Android 的 Intent
ETS Extended TypeScript,鸿蒙的 ArkUI 声明式开发语言,本质是 TypeScript 的扩展
HAR 鸿蒙的模块包格式,类似 Android 的 AAR
bundleName 鸿蒙里应用的唯一标识,类似 Android 的包名(com.xxx.xxx)
scheme URL 协议名,比如 whatsapp:// 可以直接打开 WhatsApp
RNOH React Native OpenHarmony 的缩写,RN 在鸿蒙上的运行时

第一章 · 搞清楚我们要做什么

1.1 原库有什么功能?

react-native-share 提供了 4 个主要功能:

1. open(options)          → 调起系统分享面板,让用户自己选分享到哪
2. shareSingle(options)   → 直接分享到指定的 APP(比如直接分享到微信)
3. isPackageInstalled()   → 检查某个 APP 有没有装
4. isBase64File()         → 判断一个文件是不是 base64 格式的

其中 shareSingle 支持 21 个社交平台:

Facebook、FacebookStories、Twitter、WhatsApp、WhatsAppBusiness、Instagram、InstagramStories、GooglePlus、Email、Pinterest、LinkedIn、SMS、Telegram、Snapchat、Messenger、Viber、Discord、Weibo(微博)、Douyin(抖音)、TestShare(测试用)

1.2 我们要写什么?

简单来说:用鸿蒙的语言(仓颉/ArkTS)把上面这些功能全部重写一遍。

具体来说,我们需要:

JS 层(已有,需小改)
  ↓ 调用
TurboModule(需要新写,是 JS 和鸿蒙原生之间的桥梁)
  ↓ 调用
鸿蒙原生代码(需要新写,用 Want 拉起各种 APP 分享)

1.3 找一个参考项目

不要从零开始!找一个已经适配好的类似项目,照着它的结构来。

我选了 rntpc_react-native-tts(一个文字转语音的库),因为它:

  • 也是 React Native 的三方库
  • 已经完成了鸿蒙适配
  • 结构和我们要做的类似(都是 TurboModule)

第二章 · 搭建项目骨架

2.1 创建目录结构

先看看参考项目 react-native-tts 的目录是什么样的,然后照着搭:

rntpc_react-native-tts/harmony/rn_tts/     ← 参考项目
  ├── index.ets
  ├── ts.ts
  ├── oh-package.json5
  ├── build-profile.json5
  └── src/main/
      ├── module.json5
      └── ets/
          ├── RNTTSPackage.ts
          ├── RNTTSTurboModule.ts
          └── ...

react-native-share/harmony/rn_share/       ← 我们要创建的
  ├── index.ets
  ├── ts.ts
  ├── oh-package.json5
  ├── build-profile.json5
  └── src/main/
      ├── module.json5
      └── ets/
          ├── RNSharePackage.ts
          ├── RNShareTurboModule.ts
          └── ...

关键点:目录命名规则是 harmony/rn_xxx/xxx 是库名。

2.2 配置文件

每个配置文件都有它的作用,我来逐一解释:

oh-package.json5 — 告诉鸿蒙「这个模块叫什么、依赖什么」:

{
  "name": "@react-native-ohos/react-native-share",  // 模块名,固定格式
  "version": "12.3.1-0.0.1",                         // 版本号:原库版本-适配版本
  "description": "React Native Share module for OpenHarmony",
  "main": "index.ets",                                // 入口文件
  "author": "jianguo",
  "license": "MIT",
  "dependencies": {
    "@rnoh/react-native-openharmony": "file:../react_native_openharmony"  // RNOH 依赖
  }
}

💡 小贴士:版本号 12.3.1-0.0.1 中,12.3.1 是原库版本,0.0.1 是我们的适配版本。第一次适配就用 0.0.1

module.json5 — 告诉鸿蒙「这个模块是什么类型、能在什么设备上跑」:

{
  "module": {
    "name": "rn_share",
    "type": "har",                    // har = 鸿蒙的库模块(类似 Android 的 aar)
    "deviceTypes": ["default", "tablet", "2in1"]  // 支持手机、平板、2in1 设备
  }
}

index.ets — 模块入口,就一行:

export * from './src/main/ets/RNShareTurboModule';

ts.ts — TypeScript 导出,也是一行:

export * from "./ts";

🤔 你可能问:为什么要有 index.ets 和 ts.ts 两个文件?
这是 RNOH 的规范。index.ets 给鸿蒙构建系统用,ts.ts 给 TypeScript 编译器用。照着写就行。

2.3 最简 RNPackage 注册

先写一个最简单的注册文件,后面再完善:

// RNSharePackage.ts
import type { TurboModule, TurboModuleContext } from '@rnoh/react-native-openharmony/ts';
import { RNPackage, TurboModulesFactory } from '@rnoh/react-native-openharmony/ts';

class RNShareTurboModuleFactory extends TurboModulesFactory {
  createTurboModule(name: string): TurboModule | null {
    if (name === 'RNShare') {
      // 后面会替换成真正的 TurboModule
      return null;
    }
    return null;
  }
}

export class RNSharePackage extends RNPackage {
  createTurboModulesFactory(ctx: TurboModuleContext): TurboModulesFactory {
    return new RNShareTurboModuleFactory(ctx);
  }
}

🧠 这段代码的意思:当 JS 层调用 TurboModuleRegistry.get('RNShare') 时,鸿蒙端会通过这个 Factory 创建对应的原生模块。


第三章 · 先搞懂鸿蒙的「Want」机制

在写分享功能之前,必须先理解鸿蒙的 Want。这是整个适配的核心。

3.1 什么是 Want?

Want 是鸿蒙里用来「启动另一个应用」的对象。你告诉系统:「我想打开包名为 com.whatsapp 的应用,传一些数据过去」,系统就会帮你拉起那个应用。

打个比方:Want 就像一封信 📨

  • 收件人(bundleName):你要寄给谁
  • 信的内容(parameters):你寄了什么
  • 寄信方式(action):你是用快递还是平信

3.2 一个最简单的 Want

let want: Want = {
  deviceId: '',                          // 空字符串 = 本设备
  bundleName: 'com.whatsapp',            // 目标应用的包名
  action: 'ohos.want.action.sendData',   // 动作:发送数据
  parameters: {                           // 传给目标应用的参数
    'ohos.extra.param.key.contentTitle': '分享标题',
    'ohos.extra.param.key.shareAbstract': '分享内容',
  }
};

// 用这封信去启动应用
context.startAbility(want);

3.3 对应到 Android 的概念

Android HarmonyOS 说明
Intent Want 都是用来启动应用的对象
startActivity(intent) context.startAbility(want) 都是启动应用的方法
intent.setAction() want.action 设置动作
intent.putExtras() want.parameters 传递参数
intent.setType() want.type 设置数据类型
PackageManager bundleManager 查询应用信息

3.4 常用的 Want action

ohos.want.action.sendData   → 发送数据(分享用这个)
ohos.want.action.viewData   → 查看数据(打开浏览器用这个)
ohos.want.action.select     → 选择应用(弹出选择面板用这个)

第四章 · 设计架构:画出蓝图再写代码

4.1 为什么要先设计架构?

想象你要盖 21 栋房子。如果你不设计图纸,每栋房子都从零开始想怎么盖,你会:

  • 重复做很多一样的工作(砌墙、装窗户)
  • 可能每栋房子的门都不一样高,住进去很混乱

如果先画好图纸,确定「基础结构都是一样的,只是外观不同」,那盖房子就快多了。

4.2 我们要「盖」的 21 栋房子

21 个社交平台的分享,每家都不一样,但核心行为是一样的:

  1. 构造一个 Want
  2. 调用 context.startAbility(want) 拉起目标应用
  3. 处理结果(成功/失败/应用未安装)

所以我们可以这样设计:

                    ShareBaseInstance(基类 = 房子的地基)
                    ├── bundleName       包名
                    ├── scheme           URL协议
                    ├── storeId          应用商店ID
                    ├── shareUrl         Web分享链接
                    │
                    ├── shareWant()          拉起应用(通用方法)
                    ├── isInstalledPackage() 检查是否安装(通用方法)
                    ├── openAppStore()       跳应用商店(通用方法)
                    ├── pastToPasteboard()   复制到剪贴板(通用方法)
                    │
                    └── shareSingleWant()    构造 Want(每个平台不一样,子类覆写)
                         │
                         ├── WhatsAppShare      → 只需传包名,不覆写
                         ├── FacebookShare      → 覆写,用浏览器打开Web链接
                         ├── InstagramStoriesShare → 覆写,传背景图/贴纸
                         ├── DouyinShare        → 覆写,处理图片/视频分类
                         ├── WeiboShare         → 覆写,用自定义媒体对象
                         └── ... 共 21 个子类

4.3 为什么这样设计好?

对比 不用基类 用基类
WhatsApp 代码量 ~50 行 3 行
新增一个平台 复制 50 行改改 写 3 行构造函数
修改通用逻辑 21 个文件都改 改 1 个基类
代码一致性 每个文件风格可能不同 自动保持一致

第五章 · 开始写代码:从类型定义开始

5.1 Types.ts — 先定义好所有类型

为什么先写类型?因为在 TypeScript 里,类型就像「设计图纸」。先画好图纸,后面写代码就有了参考,不容易出错。

// Types.ts

// 第一步:定义所有支持的社交平台
export enum Social {
  Facebook = 'facebook',
  Generic = 'generic',              // 系统分享面板
  FacebookStories = 'facebookstories',
  Twitter = 'twitter',
  Whatsapp = 'whatsapp',
  Whatsappbusiness = 'whatsappbusiness',
  Instagram = 'instagram',
  InstagramStories = 'instagramstories',
  Googleplus = 'googleplus',
  Email = 'email',
  Pinterest = 'pinterest',
  Linkedin = 'linkedin',
  Sms = 'sms',
  Telegram = 'telegram',
  Snapchat = 'snapchat',
  Messenger = 'messenger',
  Viber = 'viber',
  Discord = 'discord',
  Weibo = 'weibo',                  // 微博
  Douyin = 'douyin',                // 抖音
  Testshare = 'testshare'           // 测试用
}

// 第二步:定义分享选项(JS 层传过来的参数)
export interface ShareOptions {
  social?: string;           // 分享到哪个平台
  message?: string;          // 分享的文字
  title?: string;            // 标题
  url?: string;              // 单个文件URL
  urls?: string[];           // 多个文件URL
  type?: string;             // 文件类型(image/png 等)
  subject?: string;          // 主题
  email?: string;            // 邮件地址
  recipient?: string;        // 收件人(短信/邮件)
  excludedActivityTypes?: string[];  // 排除的应用
  failOnCancel?: boolean;    // 用户取消时是否算失败
  filename?: string;         // 文件名
  filenames?: string[];      // 多文件名
  saveToFiles?: boolean;     // 是否保存到文件
}

// 第三步:定义定向分享的选项(必须指定 social)
export interface ShareSingleOptions extends ShareOptions {
  social: string;  // 注意:这里没有 ? 号,表示必填
}

// 第四步:定义返回结果
export interface ShareSingleResult {
  message: string;
  success: boolean;
}

🧠 interfaceenum 的区别:

  • enum 是一组固定的选项,比如社交平台就那 21 个
  • interface 是一个数据结构,描述参数长什么样

第六章 · 核心代码:基类 ShareBaseInstance

这是整个项目最重要的文件。理解了它,后面所有分享平台就都懂了。

6.1 构造函数

export class ShareBaseInstance {
  constructor(
    public bundleName: string,   // 目标APP的包名
    public scheme: string,       // URL协议(如 whatsapp://)
    public storeId: string,      // 应用商店ID
    public shareUrl?: string     // Web分享链接(可选)
  ) {}
}

用的时候:

// WhatsApp:包名 + scheme + 商店ID
new ShareBaseInstance("com.whatsapp", 'whatsapp://', '123')

// Facebook:多一个 Web 分享链接
new ShareBaseInstance("com.facebook.katana", 'facebook://', '123',
                      'https://www.facebook.com/sharer/sharer.php')

6.2 shareWant() — 拉起目标应用

这是最核心的方法。所有分享最终都会走到这里:

public async shareWant(want: Want, context: common.UIAbilityContext): Promise<ShareSingleResult> {
  try {
    // 尝试启动目标应用
    await context.startAbility(want);
    return { success: true, message: '成功打开 ' + this.bundleName };
  } catch (err) {
    const error = err as BusinessError;
    // 如果目标应用没安装(错误码 16000050 或 16000001)
    if (error.code === 16000050 || error.code === 16000001) {
      // 尝试跳转到应用商店
      return this.openAppStore(context);
    }
    // 其他错误
    return { success: false, message: error.message };
  }
}

流程图:

shareWant(want)
    │
    ├── context.startAbility(want)  ← 尝试拉起目标应用
    │       │
    │       ├── 成功 → 返回 { success: true }
    │       │
    │       └── 失败(应用未安装)
    │               │
    │               └── openAppStore()  ← 跳转应用商店
    │
    └── 失败(其他错误)→ 返回 { success: false }

6.3 shareSingleWant() — 构造默认 Want

子类如果没有特殊需求,直接用这个默认实现:

public async shareSingleWant(options: ShareSingleOptions,
    context: common.UIAbilityContext): Promise<ShareSingleResult> {

  let want: Want = {
    deviceId: '',
    bundleName: this.bundleName,           // 我要打开谁
    uri: options.url,                       // 分享的链接
    action: 'ohos.want.action.sendData',   // 动作:发送数据
    parameters: {
      'ability.params.backToOtherMissionStack': true,  // 分享后返回原来的应用
      'ohos.extra.param.key.shareUrl': options.url,
      'ohos.extra.param.key.contentTitle': options.title || '',
      'ohos.extra.param.key.shareAbstract': options.subject || options.message || '',
    }
  };

  const res = await this.shareWant(want, context);
  return res;
}

6.4 isInstalledPackage() — 检查应用是否安装

public async isInstalledPackage(): Promise<boolean> {
  try {
    await bundleManager.getBundleInfoForSelf(this.bundleName);
    return true;
  } catch (err) {
    return false;
  }
}

6.5 openAppStore() — 跳转应用商店

当目标应用没安装时,引导用户去安装:

private async openAppStore(context: common.UIAbilityContext): Promise<ShareSingleResult> {
  // 先把分享内容复制到剪贴板,方便用户安装后粘贴
  this.pastToPasteboard(options.message || options.url, 'text/plain');

  // 拉起华为应用商店
  let want: Want = {
    action: 'ohos.want.action.viewData',
    entities: ['entity.system.browsable'],
    uri: `store://appgallery.huawei.com/appDetail?id=${this.storeId}`,
  };
  await context.startAbility(want);
  return { success: false, message: '应用未安装,已跳转应用商店' };
}

6.6 pastToPasteboard() — 复制到剪贴板

protected pastToPasteboard(subject: string, MIME: string) {
  let systemPasteboard = pasteboard.getSystemPasteboard();
  let pasteData = pasteboard.createData(MIME, subject);
  systemPasteboard.clearDataSync();
  systemPasteboard.setDataSync(pasteData);
}

💡 为什么跳应用商店前要复制到剪贴板?因为用户去安装应用后回来,可能忘了之前要分享什么内容。复制到剪贴板后,用户可以直接粘贴。


第七章 · 写 21 个分享平台

终于到实战了!根据复杂程度,我分三类来讲。

7.1 第一类:3 行代码搞定(12 个平台)

这些平台很简单:构造函数传个包名和 scheme,基类的默认实现就能工作。

// WhatsApp
export class WhatsAppShare extends ShareBaseInstance {
  constructor() {
    super("com.whatsapp", 'whatsapp://', '123')
  }
}

// WhatsApp Business
export class WhatsAppBusinessShare extends ShareBaseInstance {
  constructor() {
    super("com.whatsapp.w4b", 'whatsapp://', '123')
  }
}

// Telegram
export class TeleGramShare extends ShareBaseInstance {
  constructor() {
    super("org.telegram.messenge", 'tg://', '123')
  }
}

// Discord
export class DiscordShare extends ShareBaseInstance {
  constructor() {
    super("com.discord", 'discord://', '1213')
  }
}

// Snapchat
export class SnapChatShare extends ShareBaseInstance {
  constructor() {
    super("com.snapchat.android", 'snapchat://', '123')
  }
}

// Messenger
export class MessengerShare extends ShareBaseInstance {
  constructor() {
    super("com.facebook.orca", 'fb-messenger://', '123')
  }
}

// Viber
export class ViberShare extends ShareBaseInstance {
  constructor() {
    super("com.viber.voip", 'viber://', '123')
  }
}

// Email
export class EmailShare extends ShareBaseInstance {
  constructor() {
    super("com.google.android.gm", 'gm://', '123')
  }
}

// Instagram(非 Stories)
export class InstagramShare extends ShareBaseInstance {
  constructor() {
    super("com.instagram.android", 'instagram://', '123')
  }
}

// Twitter
export class TwitterShare extends ShareBaseInstance {
  constructor() {
    super("com.twitter.android", 'twitter://', '123')
  }
}

// Google+
export class GooglePlusShare extends ShareBaseInstance {
  constructor() {
    super("com.google.android.apps.plus", '', '123')
  }
}

🧠 没错,就这么简单!基类帮你干了所有活。这就是架构设计的威力。

7.2 第二类:用浏览器打开 Web 分享链接(5 个平台)

Facebook、Pinterest、LinkedIn 这些平台在鸿蒙上没有原生 SDK,但它们有 Web 分享接口。我们用 Want 拉起浏览器来分享:

原理

用户点「分享到 Facebook」
    ↓
构造一个 URL:https://www.facebook.com/sharer/sharer.php?u=分享链接
    ↓
用 Want 拉起浏览器打开这个 URL
    ↓
浏览器显示 Facebook 的分享页面

代码

// Facebook
export class FacebookShare extends ShareBaseInstance {
  constructor() {
    super("com.facebook.katana", 'facebook://', '123',
          'https://www.facebook.com/sharer/sharer.php')
  }

  // 覆写 shareSingleWant,不用默认的
  public async shareSingleWant(options: ShareSingleOptions,
      context: common.UIAbilityContext): Promise<ShareSingleResult> {
    let want: Want = {
      entities: ['entity.system.browsable'],           // 表示可以浏览
      uri: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(options.url)}`,
      action: 'ohos.want.action.viewData'              // 用浏览器打开
    };
    const res = await this.shareWant(want, context);
    return res;
  }
}

⚠️ 注意 encodeURIComponent(options.url):URL 里面可能有特殊字符(如 &、?、=),必须编码,否则浏览器会解析错。

其他几个类似:

// Pinterest — 打开 Pin 创建页面
export class PinterestShare extends ShareBaseInstance {
  constructor() {
    super("com.pinterest", 'pinterest://', '123',
          'https://www.pinterest.com/pin/create/button/')
  }

  public async shareSingleWant(options: ShareSingleOptions,
      context: common.UIAbilityContext): Promise<ShareSingleResult> {
    let want: Want = {
      entities: ['entity.system.browsable'],
      uri: `https://www.pinterest.com/pin/create/button/?url=${encodeURIComponent(options.url)}&title=${options.title}`,
      action: 'ohos.want.action.viewData',
      parameters: { 'ability.params.backToOtherMissionStack': true }
    };
    const res = await super.shareWant(want, context);
    return res;
  }
}

// LinkedIn
export class LinkedinShare extends ShareBaseInstance {
  constructor() {
    super("com.linkedin.android", 'linkedin://', '123',
          'https://www.linkedin.com/shareArticle')
  }

  public async shareSingleWant(options: ShareOptions,
      context: common.UIAbilityContext): Promise<ShareSingleResult> {
    let want: Want = {
      entities: ['entity.system.browsable'],
      uri: `https://www.linkedin.com/shareArticle?mini=true&url=${encodeURIComponent(options.url)}&title=${options.title}`,
      action: 'ohos.want.action.viewData',
      parameters: { 'ability.params.backToOtherMissionStack': true }
    };
    const res = await this.shareWant(want, context);
    return res;
  }
}

// Facebook Stories — 用 Want 传图片
export class FacebookStoriesShare extends ShareBaseInstance {
  constructor() {
    super("com.facebook.katana", 'facebook://story-camera', '123')
  }

  public async shareSingleWant(options: ShareSingleOptions,
      context: common.UIAbilityContext): Promise<ShareSingleResult> {
    let want: Want = {
      deviceId: '',
      bundleName: 'com.facebook.katana',
      abilityName: 'com.facebook.katana.ShareToStories',
      action: 'ohos.want.action.sendData',
      flags: wantConstant.Flags.FLAG_AUTH_READ_URI_PERMISSION
           | wantConstant.Flags.FLAG_AUTH_WRITE_URI_PERMISSION,
      type: options.type || 'image/*',
      parameters: {
        'ability.params.backToOtherMissionStack': true,
        'ohos.extra.param.key.stream': options.url,
      }
    };
    const res = await this.shareWant(want, context);
    return res;
  }
}

7.3 第三类:自定义 Want(最复杂的 4 个平台)

这 4 个平台需要特殊处理,我来逐个拆解。

7.3.1 Instagram Stories — 要传背景图和贴纸

Instagram Stories 分享和普通分享不一样,它有三种素材模式:

模式 A:背景图 + 贴纸图(backgroundImage + stickerImage)
模式 B:背景视频(backgroundVideo)
模式 C:仅背景图(backgroundImage)
export class InstagramStoriesShare extends ShareBaseInstance {
  constructor() {
    super("com.instagram.android", 'instagram://story-camera', '123')
  }

  public async shareSingleWant(options: InstagramStoriesShareSingleOptions,
      context: common.UIAbilityContext): Promise<ShareSingleResult> {
    const fileUtil = FileUtils.getInstance();

    // 根据有没有 backgroundVideo 决定 type
    let want: Want = {
      deviceId: '',
      bundleName: 'com.instagram.android',
      abilityName: 'com.instagram.android.storyshare.ShareActivity',
      action: 'ohos.want.action.sendData',
      flags: wantConstant.Flags.FLAG_AUTH_READ_URI_PERMISSION
           | wantConstant.Flags.FLAG_AUTH_WRITE_URI_PERMISSION,
      parameters: {
        'ability.params.backToOtherMissionStack': true,
        // 传素材:优先用视频,没有视频用图片
        'ohos.extra.param.key.stream': fileUtil.hasValidKey("backgroundVideo", options)
            ? options.backgroundVideo : options.backgroundImage,
      },
      // MIME 类型也要对应
      type: fileUtil.hasValidKey("backgroundVideo", options) ? 'video/*' : 'image/*',
    };
    const res = await this.shareWant(want, context);
    return res;
  }
}

💡 FLAG_AUTH_READ_URI_PERMISSION | FLAG_AUTH_WRITE_URI_PERMISSION:这两个 flag 是给目标应用读写 URI 的权限。分享文件时必须加,否则 Instagram 读不到你传的图片。

7.3.2 抖音 — 要区分图片和视频

抖音分享有两种模式:发图片和发视频。必须用 type 参数区分:

export class DouyinShare extends ShareBaseInstance {
  constructor() {
    super("com.ss.hm.ugc.aweme", 'snssdk1128://openplatform/share', '123')
  }

  public async shareSingleWant(options: ShareSingleOptions,
      context: common.UIAbilityContext): Promise<ShareSingleResult> {
    const type = options.type;
    if (!type) {
      // 没传 type 直接报错,因为抖音必须知道你分享的是图片还是视频
      return { success: false, message: 'type is not define you must define it image or video' };
    }

    let isImage = type?.startsWith('image');  // image/* 开头就是图片

    // 收集所有要分享的 URL
    let urls: string[] = [];
    if (options.url) urls.push(options.url);
    if (options.urls) urls = urls.concat(options.urls);

    // 如果 URL 是 base64 格式,需要先写入本地文件
    const fileUtil = FileUtils.getInstance();
    const urlsArr = urls.map(async (url) => {
      if (fileUtil.isBase64File(url)) {
        return await fileUtil.writeBase64File(context, url);
      }
      return url;
    });

    let want: Want = {
      bundleName: 'com.ss.hm.ugc.aweme',
      abilityName: 'MainAbility',
      uri: 'snssdk1128://openplatform/share',
      flags: wantConstant.Flags.FLAG_AUTH_READ_URI_PERMISSION
           | wantConstant.Flags.FLAG_AUTH_WRITE_URI_PERMISSION,
      parameters: {
        msg: options.subject,
        title: options.title,
        share_to_publish: 1,
        image_list_path: isImage ? urlsArr : null,   // 图片模式:传图片列表
        video_path: isImage ? null : urlsArr[0]       // 视频模式:传单个视频
      }
    };
    const res = await this.shareWant(want, context);
    return res;
  }
}

⚠️ base64 文件必须先写入本地:鸿蒙的 Want 不能直接传 base64 数据。必须先把 base64 解码,写入本地文件,然后把文件 URI 传给 Want。这个逻辑在 FileUtils.writeBase64File() 里。

7.3.3 微博 — 用自定义媒体对象

微博分享比较特殊,它需要结构化的数据(文字 + 图片 + 视频可以同时传):

export class WeiboShare extends ShareBaseInstance {
  public static readonly COMMAND_TO_WEIBO = 1;
  public static readonly WEIBO_SDK_FLAG = 0x11;

  constructor() {
    super("com.sina.weibo", 'sinaweibo://', '123')
  }

  public async shareSingleWant(options: ShareSingleOptions,
      context: common.UIAbilityContext) {
    // 构建 WeiboMultiMessage:可以同时包含文字和图片
    let message = new WeiboMultiMessage();

    if (options.message) {
      let textObject = new TextObject();
      textObject.text = options.message;
      message.textObject = textObject;               // 文字部分
    }
    if (options.urls) {
      let multiImageObject = new MultiImageObject();
      multiImageObject.uriStrs = options.urls;
      message.multiImageObject = multiImageObject;    // 图片部分
    }

    let want: Want = {
      bundleName: 'com.sina.weibo',
      abilityName: 'EditActivity',
      action: 'ohos.want.action.sendData',
      flags: wantConstant.Flags.FLAG_AUTH_READ_URI_PERMISSION
           | wantConstant.Flags.FLAG_AUTH_WRITE_URI_PERMISSION,
      parameters: {
        'ability.params.backToOtherMissionStack': true,
        _weibo_command_type: WeiboShare.COMMAND_TO_WEIBO,
        _weibo_flag: WeiboShare.WEIBO_SDK_FLAG,
        _weibo_transaction: systemDateTime.getTime(false) + "",
        msg: message,
        "ability.params.stream": message.multiImageObject?.getImageUris() ?? []
      }
    };
    const res = await this.shareWant(want, context);
    return res;
  }
}
7.3.4 SMS 短信分享

短信分享需要传收件人号码和内容:

export class SMSShare extends ShareBaseInstance {
  constructor() {
    super("com.ohos.mms", '', '123')  // mms = 短信应用
  }

  public async shareSingleWant(options: ShareSingleOptions,
      context: common.UIAbilityContext): Promise<ShareSingleResult> {
    let contactInfo = [{
      contactsName: 'ZhangSan',
      telephone: options?.recipient    // 收件人号码
    }];
    let content = options?.message + '\n' + options?.url;  // 短信内容

    let want = {
      bundleName: 'com.ohos.mms',
      abilityName: 'com.ohos.mms.MainAbility',
      parameters: {
        contactObjects: JSON.stringify(contactInfo),
        pageFlag: 'conversation',
        content: content
      }
    };
    const res = await super.shareWant(want, context);
    return res;
  }
}

第八章 · GenericShare:系统分享面板

open() 方法需要调起系统的分享面板,让用户自己选分享到哪个 APP。这是最复杂的分享实现(约 231 行),因为鸿蒙提供了两种方式。

8.1 方式一:SystemShare API(推荐)

鸿蒙提供了 @hms.collaboration.systemShare 模块,可以拉起系统分享面板:

import systemShare from '@hms.collaboration.systemShare';
import utd from '@ohos.data.uniformTypeDescriptor';

// 创建分享数据
let data: systemShare.SharedData;
urls.forEach((url) => {
  let utdType: utd.UniformDataType;
  if (fileUtil.isHttpLink(url)) {
    utdType = utd.UniformDataType.HYPERLINK;   // 网页链接
  } else {
    utdType = fileUtil.getMIMETypeFromExtension(url) as utd.UniformDataType;  // 文件
  }
  let shareAppData = systemShare.createShareAppData({ uri: url, utd: utdType });
  if (!data) {
    data = systemShare.createSharedData(shareAppData);
  } else {
    data.addRecord(shareAppData);
  }
});

// 拉起分享面板
let controller = new systemShare.ShareController(data);
controller.share(context, { excludedTargets: excludedAbilities });

8.2 方式二:Want 降级方案

如果 SystemShare API 不可用(某些低版本系统),就用 Want 的 ohos.want.action.select 拉起系统选择器:

let want: Want = {
  "action": "ohos.want.action.select",
  "type": "text/plain",
  parameters: {
    'ability.want.params.INTENT': {
      action: 'ohos.want.action.viewData',
      type: type,
      uri: filePath,
    }
  }
};
await context.startAbility(want);

8.3 saveToFiles:保存到本地

如果用户选择「保存到文件」,需要用 FileUtils.saveMultiUrlsDataToDocument() 把文件保存到文档目录。


第九章 · TurboModule:JS 和鸿蒙之间的桥梁

9.1 什么是 TurboModule?

TurboModule 是 React Native 新架构里,JS 调用原生代码的方式。你可以把它想象成一个翻译官:

JS 层:shareSingle({ social: 'whatsapp', message: '你好' })
    ↓ TurboModule 翻译
鸿蒙层:new WhatsAppShare().shareSingleWant(options, context)

9.2 实现 TurboModule

export class RNShareTurboModule extends TurboModule implements TM.RNShare.Spec {
  private context: common.UIAbilityContext;

  constructor(ctx) {
    super(ctx);
    this.context = this.ctx.uiAbilityContext;  // 获取当前 Ability 的上下文
  }

  // 根据 social 参数,找到对应的分享实例
  private getWantShareInstance(options: ShareOptions): ShareBaseInstance {
    let share: Social = options.social as Social;
    switch (share) {
      case Social.Generic:       return new GenericShare();
      case Social.Facebook:      return new FacebookShare();
      case Social.Twitter:       return new TwitterShare();
      case Social.Whatsapp:      return new WhatsAppShare();
      case Social.Instagram:     return new InstagramShare();
      case Social.Email:         return new EmailShare();
      case Social.Sms:           return new SMSShare();
      case Social.Telegram:      return new TeleGramShare();
      case Social.Discord:       return new DiscordShare();
      case Social.Weibo:         return new WeiboShare();
      case Social.Douyin:        return new DouyinShare();
      // ... 全部 21 个
      default: return null;
    }
  }

  // 导出常量给 JS 层
  public getConstants(): Object {
    return {
      "FACEBOOK": "facebook",
      "TWITTER": "twitter",
      "WHATSAPP": "whatsapp",
      // ... 全部平台
    };
  }

  // open() → 通用分享(用 GenericShare)
  public async open(options: ShareOptions): Promise<{ success: boolean; message: string }> {
    let shareInstance = this.getWantShareInstance({ ...options, social: Social.Generic });
    const res = await shareInstance.openSingleWant(options, this.context);
    return res;
  }

  // shareSingle() → 定向分享
  public async shareSingle(options: ShareSingleOptions): Promise<{ success: boolean; message: string }> {
    let shareInstance = this.getWantShareInstance(options);
    if (!shareInstance) {
      return { success: false, message: '不支持分享到此平台' };
    }
    let res = await shareInstance.shareSingleWant(options, this.context);
    return res;
  }

  // isPackageInstalled() → 检查应用是否安装
  public async isPackageInstalled(packageName: string): Promise<boolean> {
    let shareInstance = this.getWantShareInstance({ social: packageName });
    return await shareInstance.isInstalledPackage();
  }

  // isBase64File() → 判断是否为 base64 文件
  public async isBase64File(url: string): Promise<boolean> {
    return FileUtils.getInstance().isBase64File(url);
  }
}

9.3 调用流程图

JS 层调用                        TurboModule                      鸿蒙原生
──────────                      ───────────                      ─────────
RNShare.open(options)     →     open(options)               →    GenericShare.openSingleWant()
RNShare.shareSingle({...}) →   shareSingle(options)        →    WhatsAppShare.shareSingleWant()
RNShare.isPackageInstalled() → isPackageInstalled(name)     →    WhatsAppShare.isInstalledPackage()
RNShare.isBase64File(url) →    isBase64File(url)            →    FileUtils.isBase64File()

第十章 · JS 层的修改

原库的 JS 层有两处需要修改,否则在鸿蒙上会报错。

10.1 修改 shareSingle 的平台判断

// 修改前:只允许 Android 和 iOS
if (Platform.OS !== 'android' && Platform.OS !== 'ios') {
  throw new Error('Not implemented');
}

// 修改后:加上 harmony
if (Platform.OS !== 'android' && Platform.OS !== 'ios' && Platform.OS !== 'harmony') {
  throw new Error('Not implemented');
}

10.2 修改 isPackageInstalled 的平台判断

// 修改前:只允许 Android
if (Platform.OS !== 'android') {
  throw new Error('Not implemented');
}

// 修改后:加上 harmony
if (Platform.OS !== 'android' && Platform.OS !== 'harmony') {
  throw new Error('Not implemented');
}

🧠 为什么只改这两处?因为原库的 open() 方法没有平台判断(所有平台走同一个逻辑),而 shareSingleisPackageInstalled 有显式的平台检查。加上 'harmony' 就行了。


第十一章 · Spec 定义:JS 和原生的契约

11.1 什么是 Spec?

Spec 是 TurboModule 的「合同」,定义了 JS 可以调用哪些方法、传什么参数、返回什么结果。

11.2 NativeRNShare.ts

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

export interface Spec extends TurboModule {
  // 常量:社交平台名称
  readonly getConstants: () => {
    FACEBOOK?: string;
    TWITTER?: string;
    WHATSAPP?: string;
    INSTAGRAM?: string;
    TELEGRAM?: string;
    EMAIL?: string;
    SMS?: string;
    DISCORD?: string;
    // ... 等等
  };

  // 4 个方法
  open: (options: Object) => Promise<{ success: boolean; message: string }>;
  shareSingle: (options: Object) => Promise<{ success: boolean; message: string }>;
  isPackageInstalled: (packagename: string) => Promise<boolean>;
  isBase64File: (url: string) => Promise<boolean>;
}

// 注册到 TurboModuleRegistry
export default TurboModuleRegistry.get<Spec>('RNShare') as Spec | null;

💡 这个文件是 JS 层和原生层之间的「桥梁图纸」。JS 侧根据这个接口调用原生方法,原生侧(RNShareTurboModule)必须实现这个接口中定义的所有方法。


第十二章 · 工具类详解

12.1 FileUtils — 文件处理(最复杂的工具类)

FileUtils 有 735 行代码,是整个项目最大的工具类。它解决了一个核心问题:

JS 层传来的文件可能是各种格式的(base64、HTTP 链接、本地路径),但鸿蒙的 Want 只接受本地文件 URI。

所以 FileUtils 的核心工作就是:把各种格式的文件统一转成本地 URI

JS 传入的文件 URL
    │
    ├── data:image/png;base64,...  → writeBase64File() → file://xxx/123.png
    ├── https://example.com/a.png → writeUriFromHttpUrl() → file://xxx/a.png
    └── /data/local/tmp/a.png     → 直接用(已经是本地路径)
    │
    └── 统一输出:file:// URI → 传给 Want

核心方法

方法 作用 什么时候用
isBase64File() 判断是否是 base64 格式 每次处理 URL 前先判断
isHttpLink() 判断是否是网络链接 同上
isLocalFile() 判断是否是本地文件 同上
writeBase64File() 把 base64 写入本地,返回 URI base64 格式时
writeUriFromHttpUrl() 下载网络文件到本地,返回 URI HTTP 链接时
getMIMETypeFromExtension() 根据文件后缀获取 MIME 类型 构造 Want 的 type 时
saveMultiUrlsDataToDocument() 批量保存文件到文档目录 saveToFiles 时

base64 处理流程详解

// 输入:data:image/png;base64,iVBORw0KGgo...

// 第 1 步:检测
isBase64File(url)true   // scheme 是 "data",包含 "base64"

// 第 2 步:提取后缀
getUrlSuffix(url)'.png'  // 从 "image/png" 提取

// 第 3 步:写入本地文件
writeBase64File(context, url) {
  // 3.1 获取缓存目录
  let path = context.cacheDir + '/share_cache';
  // 3.2 生成文件名
  let name = Date.now() + '.png';  // 如 1717747200000.png
  // 3.3 解码 base64 并写入
  let encodedData = url.split(',')[1];              // 去掉 data:image/png;base64, 前缀
  let decodeData = buffer.from(encodedData, 'base64').buffer;  // 解码
  fs.writeSync(fd, decodeData);                     // 写入文件
  // 3.4 返回 file:// URI
  return fileUri.getUriFromPath(path + '/' + name); // file://cache/share_cache/1717747200000.png
}

// 第 4 步:URI 传给 Want
want.parameters['ohos.extra.param.key.stream'] = 'file://cache/share_cache/1717747200000.png';

12.2 Logger — 日志工具

简单但重要的工具。开发时需要看到日志来调试:

import hilog from '@ohos.hilog';

export default class Logger {
  private static domain: number = 0x0001;  // 日志域
  private static tag: string = 'RNShare';  // 日志标签

  static info(msg: string) {
    hilog.info(Logger.domain, Logger.tag, msg);
  }
  static error(msg: string) {
    hilog.error(Logger.domain, Logger.tag, msg);
  }
}

使用:

Logger.info('开始分享到 WhatsApp');
Logger.error('分享失败:应用未安装');

⚠️ 鸿蒙的日志用 hilog 而不是 console.loghilog 的日志可以在 DevEco Studio 和 hdc hilog 命令中看到。

12.3 ShareMediaObject — 分享媒体对象

为微博等需要结构化数据的平台设计的模型:

MediaObject(基类)
  └── imageUrl: string          图片URL

TextObject extends MediaObject
  └── text: string              文字内容

MultiImageObject extends MediaObject
  └── uriStrs: string[]         多图URL列表
      └── getImageUris()        获取所有图片URI

VideoSourceObject extends MediaObject
  └── videoPath: string         视频路径

WebPageObject extends MediaObject
  └── webpageUrl: string        网页URL

WeiboMultiMessage(组合对象)
  ├── textObject: TextObject         文字
  ├── multiImageObject: MultiImageObject  图片
  ├── videoSourceObject: VideoSourceObject 视频
  └── webPageObject: WebPageObject   网页

第十三章 · 我踩过的 5 个坑

坑 1:Logger 路径不一致 🔴

现象:编译报错,找不到 ../utils/Logger 模块。

原因Logger.ts 放在 ets/Logger.ts,但 ShareBaseInstance.tsets/share/ 目录下,引用路径写的是 ../utils/Logger。实际上 ets/utils/ 目录下没有 Logger.ts

解决:在 ets/utils/ 下创建一个 Logger.ts 重导出:

// ets/utils/Logger.ts
export { default } from '../Logger';

教训:目录结构设计好之后,import 路径要仔细核对。可以用 find 命令确认文件实际位置。

坑 2:双斜杠路径 🔴

现象GenericShare.tsimport { FileUtils } from '../utils//FileUtils' 有双斜杠。

原因:手误。

解决:改成 '../utils/FileUtils'

教训:虽然双斜杠在某些系统上不会报错,但为了规范应该避免。可以配置 ESLint 规则来检测。

坑 3:类名大小写不一致 🔴

现象FacebookShare.ts 导出的类名是 FaceBookShare(B 大写),但 Share.tsRNShareTurboModule.ts 中引用的是 FacebookShare(b 小写)。

原因:JavaScript/TypeScript 是大小写敏感的语言。FaceBookShareFacebookShare 是两个不同的标识符。

解决:统一改为 FacebookShare(camelCase 规范)。

教训:命名要遵循统一规范。JavaScript 中类名用 PascalCase,中间单词不大写(如 Facebook 而不是 FaceBook)。

坑 4:isPackageInstalled 不支持 harmony 🔴

现象:在鸿蒙上调用 isPackageInstalled() 直接抛出 Not implemented 错误。

原因:原库的 JS 层有平台判断 if (Platform.OS !== 'android'),鸿蒙的 Platform.OS 值是 'harmony',被拦截了。

解决:改为 if (Platform.OS !== 'android' && Platform.OS !== 'harmony')

教训:适配新平台时,要全面搜索 JS 层中的平台判断代码,逐一添加新平台支持。可以用 grep -n "Platform.OS" 快速定位。

坑 5:WeiboShare 缺少 import 🔴

现象WeiboShare.ts 中使用了 FileUtils.getInstance() 但文件头部没有 import。

原因:写代码时忘了加导入语句。

解决:添加 import { FileUtils } from '../utils/FileUtils'

教训:写完代码后检查一下 import 是否完整。TypeScript 编译器会帮你检查,但如果你还没配置编译环境就只能手动了。


第十四章 · 最终文件清单

整个适配产出 40 个文件,按重要性排序:

核心文件(必须理解)

文件 行数 作用
RNShareTurboModule.ts 165 TurboModule 主类,JS ↔ 原生桥梁
share/ShareBaseInstance.ts 152 分享基类,所有平台的父类
Types.ts 122 类型定义,整个项目的基础
share/GenericShare.ts 231 系统分享面板,open() 的实现

分享平台文件(21 个,大部分很简单)

文件 行数 模式
WhatsAppShare.ts 9 仅构造
TelegramShare.ts 9 仅构造
DiscordShare.ts 9 仅构造
SnapChatShare.ts 9 仅构造
MessengerShare.ts 10 仅构造
ViberShare.ts 9 仅构造
EmailShare.ts 12 仅构造
InstagramShare.ts 9 仅构造
TwitterShare.ts 9 仅构造
GooglePlusShare.ts 9 仅构造
WhatsAppBusinessShare.ts 9 仅构造
FacebookShare.ts 25 URL 协议
PinterestShare.ts 27 URL 协议
LinkedinShare.ts 25 URL 协议
FacebookStoriesShare.ts 30 自定义 Want
InstagramStoriesShare.ts 33 自定义 Want
SMSShare.ts 33 自定义 Want
DouyinShare.ts 70 自定义 Want
WeiboShare.ts 58 自定义 Want
TestShare.ts 48 自定义 Want(测试用)

工具和模型文件

文件 行数 作用
utils/FileUtils.ts 735 文件处理(base64/HTTP/缓存)
Logger.ts 39 日志工具
share/shareMediaObject/*.ts ~280 微博等平台的媒体对象模型

JS 层文件

文件 修改 作用
src/NativeRNShare.ts 版权头更新 Spec 定义
index.js +2 处 harmony 判断 JS 层桥接
index.d.ts 无修改 TypeScript 类型声明

第十五章 · 回顾:我们做了什么

15.1 一句话总结

用鸿蒙的 Want 机制,把 21 个社交平台的分享功能全部实现,通过 TurboModule 桥接到 JS 层。

15.2 适配流程回顾

① 搭目录结构    → 参照已适配项目(react-native-tts)
     ↓
② 定义类型      → Social 枚举 + ShareOptions 接口
     ↓
③ 设计基类      → ShareBaseInstance 封装 Want/检测/商店/剪贴板
     ↓
④ 逐平台实现    → 3 种模式(3行构造 / URL协议 / 自定义Want)
     ↓
⑤ TurboModule   → switch-case 路由 + getConstants()
     ↓
⑥ JS 层兼容     → Platform.OS 加 'harmony'
     ↓
⑦ 工具类支撑    → FileUtils + Logger + ShareMediaObject

15.3 新手建议

  1. 先找参考项目,不要从零开始。照着已适配项目的结构来,事半功倍。
  2. 先搭骨架再填内容。先让 TurboModule 注册跑通,再一个个实现分享平台。
  3. 先做最简单的。12 个「3 行代码」的平台先搞定,积累信心。
  4. 注意 import 路径。鸿蒙项目的目录嵌套比较深,路径很容易写错。
  5. 平台判断要排查。用 grep "Platform.OS" 找出所有平台判断,逐一添加 harmony。
  6. base64 文件要写本地。Want 不能直接传 base64,必须先 writeBase64File()。
  7. 善用 Logger 调试hilog 是鸿蒙端看日志的方式,比 print 调试高效。

参考文档

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐