React Native for OpenHarmony 实战:三方库 react-native-emoji-selector 的鸿蒙化适配指南
本文记录把 react-native-emoji-selector(Emoji 选择器)适配到 HarmonyOS 的完整过程。
它做的事很专一:给一个 Emoji 选择面板 —— 顶部分类 Tab、可选的搜索框、下面是按列排布的 Emoji 网格,点一个就把字符回调出去;可选支持"最近使用"历史。
结论先说:这是最省事的一类库,装完直接就能用。 具体地说:
- 纯 JS、单文件(
index.js10235 B) - 只有 1 个运行时依赖:
emoji-datasource(纯 JS 数据包) - 没有
prepare/postinstall(不会卡住安装) - 老写法九项预扫几乎全空(无
createClass、无React.PropTypes、无字符串ref、无mixins、无componentWillMount/componentWillReceiveProps、无ViewPropTypes、无measure()) - 设备侧 23 / 23 条判据全部通过
★★★★ 一、最值得记的一件事:一个"按源码该崩、实测却不崩"的隐患
库的第 10 行是从 react-native 里解构 AsyncStorage:
import {
View, Text, StyleSheet, TouchableOpacity, TextInput,
Platform, ActivityIndicator,
AsyncStorage, // ← L10
FlatList
} from "react-native";
…
L159: let history = await AsyncStorage.getItem(storage_key);
L176: AsyncStorage.setItem(storage_key, JSON.stringify(value));
L183: let result = await AsyncStorage.getItem(storage_key);
而 AsyncStorage 早在 RN 0.60 就被移出核心(改为社区包 @react-native-async-storage/async-storage)。在当前 RN 的 index.js 里,它被定义成一个"一访问就抛错"的 getter:
// react-native/index.js
Object.defineProperty(module.exports, 'AsyncStorage', {
get() {
throw new Error('AsyncStorage has been removed from react-native core. ' + …);
},
…
});
⇒ 按这个源码,任何对 RN.AsyncStorage 的读取都应该抛错 ⇒ 这个库"与 react-native 的具名导入"理应崩 ✗
我把这件事做成了页面上的一条判据(用 try/catch 探测,因为 typeof AsyncStorage 本身就会抛):
NOTE | ★ 访问 RN.AsyncStorage 是否抛错 = false
NOTE | ★ 抛错信息 =
⇒ 实测:在这套 RNOH 上,访问 RN.AsyncStorage 【不抛错】 ✓✓
⇒ 这正是这个库能正常加载、23 条判据全通过的原因 ✓
⇒ 也就是说:“上游 RN core 里它抛错” 并不等于 “在这套 RNOH 上它抛错” —— 必须实测 ✓
注意边界:本轮验证的是"读取该属性不抛错",以及默认路径(
showHistory: false)下库运行正常。
showHistory打开后那条真实读写链路(loadHistoryAsync/addToHistoryAsync)本轮没测到(页面上那个取证按钮我没点中),
见"本次验证的边界"一节。
★★★★ 二、几何可以精确预言:colSize = floor(width / columns)
组件用 onLayout 量自己的宽度,然后算每格尺寸:
handleLayout = ({ nativeEvent: { layout } }) => {
this.setState({ width: layout.width }, () => {
this.prerenderEmojis(() => {
this.setState({ isReady: true });
});
});
};
prerenderEmojis(callback) {
…
this.setState({
emojiList,
colSize: Math.floor(this.state.width / this.props.columns) // ← 公式
}, callback);
}
⇒ colSize 是个可以拿 width 直接算出来的量 ⇒ 我把两个实例的 state 通过 ref 读出来核对:
| 实例 | columns | 实测 state.width | 公式预言 | 实测 state.colSize |
|---|---|---|---|---|
| E1 | 6(默认) | 408 | floor(408/6) = 68 | ✅ 68 |
| E2 | 4 | 408 | floor(408/4) = 102 | ✅ 102 |
NOTE | E1.state.width = 408 NOTE | E1.state.colSize = 68 NOTE | E1.state.isReady = true
PASS | ★★ colSize 精确等于 floor(width / 6) | 期望=68 | 实际=68
PASS | ★★ E2(columns=4)colSize 精确等于 floor(width/4) | 期望=102 | 实际=102
PASS | ★ isReady 已变 true(prerenderEmojis 回调链跑通)
⇒ 顺带确认了一条平台经验:在 RNOH 上 onLayout 是可靠的(isReady 从 false 走到了 true,说明 onLayout → setState → prerenderEmojis → 回调 → setState 整条链都跑通了)。
(相比之下,用 measure() 量布局在 RNOH 上会拿不到高度 —— 这个库没有用 measure(),选对了。)
★★★★ 三、它把"可以直接单测"的东西导出得清清楚楚
const charFromUtf16 = utf16 =>
String.fromCodePoint(...utf16.split("-").map(u => "0x" + u));
export const charFromEmojiObject = obj => charFromUtf16(obj.unified);
export const Categories = { all: {...}, history: {...}, … }; // 11 个分类
⇒ 两个纯函数(含一个导出)+ 一个常量对象 ⇒ 不需要渲染就能断言 ✓
实测(unicode → 字符):
输入 unified | 含义 | 实测输出 |
|---|---|---|
"1F600" | 单码位 | 😀 ✓ |
"1F44D" | 单码位 | 👍 ✓ |
"1F1E8-1F1F3" | 多码位(区域指示符拼国旗) | 🇨🇳 ✓ |
PASS | ★★ charFromEmojiObject("1F600") === 😀
PASS | ★★ 多码位(国旗 CN)也能拼出 | 期望=🇨🇳 | 实际=🇨🇳
PASS | ★★ Categories 恰好 11 个键 | 期望=11 | 实际=11
⇒ 多码位那一条特别有价值:它验证了 split("-") + String.fromCodePoint(...) 这条链在两个码位时也正确 ✓
★★★★ 四、判据不必依赖点击:用 ref 调它的方法
组件是个 class,而且它的交互方法全是箭头类字段(挂在实例上)⇒ 可以 ref 直接调用:
handleTabSelect = category => {…}; // 切分类
handleSearch = q => {…}; // 改搜索词
handleEmojiSelect = emoji => {…}; // 选中一个 emoji(内部会回调 props.onEmojiSelected)
实测(完全不点屏幕):
NOTE | handleTabSelect(Categories.food) 调用 = ok
NOTE | handleSearch("smile") 调用 = ok
NOTE | returnSectionData() 返回条数 = 1766
NOTE | onEmojiSelected = "😀" ← ★★★ 回调精确拿到了字符
PASS | ★★ returnSectionData() 返回数组且非空
SUMMARY | 合计 23/23 通过,失败 0
⇒ 调用 handleEmojiSelect({unified:"1F600"}) 之后,props.onEmojiSelected 收到了 "😀" ✓✓
⇒ 而且 handleSearch("smile") 之后页面上标题从 All 变成了 Search Results ✓(渲染层的旁证)
同时验证了"两类成员混用"这件事(这个库两种写法都有):
// 原型方法:returnSectionData() / prerenderEmojis(cb) / componentDidMount() / render()
// 箭头类字段:handleTabSelect / handleEmojiSelect / handleSearch /
// addToHistoryAsync / loadHistoryAsync / renderEmojiCell / handleLayout
NOTE | 原型方法命中数(源码 5 个) = 5
NOTE | 7 个箭头类字段里在 prototype 上的个数(应为 0) = 0
PASS | ★ 5 个原型方法都在 prototype 上 | 期望=5 | 实际=5
PASS | ★★ 7 个箭头类字段都不在 prototype 上 | 期望=0 | 实际=0
⇒ 如果按"handleSearch 一定在原型上"写断言,第一条就会挂 ⇒ 写断言前先看源码写法 ✓




