本文记录把react-native-toast-notifications(全局 Toast 通知)适配到 HarmonyOS 的完整过程。
它的用法是**“Provider + 编程接口”:你在根上包一层<ToastProvider>,然后拿一个容器对象**,用show()/hide()/hideAll()/update()/isOpen()在代码里弹提示 ——不需要在 JSX 里摆组件。
结论:这属于"最省事"的一类库—— 纯 JS、零依赖(连prop-types都不需要)、没有一处已移除的 API。装完直接用。
★★★★ 最重要的一点:它的接口是"可编程验证"的
这也是本文最想推荐的一种验证姿势。因为它的核心 API 是返回值和布尔查询,而不是"点了以后看起来弹出来了":
constid=toast.show("消息",{duration:5000});// ← 返回一个 idtoast.isOpen(id);// ← 返回布尔toast.hide(id);// ← 按 id 关toast.hideAll();// ← 全关toast.update(id,"新消息",{...});// ← 按 id 改实测把容器拿到手之后,先把它的接口面印出来:
useToast() 返回值的 typeof = object useToast() 上的方法 = _reactInternalInstance, _reactInternals, context, hide, hideAll, isOpen, props, refs, show, state, update, updater⇒show/hide/hideAll/isOpen/update五个接口全在✓
⇒ 同时还带_reactInternalInstance/props/state/refs/context/updater⇒这组字段只有 class 组件实例才有⇒反过来印证了"容器是类组件"✓
show()的返回值也逐字对上了源码(源码里是Math.random().toString()):
MANUAL show id=0.31351971563374365 MANUAL show id=0.2736911534854683 MANUAL show id=0.08056915509117747⇒ 三次都是0.<17 位>的字符串 ✓
★★★ 位置与居中也是可量的
<ToastProvider placement="top" offset={48}>之下,弹出来的 toast 在布局 dump 里是:
Text#Paragraph(250) [447,195,875,244] "手动 toast(success)"| 项 | 源码 / 参数 | 实测 |
|---|---|---|
| 水平位置 | 容器alignSelf: 'center' | 文本x=447..875⇒中心 661 ≈ 屏宽的一半(660)✓ |
| 垂直位置 | placement="top"+offset=48(vp) | y=195px✓(48vp = 144px,再加 SafeArea 的内边距) |
| 弹层不占文档流 | 容器position: 'absolute' | 页面其余元素位置不受影响 ✓ |
容器自身的关键样式(源码里的styles.container):
container:{flex:0,position:Platform.OS==="web"?"fixed":"absolute",// ⇒ 鸿蒙用 absolutemaxWidth:"100%",zIndex:999999,elevation:999999,alignSelf:'center',}而且三个 placement 分支的容器都传了pointerEvents="box-none"⇒ 容器本身不拦触摸 ✓
★★ 三处平台分支(都不会崩,但值得知道)
useNativeDriver:Platform.OS!=="web"// ⇒ 鸿蒙上为 trueconstswipeThreshold=Platform.OS==="android"?10:0;// ⇒ 鸿蒙上为 0behavior={Platform.OS==="ios"?"position":undefined}// ⇒ 鸿蒙上是 undefined| 分支 | 鸿蒙上的取值 | 影响 |
|---|---|---|
useNativeDriver | true | 真正启用原生驱动(不同于"没传这个参数"的库);本工程此前已实测过"原生驱动下Animated.Value的addListener仍能收到更新" |
swipeThreshold | 0 | onMoveShouldSetPanResponder里 |dx|>0 或 |dy|>0 就会夺取手势 ⇒微小位移也算滑动(真正划走仍需 |dx|>50,所以危害有限) |
KeyboardAvoidingView.behavior | undefined | 等价于不启用该行为(不崩) |
⚠️ 本文有一件事没做成(如实记录)
我原本想在页面上放一个"断言面板",一键跑完 A/B/C/D 四组判据并把结果渲染出来。
结果:那四组判据一条日志都没产出——自动运行的入口日志打了(===== 自动运行 =====),但后续没有任何一行;页面上的四张汇总卡一直是"未运行"。
而同一个页面上的"手动"按钮(直接调用show()/hideAll())完全正常—— 从日志到 toast 渲染都验证过了 ✓
⇒原因我没有定位到(我怀疑与"把结果汇总到一个模块级数组 + 手动pushAll()刷新"这套写法有关,但没有确证)。
⇒所以本轮的设备侧证据,来自【直接调用编程接口】这条链路(它比断言面板更硬):
show() 返回值 = 0.31351971563374365 / 0.2736911534854683 / 0.08056915509117747 ✓ 与源码的 Math.random().toString() 一致 useToast() 的接口面 = show/hide/hideAll/isOpen/update + React 类实例内部字段 ✓ toast 渲染节点 = [447,195,875,244](居中 + offset=48vp ⇒ y≈195px) ✓ hideAll 调用后 toast 计数下降 ✓★ 这里有一条很值得记的教训:
判据结果应当"逐条直写日志",不要依赖一个"汇总面板"。
我这次把 20 多条断言压成一个runAll()+ 一个模块级数组 + 一次pushAll()渲染 ——只要这条链路有一处不生效,全部判据一起失效(日志里一片空白)。
而每断言一行console.log的写法,即使 UI 出问题,证据也还在日志里,采集端grep就能拿到。
一、先说结论
| 项 | 结果 |
|---|---|
| 上游最新版 | 3.4.0;license: MIT;上游 star585(⚠️ npm 的repository字段为空 ⇒ 需从homepage找仓库) |
| 库类型 | 纯 JS 组件库(src/是 TypeScript;交付包同时给了src/与lib/(commonjs + module + typescript)) |
| 是否需要原生适配 | ✅不需要(无harmony//ios//android/、无原生源文件、JS 里无NativeModules) |
| 运行时依赖 | ✅零(dependencies为空;peer 只要react/react-native) |
⚠️scripts.prepare = "bob build" | ⚠️非空(交付包已预编译好lib/⇒ 用--ignore-scripts安装即无阻塞) |
| main / module / types | lib/commonjs/index.js/lib/module/index.js/lib/typescript/index.d.ts |
| 需要 HAR / 权限 / ohpm | ❌ 都不需要 |
| ★ 是否可用 | ✅可以直接用(编程接口、渲染、几何都实测通过) |
| ★★★★ 接口面 | useToast()返回容器实例:show/hide/hideAll/isOpen/update✓ |
★★★★show()返回值 | Math.random().toString()形式的字符串 id✓(三次实测一致) |
| ★★★ 几何 | placement="top"+offset=48⇒ 文本x居中(中心 661)、y=195px✓ |
| ★ 结构 | 容器是类组件:render*在原型上,show/hide/hideAll/isOpen/update是箭头类字段(两类混用) |
| ⚠️ 导出命名易混 | 模块级单例是以Toast这个名字导出的(源码里叫GlobalToast) |
二、判定与接入
| 步 | 做法 | 结果 |
|---|---|---|
| ① | package.json里有没有harmony.autolinking | 没有 |
| ② | 仓库里有没有harmony//ios//android/ | 都没有 |
| ③ | ★ 原生痕迹预扫 | 无原生源文件、无NativeModules⇒纯 JS✓ |
| ④ | import 图体检 | 只有内部模块 + react/react-native⇒零外部依赖✓ |
| ⑤ | scripts扫prepare家族 | ⚠️有prepare: "bob build"⇒ 安装时加--ignore-scripts✓ |
| ⑥ | ★ 老 API 预扫 | 只命中Dimensions.removeEventListener(有配对的addEventListener✓ 正常);从react取的是Component/useRef/useEffect/useState/FC⇒都仍存在✓ |
| ⑦ | ★ 平台分支预扫 | 多处:useNativeDriver(web 判断)、swipeThreshold(android 判断)、KeyboardAvoidingView.behavior(ios 判断)⇒ 见上表 |
| ⑧ | 成员写法预扫 | 两类混用(原型render*+ 箭头字段show/hide/…)⇒ 断言前先分组 ✓ |
npminstall"git+https://atomgit.com/oh-react-native/react-native-toast-notifications.git#3.4.0-ohos-1.0.0"--ignore-scripts装完即可用(本工程实测:打包与构建都成功、show()/hideAll()都生效、toast 正常渲染且位置正确)。
三、用法与默认值(读源码整理)
import {ToastProvider, useToast} from 'react-native-toast-notifications'; // ① 根上包一层(placement / offset / duration 等都在这里给默认值) <ToastProvider placement="top" offset={48} duration={3000} animationType="slide-in"> <App /> </ToastProvider> // ② 任意子组件里拿容器 function App() { const toast = useToast(); const id = toast.show('保存成功', {type: 'success'}); // ⇒ 返回 id toast.show('常驻提示', {duration: 0}); // duration=0 ⇒ 不自动关闭 toast.isOpen(id); // ⇒ 布尔 toast.update(id, '已更新'); toast.hide(id); toast.hideAll(); return null; }源码里的默认值:
| 项 | 默认 |
|---|---|
placement | "bottom"(provider 上是"top"之类由你定) |
offset | 10(还有offsetTop/offsetBottom) |
swipeEnabled | true |
duration(每条 toast) | 5000(0⇒ 不自动关闭) |
animationDuration | 250 |
animationType | "slide-in"(另有"zoom-in") |
type | "normal"(另有success/danger/warning,可自定义) |
接口语义:
| 接口 | 说明 |
|---|---|
show(message, options) | 返回 id;无 id 时用Math.random().toString();内部用requestAnimationFrame后setState入栈 |
hide(id)/hideAll() | 把对应 toast 的open置false⇒ 触发关闭动画(250ms)后onDestroy出栈 |
isOpen(id) | toasts.some(t => t.id === id && t.open) |
update(id, message, options) | 按 id 就地更新 |
onClose/onPress(id)/onDestroy | 回调 |
swipeEnabled | 为真时用PanResponder支持左右划走(真正划走需 |dx|>50) |
★ 导出命名要注意(src/index.ts):
export{default}from"./toast-container";// 默认导出 = ToastContainer 类export{defaultasToastProvider}from"./hook/provider";export{defaultasuseToast}from"./hook/useToast";export{GlobalToastasToast}from'./hook/provider'// ★ 模块级单例叫 Toast(源码里叫 GlobalToast)⇒Toast这个名字指的是"模块级单例容器",不是组件—— 容易误会 ✓
四、已知限制与本次验证边界
4.1 用这个库要知道的
| 事项 | 说明 |
|---|---|
| ★ 接口是"编程式"的 | 用useToast()拿容器;show()返回 id,isOpen(id)返回布尔 ⇒很好做自动化验证 |
★duration: 0表示常驻 | 不设自动关闭定时器 |
| ★ 关闭是"先动画 250ms 再出栈" | 调hide()/hideAll()之后不是立刻从布局里消失 |
★useNativeDriver在鸿蒙上是true | 真正走原生驱动 |
★swipeThreshold在鸿蒙上是0 | 微小位移即夺取手势(但划走仍需 |dx|>50) |
★ 容器position: absolute+zIndex/elevation: 999999 | 浮在最上层;三个分支都带pointerEvents="box-none" |
★Dimensions.get('window')在模块作用域求值一次 | 宽高被缓存,之后不会更新(本机 440×744) |
⚠️scripts.prepare = "bob build" | 安装时建议--ignore-scripts(交付包已带编译产物) |
⚠️ npm 的repository为空 | 找上游仓库要走homepage |
4.2 本次验证的边界
- ★★ 我的"断言面板"(A/B/C/D 四组)没有产出任何结果⇒那四组判据(含"duration 自动关闭的时间序列")本轮全部未取到✗(原因未定位;同时段的手动按钮链路完全正常);
isOpen(id)的布尔语义未直接实测(它是断言面板的一部分);duration的自动关闭时间序列未测;update(id, …)未测;placement='bottom'/'center'未量(只量了top);swipeEnabled的左右划走未测(swipeThreshold=0的实际影响未验证);animationType='zoom-in'未测;type的四种图标/配色未逐一验证;- 自定义
renderToast/renderType未测; onPress/onClose回调未逐一验证;hideAll()的效果只做了粗略计数(受 250ms 关闭动画干扰,未能精确断言"归零");- 只在一台模拟器上验证(
Pura X View,density 3,1320×2232),没有真机;未做跨平台对照。
未改动库代码。
五、我自己的三个失误(都值得记)
| # | 失误 | 现象 | 纠正 |
|---|---|---|---|
| 1 | 把 20 多条判据压成一个runAll()+ 一次pushAll()渲染 | 该链路一处不生效 ⇒四组判据的日志"一片空白"、页面汇总卡一直"未运行" ✗ | 判据应逐条直写日志(每断言一行console.log),别把证据成败绑在汇总面板上 |
| 2 | 用 PowerShell 双引号字符串改 TS 文件 | 反引号模板字面量与${}被吃掉⇒SyntaxError: Unexpected token⇒ 打包失败 ✗ | 改用edit工具(对反引号/${}最安全) |
| 3 | .Trim()前多打了一个空格 | 整个作业解析失败、一条命令都没执行✗(我笔记里记过的老坑) | 去掉空格;或改用更简单的表达 |
六、常见问题
Q1:它需要原生模块吗?
不需要。仓库里没有harmony//ios//android/,也没有任何原生源文件,JS 里也没有NativeModules⇒纯 JS、零依赖✓
Q2:怎么在代码里弹一条 toast?
// 根上包一层 <ToastProvider><App /></ToastProvider> // 子组件里 const toast = useToast(); const id = toast.show('保存成功', {type: 'success'}); // ⇒ 返回 idQ3:怎么让它一直显示不自动关?
toast.show('常驻提示',{duration:0});源码里的判断是:if (duration !== 0 && typeof duration === "number") { setTimeout(...) }⇒0就不设定时器✓
Q4:怎么关掉?
toast.hide(id);// 按 id 关(会先播 250ms 关闭动画)toast.hideAll();// 全关toast.isOpen(id);// 查是否还开着(布尔)Q5:为什么ZIndex那么高、会不会挡住我的按钮?
容器的styles.container里是zIndex: 999999; elevation: 999999; position: absolute,但三个 placement 分支的容器都传了pointerEvents="box-none"⇒容器本身不拦触摸(只有真正渲染出来的 toast 会接收触摸)。
Q6:为什么我拿到的Toast不是组件?
因为源码里是export {GlobalToast as Toast}——它是"模块级单例容器",不是组件。想要组件请用ToastProvider(或默认导出ToastContainer)。
Q7:show()返回的 id 长什么样?
不长什么样 —— 它默认就是Math.random().toString():
0.31351971563374365 0.2736911534854683 0.08056915509117747想自己控制就用options.id指定 ✓
Q8:这个库需要鸿蒙侧做什么特殊处理吗?
不需要。纯 JS、零依赖、无原生代码 ⇒ 直接装、直接用 ✓
(唯一留意的是swipeThreshold在鸿蒙上是0,以及useNativeDriver为true)
小结
react-native-toast-notifications是纯 JS 的全局 Toast(零依赖),接口是"Provider + 编程式容器"。设备侧我验证了接口面、show()的返回值、toast 的渲染与几何、hideAll()生效。
最值得记下来的是这几条:
★★★★ 它的接口天生适合"可编程验证"。
show()返回 id、isOpen(id)返回布尔、hide/hideAll/update按 id 操作 ⇒不需要"点按钮看它弹出来"这种脆弱判据。
实测把容器拿到手后打印它的接口面,直接确认了五个接口俱在:useToast() 上的方法 = _reactInternalInstance, _reactInternals, context, hide, hideAll, isOpen, props, refs, show, state, update, updater⇒ 顺带因为看到
_reactInternalInstance/props/state/refs,反证了容器是 class 组件实例✓★★★★
show()的返回值与源码逐字一致。源码是Math.random().toString()⇒ 实测三次都是0.<17 位>的字符串 ✓★★★ 位置与居中可量。
placement="top"+offset=48⇒ 文本节点x=447..875(中心 661 ≈ 屏宽一半)、y=195px✓★★ 三处平台分支都不会崩,但会影响行为:
useNativeDriver在鸿蒙上是true(真启用原生驱动)、swipeThreshold是0(微小位移即夺取手势)、KeyboardAvoidingView.behavior是undefined。★★ 一个很容易踩的命名坑:
export {GlobalToast as Toast}——Toast是"模块级单例容器",不是组件。★★★ 最重要的一条过程教训:判据要逐条直写日志,不要依赖"汇总面板"。
本轮我把 20 多条判据压成一个runAll()+ 模块级数组 + 一次pushAll()渲染 ⇒那条链路一处不生效,全部判据的日志一起空白(页面汇总卡始终"未运行"),而同一页面上的手动调用链路完全正常。
⇒每断言一行console.log,采集端grep就能拿到证据 ——不要把"证据能不能留下来"绑在 UI 上。
本篇用到的库
| 项 | 内容 |
|---|---|
| 三方库 | react-native-toast-notifications(上游3.4.0的鸿蒙适配版) |
| 适配仓库 | https://atomgit.com/oh-react-native/react-native-toast-notifications |
| 适配 TAG | 3.4.0-ohos-1.0.0 |
| 需要 HAR / 权限 / ohpm | 都不需要(纯 JS、零依赖) |
| 库类型 | 纯 JS 全局 Toast(Provider + 编程式容器) |
| 运行时依赖 | ✅零 |
⚠️prepare | 有(bob build)⇒ 安装时用--ignore-scripts |
| 许可证 | MIT |
| 上游仓库 | https://github.com/arnnis/react-native-toast-notifications(★585) |
| 宿主工程 | RNOH084Demo(测试页rnAppKey = ToastNotificationsTestApp) |
# 从适配仓库安装(零依赖;注意库自带 prepare 脚本 ⇒ 加 --ignore-scripts)npminstall"git+https://atomgit.com/oh-react-native/react-native-toast-notifications.git#3.4.0-ohos-1.0.0"--ignore-scripts# 本地目录接入npminstall../react-native-toast-notifications --ignore-scriptsimport {ToastProvider, useToast} from 'react-native-toast-notifications'; // ① 根上包一层(这些值会成为每条 toast 的默认值) export default function App() { return ( <ToastProvider placement="top" offset={48} duration={3000} animationType="slide-in" successColor="#0A7D2C" dangerColor="#D0021B" warningColor="#E67E22"> <Page /> </ToastProvider> ); } // ② 任意子组件里用编程接口 function Page() { const toast = useToast(); return ( <> <Button title="成功提示" onPress={() => { const id = toast.show('保存成功', {type: 'success'}); // ⇒ 返回 id(Math.random().toString()) console.log(id, toast.isOpen(id)); // ⇒ true }} /> <Button title="常驻提示" onPress={() => toast.show('常驻', {duration: 0})} /> <Button title="改内容" onPress={() => toast.update(id, '已更新')} /> <Button title="全关" onPress={() => toast.hideAll()} /> </> ); } // ⚠️ 三处平台分支:useNativeDriver 鸿蒙为 true;swipeThreshold 鸿蒙为 0; // KeyboardAvoidingView.behavior 鸿蒙为 undefined // ⚠️ 导出命名:export {GlobalToast as Toast} ⇒ Toast 是"模块级单例容器",不是组件 // ⚠️ 关闭是先播 250ms 动画再出栈 ⇒ hide 之后不是立刻从布局消失 // ⚠️ Dimensions.get('window') 在模块作用域求值一次(本机 440×744) // ⚠️ 库自带 scripts.prepare = "bob build" ⇒ 安装加 --ignore-scripts# 换页启动测试页 —— force-stop 不能省hdc shell aa force-stop com.rnoh084.demo hdc shell aa start-bcom.rnoh084.demo-aEntryAbility--psrnAppKey ToastNotificationsTestApp判据的复现方式
# ① 接口面:把 useToast() 的返回值与键打出来# ⇒ show / hide / hideAll / isOpen / update(+ React 类实例内部字段)# ② show() 的返回值:期望 Math.random().toString() 形式hdc shell"hilog -x | grep toastn-test"# ⇒ MANUAL show id=0.31351971563374365# ③ 几何:toast 渲染后读 dump 里的文本节点devecocli ui layout|Select-String'手动 toast'# ⇒ [447,195,875,244] ⇒ x 居中、y 由 placement+offset 决定# ④ 关闭:调 hideAll(),再 dump 看 toast 文本是否消失(注意 250ms 关闭动画)# ⑤ ★ 判据请【逐条直写 console.log】,不要只渲染到一个汇总面板上验证环境
| 项 | 版本 |
|---|---|
| React Native | 0.84.1 |
| React | 19.2.3 |
| RNOH(npm / ohpm) | @react-native-oh/react-native-harmony/@rnoh/react-native-openharmony0.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 |
| 宿主依赖 | ✅无需补装(库零依赖) |
| 宿主 HAP 产物 | entry-default-signed.hap |
| 本次构建 | 首次因我改文件时的语法错误打包失败(未装机)⇒assembleHap3 分 39 秒 ⇒ 3 分 38 秒(修好后) |
| 验证规模 | 接口面 1 项(5 个接口俱在);show()返回值 3 次(均为Math.random().toString());渲染与几何 1 项(x居中、y=195px);hideAll1 次;4 张截图互异;⚠️断言面板 4 组未产出结果 |
相关社区与项目地址
| 项目 | 地址 |
|---|---|
| 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