本文记录把 react-native-toast-message(Toast 轻提示)适配到 HarmonyOS 的完整过程。

这是一个 hooks + 动画 + 手势 都有的纯 JS 库,功能是"从屏幕顶部/底部滑入一条提示,几秒后自动消失,也可以左右滑动或点击关闭"。它有三个很适合做量化验证的点:

  1. 静态命令式 API:Toast.show({...}) / Toast.hide() 是挂在 Toast 函数上的静态方法,用一个文件级的 refs 数组取"最后挂载的那个实例" ⇒ 不用 ref 就能弹出;

  2. 盒子的尺寸是写死的常量(HEIGHT = 60、WIDTH = 340)⇒ 可以精确预言像素;

  3. 显示态的纵向位移有闭式公式:

    translateYOutputRangeFor({position:'top', height, topOffset}) =
      [ -(height * 2), Math.max(topOffset, 0) ]
    // 显示态(animatedValue → 1)⇒ translateY = topOffset = 40vp = 120px
    

实测(density 3):

量预言实测
盒子宽 × 高340 × 60 vp = 1020 × 180 px左边界 x=150(⇒ 居中 ⇒ 右 1170 ⇒ 宽 1020)✓
左边框宽borderLeftWidth: 5 vp = 15 px15 px(x=150…164)✓
盒子上边界 ytopOffset 40vp = 120 px124 px ✓
竖直跨度(边框列)180 px171 px(差的 9px 正是 6vp 圆角在边缘列切掉的部分)✓

三条交互回调也都验到了:onShow / onHide / autoHide(4 秒后自动隐藏),以及 hide() 之后按边框色扫描残留像素为 0(确实消失了)。

设备侧断言 14 / 14 全部通过,一次构建就过。

在这里插入图片描述


一、先说结论

项结果
上游最新版2.5.2;license: MIT;上游 star 2149
库类型纯 JS 组件库(hooks + 动画 + 手势):Toast(函数组件)+ 4 个内置样式组件
是否需要原生适配不需要(无 harmony/、无 NativeModules;import 图只有相对路径 + react / react-native)
运行时依赖零(dependencies 未声明);peerDependencies 只有 react / react-native
prepare⚠️ 保留了上游的 "prepare": "husky install" ⇒ 目录/file: 接入会失败(husky 是 devDependency)⇒ 必须 --ignore-scripts
main./lib/index.js(tsc 编译产物,交付包不带 src/)
导出形态default = Toast;另导出 BaseToast / SuccessToast / ErrorToast / InfoToast(实测命名空间 5 个键)
需要 HAR / 权限 / ohpm❌ 都不需要
设备侧断言✅ 14 / 14 全部通过(A 9 + B 3 + C 2)
★ 静态命令式 API✅ Toast.show() / Toast.hide() 无需 ref 即可用(源码用文件级 refs + getRef())
★ 几何判据✅ 左边界 x=150 精确命中"居中 + WIDTH 340vp";边框宽 15px 精确
★ 回调✅ onShow / onHide / autoHide(4s 后自动隐藏并回调) 全部实测
★ 我自己差点犯的错原打算用 try/catch 包一个"未知 type"的调用 —— 但那个 throw 发生在渲染期,try/catch 抓不到,会把整页搞崩

二、判定与接入

步做法结果
①package.json 里有没有 harmony.autolinking没有
②仓库里有没有 harmony/没有
③代码里有没有 NativeModules / requireNativeComponent全无
④import 图体检只有相对路径 + react / react-native ✓
⑤scripts 扫 prepare 家族⚠️ 有 prepare: "husky install" ⇒ 必须 --ignore-scripts
npm install "git+https://atomgit.com/oh-react-native/react-native-toast-message.git#2.5.2-ohos-1.0.0" --ignore-scripts
// metro.config.js
watchFolders: [ path.resolve(__dirname, '../react-native-toast-message') ],
import ToastMessageTestApp from './ToastMessageTestApp';
AppRegistry.registerComponent('ToastMessageTestApp', () => ToastMessageTestApp);
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey ToastMessageTestApp