五、接入与预检
| 步 | 做法 | 结果 |
|---|---|---|
| ① | harmony.autolinking | 没有 |
| ② | harmony/ / ios/ / android/ | 都没有 |
| ③ | 原生痕迹预扫 | ✅ 无原生源文件、无 NativeModules |
| ④ | ★ 老写法九项预扫 | ✅ 几乎全空(见下) |
| ⑤ | 平台分支预扫 | ⚠️ 仅 1 处且无害:L344 color={Platform.OS === "android" ? theme : "#000000"}(ActivityIndicator 的颜色) |
| ⑥ | measure() 排查 | ✅ 未使用;用的是 onLayout(在 RNOH 上可靠) |
| ⑦ | scripts 扫 prepare 家族 | ✅ 无(只有 prettify) |
| ⑧ | 依赖 | ✅ 仅 emoji-datasource ^6.0.0(纯 JS 数据包);无 peerDependencies |
老写法九项逐项结果:
| 项 | 结果 |
|---|---|
React.createClass | ✅ 空 |
React.PropTypes | ✅ 空 |
从 react 具名导入 PropTypes | ✅ 空 |
ViewPropTypes / TextPropTypes | ✅ 空 |
字符串 ref="x" / this.refs.x | ✅ 空 |
mixins: | ✅ 空 |
componentWillReceiveProps | ✅ 空 |
componentWillMount | ✅ 空 |
measure( | ✅ 空 |
# 从适配仓库安装
npm install "git+https://atomgit.com/oh-react-native/react-native-emoji-selector.git#0.2.0-ohos-1.0.0" --ignore-scripts
# ★ 必须补装它自己的运行时依赖(本地目录 / file: 接入不会自动装)
npm install emoji-datasource@^6.0.0 --ignore-scripts
⚠️ 版本要按库声明的范围装:库要的是
^6.0.0,而emoji-datasource的latest已经是 16.x。
装latest会拿到不兼容的数据结构 ⇒ 务必装^6.0.0(本次装到的是 6.1.1)。
六、交付包质量与基线说明
| 类别 | 结果 |
|---|---|
| 交付包内容 | index.js(10235 B)+ index.d.ts + 文档;单文件实现 ✓ |
spec.json | ⚠️ 缺 upstreamCommit |
validation | ⚠️ "build-pass-device-pending" |
| ⚠️ 声明的基线 | reactNative: "0.88.0" + rnoh: "0.84.4"(verification: evidence/batch50/…)⇒ 两字段自相矛盾(rnoh 0.84.4 是配 RN 0.84.x 的),且既不是当前宿主(0.84.1/0.84.3)也不是权威 0.88 基线 |
关于基线:本次验证在 RN 0.84.1 + RNOH 0.84.3 上完成。这个库没有任何已移除的写法,
行为只依赖 onLayout、FlatList、StyleSheet 与 String.fromCodePoint ⇒ 与 RN 小版本无关 ⇒ 本轮结论跨基线成立。
七、已知限制与本次验证边界
7.1 用这个库要知道的
| 事项 | 说明 |
|---|---|
★ 依赖只有 emoji-datasource,但版本要卡 ^6.0.0 | 它的 latest 是 16.x,直接装 latest 会拿到不兼容的数据结构 |
★★ AsyncStorage 是从 react-native 解构的 | 在上游 RN core 里它是抛错 getter;这套 RNOH 上实测读取不抛错 ⇒ 库能正常加载;但换到别的基线时这条要重新验证 |
★ showHistory 打开后会走 AsyncStorage 的真实读写 | loadHistoryAsync / addToHistoryAsync;本轮未验证 |
★ 用 onLayout 量宽度 | colSize = floor(width / columns);在 RNOH 上 onLayout 可靠 |
| ★ 交互方法是箭头类字段 | handleTabSelect / handleSearch / handleEmojiSelect 在实例上(可用 ref 调);returnSectionData / prerenderEmojis 在原型上 |
| ★ 导出的纯函数可直接单测 | charFromEmojiObject({unified})、Categories |
★ 组件内含 FlatList | 不要把它放进同方向的 ScrollView(会触发 VirtualizedLists should never be nested… 警告并影响虚拟化) |
| ⚠️ 一处平台分支 | ActivityIndicator 的颜色:Platform.OS === "android" ? theme : "#000000" ⇒ 鸿蒙走 else(黑色) |
7.2 本次验证的边界
- ★★
showHistory的真实读写链路未验证(页面上的取证按钮没点中)⇒loadHistoryAsync/addToHistoryAsync在 RNOH 上是否能真正存取,未测; - ★ 我自己测试页有个缺陷:把
<EmojiSelector>(内含FlatList)放进了<ScrollView>⇒ 触发了VirtualizedLists should never be nested…警告(logbox_notification3 个)——
这是我的页面布局问题,不是库的问题; - 未逐格核对 Emoji 网格的实际渲染(只验证了
colSize与returnSectionData()的条数); - 未验证搜索过滤的准确性(只验证了"能调用、标题变成
Search Results、返回 1766 条"); - 未验证分类 Tab 的切换效果(只验证了
handleTabSelect能被调用); - 未验证
shouldInclude过滤 prop; - 未验证
theme/placeholder/showSectionTitles等外观 prop 的实际效果; - 未验证历史记录去重逻辑(
addToHistoryAsync里按unified去重那段); - 未在
RN 0.88.0/RNOH 0.84.4上复验(交付包声明的基线); - 未测真机与其它屏幕宽度(只验证了容器宽 408px 这一种情况);
- 未做跨平台对照。
未改动库代码。
八、常见问题
Q1:它在 RN 0.84 上能跑吗?
能,本次实测 23/23 条判据全通过:库正常加载、onLayout 链路跑通、colSize 与公式精确一致、导出的纯函数与 ref 编程调用都正确。
Q2:AsyncStorage 不是早就移出核心了吗?为什么它没崩?
因为它虽然是从 react-native 解构的,但当前这套 RNOH 上访问 RN.AsyncStorage 不抛错(实测 try/catch 探测结果为 false)。
而上游的 react-native/index.js 里它是抛错 getter ⇒ 两边行为不同 ⇒ 这种"按源码该崩、实测不崩"的点,只能实测。
⚠️ 换基线时请重新验证这一条。
Q3:装依赖时要注意什么?
卡版本:
npm install emoji-datasource@^6.0.0 --ignore-scripts
库声明的范围是 ^6.0.0,而它的 latest 已经是 16.x ⇒ 装 latest 会拿到不兼容的数据结构。
另外本地目录 / file: 方式接入时,库自己的依赖不会自动装 ⇒ 必须显式补装。
Q4:为什么我的 Emoji 面板一直转圈(ActivityIndicator)?
isReady 只有在 onLayout 拿到宽度、prerenderEmojis 回调跑完之后才会变 true。
如果它一直转圈,说明容器没有给你宽度(例如父容器高度/宽度为 0)⇒ colSize 也就算不出来。
Q5:怎么在不点屏幕的情况下测试它的交互?
用 ref 直接调它实例上的方法(它们都是箭头类字段):
const ref = useRef(null);
<EmojiSelector ref={ref} onEmojiSelected={c => console.log(c)} … />
// 之后
ref.current.handleTabSelect(Categories.food);
ref.current.handleSearch('smile');
ref.current.handleEmojiSelect({unified: '1F600'}); // ⇒ onEmojiSelected 收到 "😀"
ref.current.returnSectionData(); // 原型方法,返回当前列表
Q6:可以把 Emoji 选择器放进 ScrollView 里吗?
不建议。 组件内部是 FlatList,放进同方向的 ScrollView 会触发
VirtualizedLists should never be nested inside plain ScrollViews with the same orientation…
(本次测试页就因此产生了 3 条 LogBox 通知)⇒ 用固定高度容器或让它自己滚动。
Q7:想拿 Emoji 字符怎么拼?
用导出的纯函数:
import {charFromEmojiObject} from 'react-native-emoji-selector';
charFromEmojiObject({unified: '1F600'}); // "😀"
charFromEmojiObject({unified: '1F1E8-1F1F3'}); // "🇨🇳"(多码位也支持)
小结
react-native-emoji-selector 是单文件、纯 JS、只有一个纯 JS 依赖的 Emoji 选择器,写法很现代(老写法九项预扫几乎全空)。设备侧 23 / 23 条判据全部通过,装完即可用。
最值得记下来的是这几条:
-
★★★★ “按源码该崩、实测却不崩” —— 又一次用实测钉死了推断。
库从react-native解构AsyncStorage,而上游 RN core 把它定义成抛错 getter ⇒ 按理该崩;
实测访问它不抛错(try/catch探测为false)⇒ 这正是库能正常加载的原因 ✓
⇒ 跨平台 API 的可用性必须实测,别拿上游源码直接推(另一条同类经验:状态栏高度这类"某平台专有"的量,也可能已经被正确实现)。 -
★★★★ 几何公式可以直接预言,
onLayout是可靠那一侧。
colSize = floor(width / columns)⇒ 实测floor(408/6)=68与floor(408/4)=102全部精确命中 ✓
同时isReady从false走到true,证明onLayout → setState → 回调 → setState整条链在 RNOH 上跑通 ✓
(反过来,用measure()量布局在 RNOH 上拿不到高度 —— 这个库没用它,选对了。) -
★★★★ 导出的纯函数是最便宜的硬判据。
charFromEmojiObject三条断言:1F600 → 😀、1F44D → 👍、多码位1F1E8-1F1F3 → 🇨🇳(后者验证了split("-")+fromCodePoint这条链)✓ -
★★★★ 交互方法都是箭头类字段 ⇒ 可以用
ref编程调用,判据不依赖点击。
handleSearch("smile")、handleTabSelect(Categories.food)、handleEmojiSelect({unified:"1F600"})
⇒onEmojiSelected精确收到"😀"✓,returnSectionData()返回 1766 条 ✓ -
★★★ 两类成员混用必须先看源码再写断言。
5 个原型方法(returnSectionData/prerenderEmojis/…)+ 7 个箭头类字段(handleTabSelect/handleSearch/…)
⇒ 实测 5/5 在 prototype、0/7 箭头字段在 prototype ✓ -
★★ 依赖要按库声明的范围装。 库要
emoji-datasource@^6.0.0,而它latest已是 16.x ⇒ 装 latest 会拿到不兼容数据结构;
且本地目录 /file:接入不会自动装库自己的依赖 ⇒ 必须显式补装 ✓(本次装到 6.1.1) -
★ 测试页自身的坑也要如实写出来。 我把含
FlatList的组件放进了ScrollView⇒ 触发VirtualizedLists should never be nested…警告(LogBox 3 条)——
那是我的页面问题,不是库的问题 ✓ -
★ 判据逐条直写日志(
PASS|描述|期望|实际)⇒ 采集端grep即得全证据,不把证据留存绑在 UI 上 ✓
本篇用到的库
| 项 | 内容 |
|---|---|
| 三方库 | react-native-emoji-selector(上游 0.2.0 的鸿蒙适配版) |
| 适配仓库 | https://atomgit.com/oh-react-native/react-native-emoji-selector |
| 适配 TAG | 0.2.0-ohos-1.0.0 |
| 需要 HAR / 权限 / ohpm | 都不需要(纯 JS、单文件) |
| 库类型 | Emoji 选择器(分类 Tab + 搜索 + 网格 + 可选历史) |
| 运行时依赖 | ⚠️ emoji-datasource(必须装 ^6.0.0;latest 是 16.x) |
prepare / postinstall | ✅ 都没有 |
| 许可证 | MIT |
| 上游仓库 | https://github.com/arronhunt/react-native-emoji-selector(★307,pushed 2025-03) |
| 宿主工程 | RNOH084Demo(测试页 rnAppKey = EmojiSelectorTestApp) |
# 从适配仓库安装(⚠️ 必须补装它自己的依赖,并卡版本)
npm install "git+https://atomgit.com/oh-react-native/react-native-emoji-selector.git#0.2.0-ohos-1.0.0" --ignore-scripts
npm install emoji-datasource@^6.0.0 --ignore-scripts
import EmojiSelector, {Categories, charFromEmojiObject} from 'react-native-emoji-selector';
// ① 基本用法
<EmojiSelector
onEmojiSelected={emoji => console.log(emoji)} // ⇒ 例如 "😀"
showSearchBar
showTabs
showHistory={false} // 打开后会走 AsyncStorage 的真实读写
columns={6}
placeholder="Search..."
theme="#007AFF"
style={{height: 320}} // ★ 给固定高度,别放进同方向的 ScrollView
/>
// ② 不点屏幕也能测(方法都是【箭头类字段】,挂在实例上)
const ref = useRef(null);
// ref.current.handleTabSelect(Categories.food);
// ref.current.handleSearch('smile');
// ref.current.handleEmojiSelect({unified: '1F600'}); // ⇒ onEmojiSelected 收到 "😀"
// ref.current.returnSectionData(); // 原型方法:返回当前列表(实测 1766 条)
// ref.current.state.colSize // 应等于 floor(state.width / columns)
// ③ 纯函数(可直接单测)
charFromEmojiObject({unified: '1F600'}); // "😀"
charFromEmojiObject({unified: '1F1E8-1F1F3'}); // "🇨🇳" 多码位也支持
// ⚠️ AsyncStorage 是 L10 从 react-native 解构的:上游 core 里是抛错 getter,
// 但这套 RNOH 上【读取不抛错】⇒ 库能正常加载(换基线请重新验证)
// ⚠️ 组件内部是 FlatList,放进同方向 ScrollView 会触发 VirtualizedLists 嵌套警告
# 换页启动测试页 —— force-stop 不能省
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey EmojiSelectorTestApp
判据的复现方式
# ① 契约与纯函数:逐条 PASS/FAIL 日志
hdc shell "hilog -x | grep emoji-test" | Select-String 'PASS|FAIL|SUMMARY|NOTE'
# ⇒ charFromEmojiObject("1F600")=😀 / ("1F44D")=👍 / (1F1E8-1F1F3)=🇨🇳
# ② 几何:用 ref 读 state,核对 colSize = floor(width / columns)
# ⇒ E1(columns=6)width=408 ⇒ colSize=68;E2(columns=4)⇒ colSize=102
# ③ 编程调用:handleTabSelect / handleSearch / handleEmojiSelect(⇒ onEmojiSelected)
# ④ AsyncStorage 探测(★ 必须 try/catch —— typeof 本身会抛)
# ⇒ 这套 RNOH 上:访问 RN.AsyncStorage 不抛错
验证环境
| 项 | 版本 |
|---|---|
| React Native | 0.84.1 |
| React | 19.2.3 |
| RNOH(npm / ohpm) | @react-native-oh/react-native-harmony / @rnoh/react-native-openharmony 0.84.3 |
| Node.js / npm | v24.14.0 / 11.9.0 |
| DevEco Studio | 26.0.0.621 |
| HarmonyOS SDK | API 26(26.0.0.32) |
| 设备 | HarmonyOS 7.0.0(26.0.0) Beta2 模拟器 Pura X View(ohos-x64),1320×2232,density 3 |
| 本机状态栏高度 | 117px = 39vp |
| 宿主依赖 | ⚠️ 需补装 emoji-datasource@^6.0.0(本次装到 6.1.1) |
| 宿主 HAP 产物 | entry-default-signed.hap |
| 本次构建 | assembleHap 3 分 42 秒 ⇒ 一次成功(打包与构建退出码都检查了) |
| 验证规模 | 23 条判据全部通过(契约 9 + 几何 4 + 编程调用 4 + 安全区与记录若干);纯函数 3 条精确断言(含多码位);几何公式 2 组精确命中(68 / 102);ref 编程调用 3 个方法;4 张截图互异 |
⚠️ 基线说明:交付包
spec.json声明RN 0.88.0 + RNOH 0.84.4(两字段自相矛盾),本次验证在RN 0.84.1 + RNOH 0.84.3上完成;
本库无任何已移除写法,行为只依赖onLayout/FlatList/StyleSheet/String.fromCodePoint⇒ 结论跨基线成立。
但AsyncStorage那条(“读取不抛错”)建议在换基线时重新验证一次。
相关社区与项目地址
| 项目 | 地址 |
|---|---|
| React Native 鸿蒙官方社区(CPF-RN) | https://atomgit.com/CPF-RN |
| RNOH 框架仓库 | https://atomgit.com/openharmony-RN/ohos_react_native |
| RNOH 三方库使用文档 | https://atomgit.com/OpenHarmony-RN/usage-docs |
| React Native for OpenHarmony 组织 | https://atomgit.com/oh-react-native |
| RN 三方库鸿蒙适配清单 | https://atomgit.com/oh-react-native/rn-ohos-adaptation-overview |
欢迎加入 CPF-RN 鸿蒙社区:https://atomgit.com/CPF-RN
React Native for OpenHarmony 组织:https://atomgit.com/oh-react-native
RN 三方库鸿蒙适配清单:https://atomgit.com/oh-react-native/rn-ohos-adaptation-overview
更多推荐



所有评论(0)