☰
React Native for OpenHarmony 实战:三方库 react-native-emoji-selector 的鸿蒙化适配指南
2026/10/24 5:28:55 网站建设 项目流程

本文记录把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,// ← L10FlatList}from"react-native";…L159:lethistory=awaitAsyncStorage.getItem(storage_key);L176:AsyncStorage.setItem(storage_key,JSON.stringify(value));L183:letresult=awaitAsyncStorage.getItem(storage_key);

而AsyncStorage早在 RN 0.60 就被移出核心(改为社区包@react-native-async-storage/async-storage)。在当前 RN 的index.js里,它被定义成一个"一访问就抛错"的 getter:

// react-native/index.jsObject.defineProperty(module.exports,'AsyncStorage',{get(){thrownewError('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
E16(默认)408floor(408/6) = 68✅68
E24408floor(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(),选对了。)

★★★★ 三、它把"可以直接单测"的东西导出得清清楚楚

constcharFromUtf16=utf16=>String.fromCodePoint(...utf16.split("-").map(u=>"0x"+u));exportconstcharFromEmojiObject=obj=>charFromUtf16(obj.unified);exportconstCategories={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(✅ 空
# 从适配仓库安装npminstall"git+https://atomgit.com/oh-react-native/react-native-emoji-selector.git#0.2.0-ohos-1.0.0"--ignore-scripts# ★ 必须补装它自己的运行时依赖(本地目录 / file: 接入不会自动装)npminstallemoji-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是配 RN0.84.x的),且既不是当前宿主(0.84.1/0.84.3)也不是权威 0.88 基线

关于基线:本次验证在RN0.84.1+ RNOH0.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 本次验证的边界

  1. ★★showHistory的真实读写链路未验证(页面上的取证按钮没点中)⇒loadHistoryAsync/addToHistoryAsync在 RNOH 上是否能真正存取,未测;
  2. ★ 我自己测试页有个缺陷:把<EmojiSelector>(内含FlatList)放进了<ScrollView>⇒触发了VirtualizedLists should never be nested…警告(logbox_notification3 个)——
    这是我的页面布局问题,不是库的问题;
  3. 未逐格核对 Emoji 网格的实际渲染(只验证了colSize与returnSectionData()的条数);
  4. 未验证搜索过滤的准确性(只验证了"能调用、标题变成Search Results、返回 1766 条");
  5. 未验证分类 Tab 的切换效果(只验证了handleTabSelect能被调用);
  6. 未验证shouldInclude过滤 prop;
  7. 未验证theme/placeholder/showSectionTitles等外观 prop 的实际效果;
  8. 未验证历史记录去重逻辑(addToHistoryAsync里按unified去重那段);
  9. 未在RN 0.88.0/RNOH 0.84.4上复验(交付包声明的基线);
  10. 未测真机与其它屏幕宽度(只验证了容器宽 408px 这一种情况);
  11. 未做跨平台对照。

未改动库代码。


八、常见问题

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:装依赖时要注意什么?

卡版本:

npminstallemoji-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 条判据全部通过,装完即可用。

最值得记下来的是这几条:

  1. ★★★★ “按源码该崩、实测却不崩” —— 又一次用实测钉死了推断。
    库从react-native解构AsyncStorage,而上游 RN core 把它定义成抛错 getter⇒ 按理该崩;
    实测访问它不抛错(try/catch探测为false)⇒ 这正是库能正常加载的原因 ✓
    ⇒跨平台 API 的可用性必须实测,别拿上游源码直接推(另一条同类经验:状态栏高度这类"某平台专有"的量,也可能已经被正确实现)。

  2. ★★★★ 几何公式可以直接预言,onLayout是可靠那一侧。
    colSize = floor(width / columns)⇒ 实测floor(408/6)=68与floor(408/4)=102全部精确命中 ✓
    同时isReady从false走到true,证明onLayout → setState → 回调 → setState整条链在 RNOH 上跑通 ✓
    (反过来,用measure()量布局在 RNOH 上拿不到高度—— 这个库没用它,选对了。)

  3. ★★★★ 导出的纯函数是最便宜的硬判据。
    charFromEmojiObject三条断言:1F600 → 😀、1F44D → 👍、多码位1F1E8-1F1F3 → 🇨🇳(后者验证了split("-")+fromCodePoint这条链)✓

  4. ★★★★ 交互方法都是箭头类字段 ⇒ 可以用ref编程调用,判据不依赖点击。
    handleSearch("smile")、handleTabSelect(Categories.food)、handleEmojiSelect({unified:"1F600"})
    ⇒onEmojiSelected精确收到"😀"✓,returnSectionData()返回1766条 ✓

  5. ★★★ 两类成员混用必须先看源码再写断言。
    5 个原型方法(returnSectionData/prerenderEmojis/…)+ 7 个箭头类字段(handleTabSelect/handleSearch/…)
    ⇒ 实测5/5 在 prototype、0/7 箭头字段在 prototype✓

  6. ★★ 依赖要按库声明的范围装。库要emoji-datasource@^6.0.0,而它latest已是16.x⇒ 装 latest 会拿到不兼容数据结构;
    且本地目录 /file:接入不会自动装库自己的依赖⇒ 必须显式补装 ✓(本次装到 6.1.1)

  7. ★ 测试页自身的坑也要如实写出来。我把含FlatList的组件放进了ScrollView⇒ 触发VirtualizedLists should never be nested…警告(LogBox 3 条)——
    那是我的页面问题,不是库的问题✓

  8. ★ 判据逐条直写日志(PASS|描述|期望|实际)⇒ 采集端grep即得全证据,不把证据留存绑在 UI 上 ✓


本篇用到的库

项内容
三方库react-native-emoji-selector(上游0.2.0的鸿蒙适配版)
适配仓库https://atomgit.com/oh-react-native/react-native-emoji-selector
适配 TAG0.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)
# 从适配仓库安装(⚠️ 必须补装它自己的依赖,并卡版本)npminstall"git+https://atomgit.com/oh-react-native/react-native-emoji-selector.git#0.2.0-ohos-1.0.0"--ignore-scriptsnpminstallemoji-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-bcom.rnoh084.demo-aEntryAbility--psrnAppKey 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 for OpenHarmony(RNOH)。从零准备开发环境(DevEco Studio、HarmonyOS SDK API 26、Node.js、ohpm、hvigorw、模拟器与hdc调试)请按 CPF-RN 开发者指南的「环境初始化」操作:

  • CPF-RN 开发者指南 · 02-搭建准备 · 环境初始化:https://atomgit.com/CPF-RN/docs/blob/main/开发者指南/02-搭建准备/环境初始化.md

环境就绪后,本次验证使用的具体版本组合见文末「验证环境」一节。


验证环境

项版本
React Native0.84.1
React19.2.3
RNOH(npm / ohpm)@react-native-oh/react-native-harmony/@rnoh/react-native-openharmony0.84.3
Node.js / npmv24.14.0/11.9.0
DevEco Studio26.0.0.621
HarmonyOS SDKAPI 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
本次构建assembleHap3 分 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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询