一套模型六种原生:Native Shell 全平台 UI 框架

一套模型,六种原生:Native Shell 如何让一套界面跑遍六个平台
Rust 优先、TSX 并行、双线程引擎,以及发布之后还能更新的界面。
如果一套界面代码,能在 iPhone、安卓、Mac、Windows、Linux 和浏览器上跑,而且每个平台看起来、用起来都像它自己的应用——这就是 Native Shell 想做的事。
它不是把网页塞进一个外壳里。它把同一棵界面树,挂到每个平台自己的视图上:iOS 用 UIKit,安卓用原生 View,桌面上是 AppKit、Win32 和 GTK,Web 输出的是真正的 HTML。
Native Shell(NSH)由 Longbridge 开发,Apache-2.0 开源,属于 GPUI Kit 生态。它的目标不是“跨平台能跑”,而是“跨平台之后,仍然是那个平台的应用”。
图 1:同一个搜索界面,放在 iOS、Android、macOS、Windows、Linux、Web 六个平台外壳里。
一分钟版本
- 一套应用模型:组件、signal、样式和 API 不变,覆盖 iOS、Android、macOS、Windows、Linux、Web。
- 原生有两层:平台自己的视图、文本、输入法、无障碍、滚动;以及平台自己的交互方式(菜单从哪里弹出、右键还是长按)。
- Rust First + TSX:Rust 编译成原生代码;纯 Rust 应用的运行器不链接 JS 引擎。TSX 走 QuickJS,用于快速界面、插件和运行时下发。
- 双线程:引擎线程跑应用逻辑与响应式图,主线程只画文字、跑原生动画、处理手势;主线程从不等待 JS。
- 发布后还能更新:签名
.nshpkg包、QuickJS 沙箱、能力由宿主授予;一个nsh命令覆盖创建、构建、测试、打包。
01 为什么“跨平台”之后还像原生?
大多数跨平台方案先解决“能不能跑”,再解决“像不像”。NSH 反过来:它先决定每个平台用平台自己的东西,再让应用模型去适配。
所以你会看到一些很具体的差异:
Menu在桌面从触发点弹出,在手机上从底部升起。ContextMenu在桌面用右键,在手机上用长按。Tooltip在桌面悬停出现,在手机由应用打开时出现。- 文本选择、输入法、无障碍、滚动,全部交给系统。
NSH 提供原生视图与交互,但不规定视觉。颜色、字体、间距、形状、状态反馈和动画,由应用自己决定。这也是它和“把网页控件套一层皮”最大的区别:不是外观像,而是行为真的落在平台上。
图 2:剖开窗口,同一棵界面树落到 UIKit、ViewGroup、NSView、Direct2D、GTK、HTML 六种平台视图上。
02 原生不是一种皮肤,而是两层东西
第一层是平台基础设施。iOS 是 UIKit、CoreText、系统文本框;Android 是 Android 视图、StaticLayout、EditText;macOS 是 AppKit、CoreText、NSTextField;Windows 是 Direct2D 绘制的 Win32 窗口、DirectWrite、IMM32 输入法;Linux 是 GTK 4、Pango;Web 是 HTML、CSS 和浏览器输入框。
第二层是平台交互方式。同样是“更多操作”,桌面用户期待右键菜单,手机用户期待长按;同样是“选择”,桌面用户期待浮层,手机用户期待底部面板。NSH 把这些差异放在组件行为里,而不是让每个应用自己判断。
这也是为什么它的基础组件 nsh/base 是无样式的:它负责行为、焦点、响应式状态、无障碍和适应设备的呈现方式;视觉默认值属于应用或第三方组件库。这个分层来自 GPUI Kit 的实践:Longbridge Pro 的定制压力导致 fork 组件,于是把共享能力提取到 gpui-base,再由 nsh/base 承接。
03 Rust 和 TSX,一个引擎
NSH 的第一语言是 Rust。界面、逻辑和组件都可以用 Rust 写,编译成原生代码;纯 Rust 应用的运行器在任何平台上都不链接 JavaScript 引擎。
需要快速迭代、插件,或者运行时下发的界面,就用 TSX。TSX 走 QuickJS,编译成字节码;它和 Rust 共用同一套基础元素、基础组件、signal 和布局。两种路径也可以结合:Rust 应用承载 TSX 页面时会链接 QuickJS,TSX 应用则使用预编译运行时。
let count = signal(0);
view().flex_1().items_center().justify_center()
.child(text(move || count.get().to_string()))
.on_press(move || count.update(|n| *n += 1))
function App() {
const count = signal(0);
return (
<View class="flex-1 items-center justify-center gap-4">
<Text class="text-3xl">{count()}</Text>
<View onPress={() => count.update((c) => c + 1)} role="button">
<Text>Tap</Text>
</View>
</View>
);
}
mount(App);
两种写法看起来不同,运行模型是一样的:组件只运行一次,变化时只重跑读到该 signal 的表达式。没有组件重渲染,也没有虚拟 DOM diff。
这套模型是 Retained Mode:每个元素都是平台保留的视图。每一轮的视图操作作为一批交给主线程;动画在平台的帧时钟上计算,不经过 JS 逐帧调用。
04 主线程从不等待 JS
NSH 的每个应用实例有两个线程。
引擎线程跑应用代码、响应式图和渲染器。响应式图遵循 Solid 1.9 算法;渲染器维护一棵影子树,在每一轮结束时一次性 commit:渲染 Rust 组件、处理 env() 边距、重建文本、用 Taffy 做 flexbox/grid 布局、对 frame 和样式做 diff,最后生成一个 MountBatch。
主线程只做必须在那里做的事:绘制文字、跑原生动画(iOS 用 CADisplayLink,Android 用 UI 线程动画器)、处理手势和滚动。
关键原则是:主线程从不等待 JS。需要即时答复的问题,答案已经作为 prop 挂载在 Native 侧;事件随后到达引擎线程。
一次点击到屏幕更新,官方架构页给出的 Release 构建数字是约 0.12 ms。文本在引擎线程用平台文本引擎布局(CoreText、StaticLayout、DirectWrite、Pango 或浏览器度量),主线程只负责绘制;List 在原生侧虚拟化,没有新行的帧完全不调用 JS。
性能方面,官方文档给出的数字是:在 ProMotion 屏幕上,滚动和动画稳定在 120 FPS;500 行行情表每秒更新 1,000 次,Linux GTK 4 后端下 NSH Rust 占单核 18%,同场景 GPUI Fast 为 20%;10 万行排序约 3 ms。这些数字都有明确的前置条件,不是所有设备、所有界面的保证。
图 3:引擎线程只把变化打包成 MountBatch,一次跳转交给主线程;0.12 ms 是 Release 构建下的点击到屏幕。
05 发布之后,界面还能继续长
NSH 的另一个重点是“发布后还能更新”。
它用签名的 .nshpkg 包下发新界面。宿主先校验签名和吊销列表,再把模块放进每个模块独立的 QuickJS 虚拟机;内存、调用时长、节点数量都有上限,能访问哪些网络和系统能力,由宿主授予。模块可以使用宿主注册的 Native 视图,但信任和权限始终在宿主手里。
AI 生成的界面也走同一条路径:按声明好的 API 写 TSX、跑测试、签名,再按宿主策略在沙箱里验证。NSH 会从 Rust 源码生成 TypeScript 声明(.nsh/types),提供 llms.txt,并让 nsh test 在没有设备、没有界面的情况下运行真实运行时。
这也是它和“热更新”最大的区别:不是把新代码直接塞进应用,而是让新界面以一个可签名、可吊销、可限制的模块运行。
图 4:签名包通过校验后进入 QuickJS 沙箱;能力槽由宿主授予,内存、调用时长、节点数都有上限。
06 它适合谁,不适合谁
先看一张简化的对比:
| Native Shell | React Native | Flutter | Electron / Tauri | |
|---|---|---|---|---|
| 由什么绘制 | 平台界面 | 平台视图 | 自绘引擎 | 网页内容 |
| 应用语言 | Rust + TSX | JS / TS | Dart | JS / TS(Tauri 另有 Rust) |
| Web 输出 | HTML | 社区方案 | Canvas | 网页 |
| 纯 Rust 应用无 JS runtime | 支持 | 不支持 | 支持 | 不支持 |
| 签名下发界面 | 支持 | 部分支持 | 部分支持 | 不支持 |
官方文档的原话是:NSH 是唯一把以下几点同时做到的——每个平台自己的界面与文本输入;一套应用覆盖手机、桌面和 Web,而且 Web 输出是 HTML;Rust 应用不带 JavaScript runtime;发布之后还能下发经过签名、运行在沙箱中的界面。
反过来说,它并不适合所有场景:
- 需要各平台像素级一致:Flutter、Slint 更合适。
- 需要庞大的第三方包生态:React Native、Flutter、Qt 更合适。
- 界面有数千个视图持续密集更新(例如每秒数万条行情的终端):GPUI Fast 更合适。
- 只是展示文档或现成网页:WebView 更合适。
- 页面固定、只变内容和顺序:服务端驱动 UI 更合适。
平台要求:iOS 15+、Android 9+、macOS 14+、Windows 10 1809+、Linux GTK 4.14+、现代浏览器。代码 Apache-2.0,文档 CC BY 4.0。
图 5:AI 生成界面也走“写 TSX → 跑测试 → 签名 → 沙箱验证”,整套流程只有一个 nsh 命令。
07 上手只要一行命令
macOS / Linux:
curl -fsSL https://nsh.dev/install.sh | sh
Windows PowerShell:
irm https://nsh.dev/install.ps1 | iex
也可以用 Homebrew、Scoop 或 mise。装完后 PATH 上是同一个 nsh 命令。
nsh init my-app --template ts
nsh run my-app --target ios
nsh run my-app --target android
nsh run my-app --target web
nsh package my-app --target macos
TSX 应用只需要 nsh;Rust 应用还需要 Rust 工具链。除此之外,每个平台还需要自己的工具:iOS 要 Xcode,Android 要 SDK/JDK,Linux 要 GTK 4.14+,Windows 要 VS Build Tools、Windows SDK 和 LLVM。
08 视频与章节
这篇图文对应一支 2 分 48 秒的介绍片:
00:00一套模型,六种原生00:55同一个引擎:Rust + TSX、双线程与 0.12ms01:52发布之后:签名包、QuickJS 沙箱与一行安装
视频:[B站链接] · [公众号链接]
资料来自 nsh.dev 官方中文文档 与 Longbridge/nsh。画面是按官方视觉语言用代码重绘的示意 UI,不是产品截图;本文非官方出品。
AI 声明:口播为 AI 合成(Edge TTS 云希),稿件由 AI 起草、本人确认;BGM 为 AI 合成;画面由代码生成;本文由 AI 协助整理,事实已对照官方文档。
图 6:一套模型,六种原生。