For AI agents: the complete documentation index is at llms.txt. Markdown versions of any page are available by appending .md to the URL path.

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

Native ShellRust跨平台UI框架GPUI

一套模型,六种原生: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 在没有设备、没有界面的情况下运行真实运行时。

这也是它和“热更新”最大的区别:不是把新代码直接塞进应用,而是让新界面以一个可签名、可吊销、可限制的模块运行。

签名包进入 QuickJS 沙箱 图 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。

AI 流水线与一个 nsh 命令 图 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.12ms
  • 01:52 发布之后:签名包、QuickJS 沙箱与一行安装

视频:[B站链接] · [公众号链接]

资料来自 nsh.dev 官方中文文档 与 Longbridge/nsh。画面是按官方视觉语言用代码重绘的示意 UI,不是产品截图;本文非官方出品。

AI 声明:口播为 AI 合成(Edge TTS 云希),稿件由 AI 起草、本人确认;BGM 为 AI 合成;画面由代码生成;本文由 AI 协助整理,事实已对照官方文档。

落版 图 6:一套模型,六种原生。