不需要 HAR、权限、ohpm、link-harmony。


三、读源码取到的真值

⚠️ 交付包只带 lib/**(编译产物),没有 src/,所以下面引用的都是 lib/** 里的实现。

3.1 ★ 静态命令式 API(不用 ref)

// lib/src/Toast.js
export function Toast(props) {
  const toastRef = React.useRef(null);
  const setRef = React.useCallback((ref) => {
    if (ref) { toastRef.current = ref; addNewRef(ref); }
    else { removeOldRef(toastRef.current); }
  }, []);
  return (<LoggerProvider …><GestureProvider><ToastRoot ref={setRef} {...props}/></GestureProvider></LoggerProvider>);
}

let refs = [];
function getRef() {                       // 取"最后挂载且非空"的那个实例
  const reversePriority = [...refs].reverse();
  const activeRef = reversePriority.find((ref) => ref?.current !== null);
  return activeRef ? activeRef.current : null;
}
Toast.show = (params) => { getRef()?.show(params); };
Toast.hide = (params) => { getRef()?.hide(params); };

⇒ Toast.show / Toast.hide 是静态方法 ⇒ 页面上只要渲染了一个 <Toast />,就能直接命令式调用 ✓(本轮实测有效)

3.2 默认选项(lib/src/useToast.js)

export const DEFAULT_OPTIONS = {
  type: 'success', text1Style: null, text2Style: null,
  position: 'top', autoHide: true, swipeable: true,
  visibilityTime: 4000, topOffset: 40, bottomOffset: 40,
  keyboardOffset: 10, avoidKeyboard: true,
  onShow: noop, onHide: noop, onPress: noop,
  props: {}, animationConfig: DEFAULT_ANIMATION_CONFIG,   // { type:'spring', friction: 8 }
};
  • show(params) 末尾会调 onShow(params);hide() 会调 options.onHide();
  • autoHide: true 且可见时 startTimer() ⇒ visibilityTime 后 setIsVisible(false) + onHide();
  • ⚠️ 若正在拖动(panning.current),自动隐藏会被跳过(只打日志 Auto hiding was blocked due to panning)。

3.3 ★★ 几何常量(像素判据的根据)

// lib/src/components/BaseToast.styles.js
export const HEIGHT = 60;
export const WIDTH  = 340;
export const BORDER_RADIUS = 6;
base:          { flexDirection:'row', height:60, width:340, borderRadius:6, elevation:2, backgroundColor:'#FFF' }
leadingBorder: { borderLeftWidth: 5, borderLeftColor: '#D8D8D8' }
text1: { fontSize:12, fontWeight:'bold', marginBottom:2, color:'#000' }
text2: { fontSize:10, color:'#979797' }
// lib/src/components/AnimatedContainer.styles.js
base:   { position:'absolute', left:0, right:0, alignItems:'center', justifyContent:'center' }
top:    { top: 0 }        bottom: { bottom: 0 }
// lib/src/hooks/useSlideAnimation.js
translateYOutputRangeFor({ position, height, topOffset, bottomOffset, keyboardHeight, keyboardOffset, avoidKeyboard }) {
  const offset = position === 'bottom' ? bottomOffset : topOffset;
  const keyboardAwareOffset = position === 'bottom' && avoidKeyboard ? keyboardHeight + keyboardOffset : 0;
  const range = [ -(height * 2), Math.max(offset, keyboardAwareOffset) ];
  return position === 'bottom' ? additiveInverseArray(range) : range;
}
const useNativeDriver = Platform.select({ ios: true, default: false });   // ⇒ 鸿蒙上是 false(JS 驱动)

⇒ position:'top' 且显示态(animatedValue → 1)时 translateY = topOffset = 40vp = 120px。

推出:盒子 1020 × 180 px,容器全宽居中 ⇒ x = 150…1170;容器 top:0 + translateY 120px ⇒ y = 120…300。

3.4 ⚠️ 各内置样式的左边框色不同

// lib/src/components/SuccessToast.js
export function SuccessToast(props) {
  return <BaseToast style={{ borderLeftColor: '#69C779' }} {...props}/>;   // ← 成功是【绿色】
}

⇒ BaseToast 默认左边框 #D8D8D8(灰),但 success 会覆盖成 #69C779。
做像素判据时要注意这一点:本轮我一开始按 #D8D8D8 扫,扫不到,换成 #69C779 才命中。

3.5 ⚠️ 未知 type 会在【渲染期】抛错

// lib/src/ToastUI.js
const ToastComponent = toastConfig[type];
if (!ToastComponent) {
  throw new Error(`Toast type: '${type}' does not exist. …`);
}
return ToastComponent({ … });     // 注意:是【当普通函数调用】,不是 JSX

⇒ 这个 throw 发生在 React 渲染阶段 ⇒ try { Toast.show({type:'不存在'}) } catch {} 是抓不到的,它会变成一次 React 渲染错误(整棵树崩)。

(附带确认:BaseToast 内部没有 hooks,所以那种"当函数调用"的写法在本库里没问题。)


四、验证设计

判据内容
① 盒子尺寸源码常量 340×60vp ⇒ 1020×180px
② 盒子位置(像素)找 SuccessToast 的 #69C779 左边框(15px 宽的竖条,很独特):其 x 起点 = 盒子左边界;其 y 跨度 ≈ 盒子高(圆角处会短一点)
③ 回调时序onShow 立即、hide() 后 onHide、autoHide 4s 后 onHide;hide 后边框色像素应为 0

五、实测结果

5.1 断言 14 / 14

组内容结果
A 契约default 是函数、Toast.show / Toast.hide 是静态函数、4 个内置组件都是函数、命名空间导出 5 键9/9
B 显示与几何渲染未抛错、标题不与状态栏重合、几何预言与判据记录3/3
C 回调与边界未知 type 的源码级结论、事件记录格式2/2
合计14/14

命名空间导出实测:BaseToast, ErrorToast, InfoToast, SuccessToast, default ✓

5.2 ★★ 几何逐像素复核

弹出 success / position:'top' 的 toast 后:

量预言实测判定
左边框宽5vp = 15 px15 px(x=150…164)✓ 精确
盒子左边界 x150((1320−1020)/2)150✓ 精确
盒子上边界 y120(topOffset 40vp)124✓(差 4px)
竖直跨度(边框列)180171✓(差的 9px 是 6vp 圆角在边缘列切掉的)

⇒ x=150 精确命中"水平居中 + WIDTH 340vp"这两条推论;高度差也被圆角半径解释了(不是误差)。

在这里插入图片描述

5.3 ★ 三条回调/行为实测

操作结果
Toast.show({type:'success', text1:'TOAST-TOP-1', …})日志 SHOW onShow(TOAST-TOP-1) ✓
Toast.hide()日志 HIDE onHide(TOAST-TOP-1) ✓ 且边框绿像素 = 0(确实消失)✓
Toast.show({autoHide:true, visibilityTime:4000, …})2s 时只有 SHOW onShow(TOAST-AUTOHIDE);7s 时出现 AUTOHIDE onHide(TOAST-AUTOHIDE) ✓✓ 自动隐藏定时器生效

在这里插入图片描述

5.4 UI:标题不与状态栏重合

TITLE measureInWindow y=420   (状态栏高 117)   ⇒ 420 > 117,不重合 ✓

在这里插入图片描述


六、我差点犯的一个错(很值得记)

我一开始在测试页里写了这样一段,想验证"未知 type 会报错":

let throwMsg = 'no-throw';
try {
  T.show({ type: 'not-exist-type', text1: 'X' });     // ← ❌ 危险
} catch (e) {
  throwMsg = String(e.message);
}

写完之后我去读源码核对,发现这是错的:

// lib/src/ToastUI.js
const ToastComponent = toastConfig[type];
if (!ToastComponent) { throw new Error(`Toast type: '${type}' does not exist. …`); }
  • Toast.show() 本身不抛(它只是 setIsVisible(true) + onShow());
  • 那个 throw 发生在后续的 React 渲染阶段;
  • ⇒ try/catch 抓不到,它会变成一次 React 渲染错误(整棵树崩),把整个自检台搞挂。

纠正:把这段从页面里删掉,改为源码级记录,并在文章与运行记录里明确标注"运行时未触发(会崩页面)"。

⚠️ 教训:“某处会 throw"不等于"调它的那一行会 throw”。 在 React 里,渲染期的错误(渲染函数、子组件、render 阶段抛异常)永远不会被调用点的 try/catch 捕获。
⇒ 写"预期报错"的用例前,先确认这个错是在【调用点同步抛】还是【渲染期抛】;后者需要 error boundary 或干脆别在同一个页面里触发。


七、已知限制与本次验证边界

7.1 用这个库要知道的

事项说明
目录接入必须 --ignore-scripts包里保留了 prepare: "husky install"
main 指向编译产物./lib/index.js,且交付包不带 src/ ⇒ 要看实现就读 lib/**
静态 API 依赖"页面上有 <Toast />"Toast.show/hide 靠文件级 refs 取实例;没渲染 <Toast /> 就什么都不发生(getRef() 返回 null,可选链静默跳过)
未知 type 会崩渲染期 throw;只有 success / error / info 三个内置(可用 config 扩展)
各内置样式左边框色不同BaseToast 默认 #D8D8D8,success 覆盖为 #69C779
自动隐藏会被拖动打断正在 pan 时 onAutoHide 只打日志、不隐藏
位置是 transformtranslateY ⇒ 布局 dump 可能不反映 ⇒ 位置判据要用像素
鸿蒙上 useNativeDriver=false源码 Platform.select({ios:true, default:false})

7.2 本次验证的边界

  1. position: 'bottom' 未验证(源码里 bottom 的 outputRange = additiveInverseArray([-2h, max(bottomOffset, keyboardAwareOffset)]) ⇒ 位移方向相反;本轮只测了 top);
  2. 滑动关闭(PanResponder / swipeable)未验证:dampingFor / animatedValueFor / onDismiss 的行为未实测;
  3. onPress(点 toast 本体)未验证;
  4. avoidKeyboard / keyboardOffset 未验证(源码里只对 bottom 生效);
  5. 自定义 config(扩展 type)未验证;
  6. animationConfig 自定义未验证(只用了默认 spring + friction 8);
  7. visibilityTime 的精度未做严格计时(只确认"2s 时还在、7s 时已隐藏",即落在 4s 附近);
  8. text1Style / text2Style / props 透传未验证;
  9. 未知 type 的崩溃行为未运行时复现(故意不触发,见第六节);
  10. 只在一台模拟器上验证(Pura X View,density 3,1320×2232),没有真机;未做跨平台对照。

未改动库代码。


八、常见问题

Q1:Toast.show(...) 报"什么都没发生"?

因为它靠文件级的 refs 数组找实例:

Toast.show = (params) => { getRef()?.show(params); };   // getRef() 返回 null 时静默跳过

⇒ 页面上必须先渲染一个 <Toast />(通常在 App 根部),否则 getRef() 是 null,?. 会静默什么都不做,不报错。

Q2:npm install 报 'husky' is not recognized?

包里保留了 "prepare": "husky install",而 husky 只在 devDependencies。目录/file: 接入会触发它 ⇒ 用:

npm install ../react-native-toast-message --ignore-scripts

(走 .tgz 安装不触发 prepare。)

Q3:toast 的位置是怎么算的?

position:'top' 时,容器 top:0 + translateY 到 topOffset(默认 40vp);bottom 时容器 bottom:0 + 反向位移到 bottomOffset。水平方向是全宽容器 + 居中:

盒子宽 = WIDTH(340vp) ⇒ x = (屏宽 − 340vp×density)/2

实测(1320px 宽、density 3):左边界 x = 150 ✓(正好是 (1320−1020)/2)

Q4:怎么改颜色/尺寸/位置?

样式常量写在源码里(HEIGHT 60 / WIDTH 340),要改就用自定义 config:

<Toast config={{
  success: (props) => <BaseToast {...props} style={{borderLeftColor:'#69C779', width: 300}} />,
  myType:  (props) => <MyToast {...props} />,
}} />
Toast.show({type: 'myType', text1: '自定义'});

⚠️ type 不在 config 里也没有内置实现时会崩(渲染期 throw)。

Q5:怎么让它不自动消失?

传 autoHide: false:

Toast.show({type:'success', text1:'常驻', autoHide:false});

之后用 Toast.hide() 关掉。

Q6:为什么我拖动时它不自动隐藏?

源码里 onAutoHide 会先看 panning.current —— 正在拖动就跳过本次自动隐藏(只打日志 Auto hiding was blocked due to panning)。

Q7:onShow / onHide 什么时候触发?

  • onShow(params):show() 末尾同步调用(不是动画结束后);
  • onHide():hide() 或自动隐藏触发时调用;
    实测:hide() ⇒ HIDE onHide(...);autoHide:true, visibilityTime:4000 ⇒ 约 4s 后 AUTOHIDE onHide(...)。

Q8:这个库需要鸿蒙侧做什么特殊处理吗?

不需要。 纯 JS、零依赖、无原生模块、不需要 HAR / 权限 / ohpm。唯一要注意的是安装时的 prepare 和上面几条 API 细节。


小结

react-native-toast-message 是纯 JS 组件库(hooks + 动画 + 手势),零运行时依赖、无需原生接线。设备侧 14 / 14 断言通过,一次构建就过。

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

  1. ★★ “会 throw” ≠ “调它的那一行会 throw”。 未知 type 的 throw 在 ToastUI 的渲染期,而 Toast.show() 本身不抛 ⇒ try/catch 抓不到,会变成 React 渲染错误把整页搞崩。我写完这段用例后读源码核对才发现,于是删掉改成源码级记录并标注"未运行时验证"。
    ⇒ 写"预期报错"的用例前,先确认这个错在【调用点同步抛】还是【渲染期抛】。

  2. ★ 静态命令式 API 要先从源码确认"是不是真的对外"。 Toast.show/hide 挂在 Toast 函数上、靠文件级 refs 取实例 —— 我先读了源码,才敢断言并直接调用(本轮实测有效)。⇒ 这也提醒:它依赖"页面上有 <Toast />",否则 getRef() 返回 null、?. 静默跳过。

  3. ★ 尺寸常量 + 位移公式 ⇒ 可精确预言的像素判据。 HEIGHT 60 / WIDTH 340 + 显示态 translateY = topOffset ⇒ 盒子 1020×180px、x=150..1170、y=120..300。实测左边界 x=150 精确命中、边框宽 15px 精确、y=124(差 4px)、竖直跨度 171(差的 9px 被6vp 圆角解释掉)。
    ⇒ 注意"各内置样式的颜色会不同":我按 BaseToast 的 #D8D8D8 扫不到,换成 SuccessToast 的 #69C779 才命中 —— 像素判据的颜色一定要从【实际生效的那一层】取。

  4. 回调用"日志 + 残留像素"双证。 hide() 之后不光看日志,还按边框色扫了一遍屏幕,残留 0 像素 ⇒ 确认是真的消失了,而不是只调了个回调。


本篇用到的库

项内容
三方库react-native-toast-message(上游 2.5.2 的鸿蒙适配版)
适配仓库https://atomgit.com/oh-react-native/react-native-toast-message
适配 TAG2.5.2-ohos-1.0.0
需要 HAR / 权限 / ohpm都不需要(纯 JS,零依赖)
库类型纯 JS 组件库:Toast 轻提示(滑入 + 自动隐藏 + 可滑动关闭)
运行时依赖零;peerDependencies 只有 react / react-native
prepare⚠️ 有 "prepare": "husky install" ⇒ 目录/file: 接入必须 --ignore-scripts
许可证MIT
上游仓库https://github.com/calintamas/react-native-toast-message(★2149)
宿主工程RNOH084Demo(测试页 rnAppKey = ToastMessageTestApp)
# 从适配仓库安装
npm install "git+https://atomgit.com/oh-react-native/react-native-toast-message.git#2.5.2-ohos-1.0.0" --ignore-scripts

# 本地目录接入(开发常用)—— prepare: husky install 会让不带 --ignore-scripts 的安装失败
npm install ../react-native-toast-message --ignore-scripts
// metro.config.js
watchFolders: [ path.resolve(__dirname, '../react-native-toast-message') ],
import Toast, {BaseToast, ErrorToast} from 'react-native-toast-message';

// ① 在 App 根部渲染一个实例(静态 API 依赖它)
export default function App() {
  return (
    <>
      <Root/>
      <Toast config={{
        success: (props) => <BaseToast {...props} style={{borderLeftColor: '#69C779'}} />,
      }}/>
    </>
  );
}

// ② 任意位置命令式弹出(不需要 ref)
Toast.show({
  type: 'success',                       // success | error | info(或用 config 自定义)
  text1: '保存成功',
  text2: '已同步到云端',
  position: 'top',                       // top | bottom
  visibilityTime: 4000,
  topOffset: 40,
  autoHide: true,
  onShow: () => console.log('shown'),
  onHide: () => console.log('hidden'),
  onPress: () => console.log('pressed'),
});

// ③ 手动关闭
Toast.hide();
# 换页启动测试页(force-stop 不能省,换页参数只在冷启动生效)
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey ToastMessageTestApp

几何/像素判据的复现方式

# ★ 尺寸常量在源码里:HEIGHT 60 / WIDTH 340(vp)⇒ × density
#    位置:显示态 translateY = topOffset(默认 40vp),容器全宽居中
# ⇒ 1320px 宽、density 3 时:盒子 1020×180px、x=150..1170、y=120..300

# ★ 像素判据:找【实际生效样式】的左边框色
#    success ⇒ #69C779(SuccessToast 覆盖);BaseToast 默认是 #D8D8D8
Add-Type -AssemblyName System.Drawing
$b = New-Object System.Drawing.Bitmap('shot.jpeg')
$bx = @(); for ($x=0; $x -lt $b.Width; $x++) {
  $p = $b.GetPixel($x, 200)
  if ([Math]::Abs($p.R-0x69) -lt 28 -and [Math]::Abs($p.G-0xC7) -lt 28 -and [Math]::Abs($p.B-0x79) -lt 28) { $bx += $x }
}
"左边框 x = $($bx[0])..$($bx[-1])(宽 $($bx.Count)px,期望 15px、起点 150)"

验证环境

项版本
React Native0.84.1
React19.2.3
RNOH(npm / ohpm)@react-native-oh/react-native-harmony / @rnoh/react-native-openharmony 0.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(页面留白取 48vp)
宿主 HAP 产物entry-default-signed.hap
本次构建assembleHap 3 分 38 秒(一轮就过,无重编)
验证规模设备侧 14 / 14 断言;几何复核 4 项(边框宽 15px、左边界 x=150、上边界 y≈124、竖直跨度 171 vs 180);回调/行为 3 项(onShow / onHide / autoHide 4s)+ hide 后残留像素 0;UI 安全区 1 项

相关社区与项目地址

项目地址
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

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