指南
平台特定代码
同一个 nsh 包可以在 iOS、Android 和 Mac Catalyst 上运行。应用的大部分代码根本不需要判断平台:元素、样式、布局、组件和模块在各处的行为都相同,nsh/ui/mobile 的组件由 nsh 自己绘制而不是借用各平台的控件,所以两边看起来一样。当某样东西确实只属于一个平台时,nsh 给它安排了明确的位置,让平台代码一目了然、范围可控:
| 位置 | 用途 |
|---|---|
ios={{…}}、android={{…}}、desktop={{…}} | 只有一个平台才有的属性。 |
nsh/ios、nsh/android | 只有一个平台才有的 API。 |
Platform.OS、Platform.select | 按平台选择值或代码。 |
ios: / android: 类 | 按平台设置样式。 |
#平台专属属性
只存在于某一个平台的属性,归入以该平台命名的对象中。每个元素都接受 ios、android 和 desktop(macOS、Windows、Linux);在其他平台上这个对象会被忽略,在它自己的平台上只应用其中的键。
<View
onPress={open}
activeOpacity={0.6}
android={{ ripple: { color: "rgba(0,0,0,0.12)" }, elevation: 4 }}
style={{ padding: 16, borderRadius: 12, backgroundColor: "#ffffff" }}
>
<Text>Open</Text>
</View>
<Input
placeholder="Code"
ios={{ textContentType: "oneTimeCode" }}
/>
<Modal visible={open()} ios={{ presentationStyle: "pageSheet" }} android={{ statusBarTranslucent: true }}>
…
</Modal>
这些对象和其他属性一样是响应式的;从对象中移除的键会被清除。
目前已有的:
| 属性 | 适用于 | 说明 |
|---|---|---|
android={{ ripple }} | 所有元素 | 按下时的 Material 水波纹:{ color?, borderless?, radius?, foreground? }。 |
android={{ elevation }} | 所有元素 | 根据视图形状生成的 Material 阴影,单位为点。 |
android={{ accessibilityLiveRegion }} | 所有元素 | "none"、"polite" 或 "assertive":TalkBack 播报视图的变化。 |
android={{ statusBarTranslucent }} | Modal | 模态视图的内容延伸到状态栏下方。 |
android={{ backgroundColor }} | StatusBar | 状态栏颜色(Android 15 全面屏之前)。 |
ios={{ inputAccessoryViewID }} | <Input> | 显示在键盘上方的 InputAccessoryView。 |
ios={{ textContentType }} | <Input> | iOS 对自动填充类型的叫法。 |
ios={{ headerLargeTitle }}、ios={{ headerBackTitle }} | Screen | 大标题;返回按钮上的文字。 |
ios={{ presentationStyle }}、ios={{ supportedOrientations }} | Modal | "pageSheet" / "formSheet" 表单样式;允许的方向。 |
ios={{ allowsBackForwardNavigationGestures }} | WebView | 滑动在页面历史中前进后退。 |
desktop={{…}} 在所有地方都可以使用,为桌面专属属性预留;目前还没有。类型(IOSProps、AndroidProps、DesktopProps 及其各元素的形式)会告诉编辑器每个元素接受什么。
在两个平台上都存在、只是效果略有不同的属性不做分组:Alert.alert 的 cancelable 选项两个平台都有(iOS 提示框本来就不会因点击外部而关闭),pagingEnabled、refreshing、hitSlop 等在所有平台上都可用。
#平台模块
nsh/ios 和 nsh/android 存放只有一个平台才有的 API。两者在所有平台上都可以导入,因此同一个包可以到处运行:在另一个平台上,它们的函数什么都不做,组件什么都不渲染。每个模块都导出 isAvailable,只在自己的平台上为 true。
#nsh/android
| 导出 | 说明 |
|---|---|
Toast.show(message, duration?) | 在屏幕底部显示 toast;"short"(默认)或 "long"。 |
BackHandler.addEventListener("hardwareBackPress", listener) | 在模态视图和导航栈之后监听返回键;处理了就返回 true。返回 { remove() }。 |
BackHandler.exitApp() | 退出应用。 |
setStatusBarBackgroundColor(color, animated?) | StatusBar 组件所设置内容之下的状态栏颜色(全面屏之前,API 35)。 |
isAvailable | Android 上为 true。 |
import { cleanup } from "nsh";
import { BackHandler, Toast } from "nsh/android";
import { Platform } from "nsh/device";
import { Alert } from "nsh/ui/mobile";
function saved() {
if (Platform.OS === "android") Toast.show("Saved");
else Alert.alert("Saved");
}
function Player() {
// 返回键先退出全屏,再离开页面。
const sub = BackHandler.addEventListener("hardwareBackPress", () => {
if (!fullScreen()) return false;
fullScreen.set(false);
return true;
});
cleanup(() => sub.remove());
// …
}
返回键先由最上层的模态视图处理,然后是最上层的导航栈,再然后是 BackHandler 监听器(最近添加的最先),都没有处理时应用退出。见导航。
#nsh/ios
| 导出 | 说明 |
|---|---|
InputAccessoryView | 在 ios={{ inputAccessoryViewID }} 中引用其 nativeID 的输入框的键盘上方显示的工具栏。 |
isAvailable | iOS 和 Mac Catalyst 上为 true。 |
见表单与键盘。
#状态栏
状态栏在两个平台上都用 nsh/ui/mobile 中的 StatusBar 设置:barStyle("default"、"light-content"、"dark-content")、hidden 和 animated,Android 的颜色放在 android={{ backgroundColor }} 中。最近挂载的 StatusBar 按属性逐项生效,所以页面可以为自己渲染一个,页面消失后之前的设置会恢复。StatusBar.setBarStyle() 和 StatusBar.setHidden() 以命令式方式设置基础值。
import { View } from "nsh";
import { StatusBar } from "nsh/ui/mobile";
function PhotoViewer() {
return (
<View style={{ flex: 1, backgroundColor: "#000000" }}>
<StatusBar barStyle="light-content" animated />
…
</View>
);
}
在嵌入原生应用的场景下,iOS 上由宿主应用的视图控制器决定状态栏。在 nsh.json 中声明了 "capabilities" 的应用,需要 "statusBar": true 才能修改它。
#Platform.OS 与 Platform.select
nsh/device 中的 Platform 告诉你应用运行在哪里:Platform.OS 为 "ios"、"android"、"macos"、"windows" 或 "linux"(Mac Catalyst 为 "ios",同时 Platform.isMacCatalyst 为真)。Version、isPad 等见设备与环境。
import { Platform } from "nsh/device";
if (Platform.OS === "android") {
// …
}
#Platform.select
Platform.select(spec) 返回当前平台对应的值。它依次查找该操作系统自己的键,然后是 mobile(iOS 和 Android)或 desktop(macOS、Windows、Linux),再是 native(所有平台),最后是 default:
const styles = {
card: {
borderRadius: 12,
backgroundColor: "#ffffff",
...Platform.select({
ios: { shadowColor: "#000000", shadowOpacity: 0.12, shadowRadius: 8, shadowOffset: { width: 0, height: 2 } },
android: { borderWidth: 1, borderColor: "#e5e7eb" },
}),
},
};
const hint = Platform.select({ ios: "Swipe down to close", android: "Press back to close", default: "Close" });
没有匹配项且没有 default 时返回 undefined。
#渲染不同的组件
组件也可以这样选择;选择永远不变时放在模块顶层:
const Spinner = Platform.select({ ios: IosSpinner, default: MaterialSpinner })!;
或者用 <If> 内联:
<If when={Platform.OS === "ios"} else={<Text>Android has no input accessories.</Text>}>
<InputAccessoryView nativeID="bar">…</InputAccessoryView>
</If>
#Tailwind 中的平台变体
ios: 和 android: 变体只在一个平台上应用对应的类:
<View class="rounded-xl bg-white p-4 ios:shadow-md android:border android:border-zinc-200">…</View>
<Text class="text-base android:font-medium">Title</Text>
#优先使用共享的行为
在添加平台判断之前,先想想这个差异对用户是否重要。元素和样式已经在应当不同的地方映射到各平台的原生行为(滚动物理效果、文字渲染、键盘、无障碍),在用户期望一致的地方(你的应用自己的外观)保持一致。平台判断最好留给用户依赖的平台惯例:Android 的返回键和 toast、iOS 的表单弹窗和大标题、自动填充的名称、水波纹与不透明度反馈。
#相关内容
nsh/ios与nsh/android:参考。nsh/device:Platform。- 原生组件:使用宿主应用在各平台上提供的视图。
- Tailwind:所有变体。