元素

元素

宿主元素是构成每个界面的原语:<View>、<Text>、<Image> 等,从 nsh 的模块导入。每个元素都对应一个原生视图(iOS 和 Mac Catalyst 上是 UIView 子类,Android 上是 View),由 nsh 运行时创建并布局。应用显示的一切都由它们组成: nsh/ui/mobile 的组件同样是用这些元素绘制的。

import { mount, signal, Text, View } from "nsh";

function App() {
  const count = signal(0);
  return (
    <View style={{ flex: 1, alignItems: "center", justifyContent: "center", gap: 12 }}>
      <Text style={{ fontSize: 24 }}>已点击 {count()} 次</Text>
      <View onPress={() => count.update((n) => n + 1)} activeOpacity={0.6} class="rounded-lg bg-blue-600 px-4 py-2">
        <Text class="text-white">点我</Text>
      </View>
    </View>
  );
}

mount(App);

每个标签都使用 PascalCase,并指向作用域中的绑定:按下表从各自的模块导入每个元素。只有 <If> 和 <Each> 是无需导入的全局组件。<view> 这样的小写标签会导致构建错误,因为 TypeScript 只能对照每个名称一个的全局类型来检查它,List、Each 这类泛型组件就无法推断条目类型。编译器通过导入识别这些元素并就地创建原生视图,所以导入它们在运行时没有任何开销。参见 JSX 与组件。

#全部元素

标签导入自原生视图(iOS / Android)页面
<View>nshUIView / ViewGroupView
<Text>nshCoreText 视图 / StaticLayout 视图Text
<Image>nsh/image带图片图层的视图 / 图片视图Image
<Scroll>nshUIScrollView / ScrollViewScroll
<Input>nshUITextField 或 UITextView / EditTextInput
<List>nsh/list复用单元格的 UIScrollView / ScrollViewList
<Canvas>nsh/canvas回放 2D 绘制的视图Canvas
<WebView>nsh/webviewWKWebView / android.webkit.WebViewWebView
<Video>nsh/videoAVPlayerLayer / TextureView 上的 MediaPlayerVideo
<Modal>nsh/ui/mobile模态呈现的 view controller / DialogModal
<InputAccessoryView>nsh/ios键盘上方的工具栏(仅 iOS)nsh/ios
<If>、<Each>无:全局组件无:控制流控制流

#通用属性

所有宿主元素都接受下列属性(即 CommonProps 类型)。各元素页面只列出该元素自己的属性。

#样式

属性类型说明
styleStyleProp<ViewStyle>(<Text> 和 <Input> 为 TextStyle,<Image> 为 ImageStyle)内联样式,或从左到右合并的样式数组(假值项会被跳过)。优先级高于 class。参见样式参考。
classstringTailwind 类名,在原生侧解析("flex-row gap-2 p-4 dark:bg-zinc-900")。后面的类覆盖前面的类。参见 Tailwind。
classListRecord<string, boolean | null | undefined>值为真时才应用的类({ "opacity-50": busy() })。
disabledboolean忽略按压(也没有按压反馈),应用 disabled: 类,并禁用输入框等控件。

#按压与反馈

按压处理在原生侧完成:由平台识别按压并显示反馈,JS 只在事后收到通知。只有最内层可按压的视图会响应;在滚动视图内,按下会等待约 130 ms,让滚动有机会先开始。

属性类型默认值说明
onPress() => void在视图内完成一次点击。
onLongPress() => void手指按住达到 delayLongPress。
onPressIn() => void手指在视图上按下。
onPressOut() => void手指抬起或移出。
delayLongPressnumber500按压变为长按前的毫秒数。
activeOpacitynumber按下时视图的不透明度。
underlayColorColorValue按下时的背景色;子元素淡化到 underlayOpacity。
underlayOpacitynumber0.85显示 underlayColor 时子元素的不透明度。
hitSlopnumber | { top?, right?, bottom?, left? }把触摸区域扩展到边界之外,单位为点。
pointerEvents"auto" | "none" | "box-none" | "box-only""auto"视图及其子元素接收哪些触摸:"none" 都不接收,"box-none" 只有子元素接收,"box-only" 只有视图本身接收。

视图被按下时,active: Tailwind 类同样会生效,无需经过 JS。

#布局与标识

属性类型说明
onLayout(e: LayoutEvent) => void每次布局改变视图位置尺寸后,给出相对父视图的 frame { x, y, width, height }。
refYNode | (node: YNode) => void原生节点(参见 ref 方法)。ref={el} 赋值给变量,ref={fn} 调用函数。
nativeIDstring为视图命名,供其他元素引用,例如输入框引用的 InputAccessoryView。
testIDstringUI 测试使用的标识(在 Android 上是视图的 resource id)。

#驱动原生值的手势

这些属性把手势绑定到原生动画值,值在 UI 线程上变化,每帧都不经过 JS。参见触摸与手势。

