小白的鸿蒙适配之旅:让 react-native-share 跑在 HarmonyOS 上
React Native(简称 RN)是 Facebook 出的一个框架,让你用 JavaScript 写代码,就能同时跑在 Android 和 iOS 上。比如你写了一个「分享到微信」的功能,Android 和 iOS 都能用同一套 JS 代码调用。别人写好的、可以拿来用的代码包。就是一个三方库——它帮你在 RN 里一行代码搞定分享到 Facebook、WhatsApp 等社交平台。华为自己做的
小白的鸿蒙适配之旅:让 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 个社交平台的分享,每家都不一样,但核心行为是一样的:
- 构造一个 Want
- 调用
context.startAbility(want)拉起目标应用 - 处理结果(成功/失败/应用未安装)
所以我们可以这样设计:
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;
}
🧠
interface和enum的区别:
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()方法没有平台判断(所有平台走同一个逻辑),而shareSingle和isPackageInstalled有显式的平台检查。加上'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.log。hilog的日志可以在 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.ts 在 ets/share/ 目录下,引用路径写的是 ../utils/Logger。实际上 ets/utils/ 目录下没有 Logger.ts。
解决:在 ets/utils/ 下创建一个 Logger.ts 重导出:
// ets/utils/Logger.ts
export { default } from '../Logger';
教训:目录结构设计好之后,import 路径要仔细核对。可以用 find 命令确认文件实际位置。
坑 2:双斜杠路径 🔴
现象:GenericShare.ts 中 import { FileUtils } from '../utils//FileUtils' 有双斜杠。
原因:手误。
解决:改成 '../utils/FileUtils'。
教训:虽然双斜杠在某些系统上不会报错,但为了规范应该避免。可以配置 ESLint 规则来检测。
坑 3:类名大小写不一致 🔴
现象:FacebookShare.ts 导出的类名是 FaceBookShare(B 大写),但 Share.ts 和 RNShareTurboModule.ts 中引用的是 FacebookShare(b 小写)。
原因:JavaScript/TypeScript 是大小写敏感的语言。FaceBookShare 和 FacebookShare 是两个不同的标识符。
解决:统一改为 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 新手建议
- 先找参考项目,不要从零开始。照着已适配项目的结构来,事半功倍。
- 先搭骨架再填内容。先让 TurboModule 注册跑通,再一个个实现分享平台。
- 先做最简单的。12 个「3 行代码」的平台先搞定,积累信心。
- 注意 import 路径。鸿蒙项目的目录嵌套比较深,路径很容易写错。
- 平台判断要排查。用
grep "Platform.OS"找出所有平台判断,逐一添加 harmony。 - base64 文件要写本地。Want 不能直接传 base64,必须先 writeBase64File()。
- 善用 Logger 调试。
hilog是鸿蒙端看日志的方式,比 print 调试高效。
参考文档
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)