属性类型说明
pan{ x?: NativeValue; y?: NativeValue }拖动驱动这些值(单位为点)。
onPanStart(e: PanEvent) => void拖动开始。
onPanEnd(e: PanEvent) => void拖动结束,带有松手时的速度(例如用来启动 decay)。
pinch{ scale?: NativeValue }双指捏合驱动 scale。
onPinchStart / onPinchEnd(e: { scale: number; velocity: number }) => void捏合开始 / 结束。
rotation{ angle?: NativeValue }双指旋转驱动 angle(角度)。
onRotationStart / onRotationEnd(e: { rotation: number; velocity: number }) => void旋转开始 / 结束。

PanEvent 为 { translationX, translationY, velocityX, velocityY, valueX?, valueY? }:自开始以来的位移、以点每秒计的速度,以及此刻所绑定值的数值(拖动可能在动画中途抓住一个值)。

#原始触摸与响应者系统

对于上面属性覆盖不到的手势,每个元素都支持触摸与响应者属性: onTouchStart、onTouchMove、onTouchEnd、onTouchCancel(以及对应的 …Capture 形式)、 onStartShouldSetResponder、onMoveShouldSetResponder(以及 …Capture)、onResponderGrant、onResponderReject、 onResponderStart、onResponderMove、onResponderEnd、onResponderRelease、onResponderTerminate 和 onResponderTerminationRequest。每个处理函数都会收到一个 GestureResponderEvent ({ identifier, locationX, locationY, pageX, pageY, target, timestamp, touches, changedTouches, nativeEvent, stopPropagation() })。onResponderGrant 返回 true 可阻止原生滚动视图接管触摸。 nsh/gesture 中的 PanResponder 就构建在它们之上。

#无障碍

属性类型说明
accessibleboolean视图对辅助技术而言是一个整体元素,子元素不再单独暴露。
accessibilityLabelstring屏幕阅读器朗读的内容。没有标签的视图会朗读其中的文本。
accessibilityHintstring激活该元素会发生什么。
accessibilityRole / roleAccessibilityRole元素是什么("button"、"header"、"link"、"switch" 等)。
accessibilityState{ disabled?, selected?, checked?, busy?, expanded? }辅助技术报告的状态(checked 可以是 "mixed")。
accessibilityValue{ min?, max?, now?, text? }范围类或文本类元素的值。
accessibilityElementsHiddenboolean对辅助技术隐藏该视图及其子树。
importantForAccessibility"auto" | "yes" | "no" | "no-hide-descendants"是否暴露该视图。
accessibilityActions{ name: string; label?: string }[]提供给辅助技术的自定义操作。
onAccessibilityAction(e: { actionName: string }) => void用户触发了 accessibilityActions 中的某个操作。

参见无障碍。

#平台专属属性

只在某个平台上存在的属性放在以该平台命名的对象里,这样共享属性在各平台上保持一致。只有当前平台的对象会被发送,其他的会被忽略。

属性说明
ios={{…}}iOS 和 Mac Catalyst。<Input>、Modal 和 WebView 有各自的键。
android={{…}}Android。所有元素都支持 ripple、elevation 和 accessibilityLiveRegion(见下表)。
desktop={{…}}桌面平台;在移动端被忽略。
android 键类型说明
ripple{ color?, borderless?, radius?, foreground? }按下时的 Material 水波纹。
elevationnumber根据视图形状生成的 Material 阴影(单位为点)。
accessibilityLiveRegion"none" | "polite" | "assertive"视图变化如何播报给 TalkBack。
<View onPress={save} android={{ ripple: { color: "#00000022" }, elevation: 2 }} class="rounded-lg bg-white p-4">
  <Text>保存</Text>
</View>

参见平台相关代码。

#ref 方法

ref 收到的是一个 YNode。它的方法向原生视图发送命令,与同一轮中先前的改动按顺序执行。各元素页面会说明哪些方法适用。

方法适用于说明
focus() / blur()<Input>获取焦点(弹出键盘)/ 失去焦点。
setSelection(start, end?)<Input>选中 start..end(UTF-16 单位);省略 end 时为光标。
scrollTo({ x?, y?, animated? })<Scroll>、List滚动到某个偏移量。
zoomTo({ scale, x?, y?, animated? })可缩放的 <Scroll>围绕内容中的某点缩放到 scale。
scrollToOffset({ offset, animated? })List沿列表方向滚动到某个偏移量。
scrollToIndex({ index, animated?, viewPosition?, viewOffset? })List把某一项滚动到可见区域。
scrollToEnd({ animated? })List滚动到内容末尾。
reload()、goBack()、goForward()、stopLoading()、injectJavaScript(script)、postMessage(data)WebView控制页面。
seek(seconds)Video跳转到某个时间点。
command(name, args?)原生组件调用已注册原生组件的命令(nsh/native)。
import { Input, View, type YNode } from "nsh";

let input!: YNode;
<View onPress={() => input.focus()}>
  <Input ref={input} placeholder="点击这一行开始输入" />
</View>;

#为什么处理函数不会阻塞 UI

主线程从不等待 JS。当系统需要立即得到答复时(这段文本能否修改、返回键能否返回上一页、这个 sheet 能否关闭),nsh 会根据事先设置好的属性(maxLength、preventBack、dismissible、pointerEvents 等)在原生侧作答,事件随后才送达 JS。正因如此,这类行为都是属性,而不是处理函数的返回值。