文档

用 Rust 开发

Native Shell 应用可以用 TSX 编写,也可以用 Rust,或者两者结合。三种方式运行在同一个引擎上:响应式图、渲染器、Taffy 布局和 nsh/ui/mobile 组件始终是 Rust,各平台都用自己的原生 UI 渲染。不同的是你自己的代码用什么语言,以及应用是否带着 JavaScript 虚拟机。

TSXRustRust + TSX
你的 UI、状态和事件处理TSX,运行在 QuickJS 上RustRust 组件和模块,TSX 页面使用它们
应用运行的内容QuickJS 和引擎只有引擎和你的 crate,所有平台和浏览器都一样QuickJS 和引擎,加上你的 crate
适合运行时下发的应用和模块、快速迭代业务复杂度高的中大型应用Rust 核心加上经常变化的部分
修改代码热更新重新构建并重启TSX 热更新,Rust 重新构建并重启
创建nsh init my-appnsh init my-app --rustnsh init my-app --rust,然后 nsh run App.tsx

#如何选择

Rust 适合业务逻辑庞大、要求高的应用:交易、编辑器、数据密集的工具。整个应用都有 Rust 的类型系统和编译期检查,可以使用 crates.io 生态,你自己的代码(不只是 NSH 的代码)也是原生性能,还能用 cargo test 对真实 UI 做无界面测试。应用从头到尾都是 Rust 和原生代码:在 iOS、Android、macOS、Windows、Linux、GPUI 和 Web 上,应用里既不带也不运行 JavaScript 虚拟机。 组件是只运行一次的函数,把信号绑定到显示的内容上,和 TSX 一样(响应式),样式用类型化的方法而不是 class 字符串。

Rust + TSX 保留这个核心,在值得的地方加上 TSX:页面、设置、引导、实验性功能,任何需要热更新快速迭代、或作为签名包 在运行时下发的部分。TSX 像导入其他 NSH 模块一样导入你的 Rust 组件和模块,两部分通过类型化的接口衔接。TSX 运行在 QuickJS 上,这时应用会带上 QuickJS。

#完全用 Rust 编写的应用

nsh init my-app --rust
cd my-app
nsh run                        # 本机的后端;--target ios、android、web、macos、linux、windows 或 gpui
nsh package --target web       # 或 ios、ios-simulator、android、macos、linux、windows、gpui

src/lib.rs 包含整个应用:组件、状态、事件处理和任务。

use nsh_core::ui::prelude::*;
use nsh_ui_mobile::{Button, ButtonEvent, ButtonProps};

/// TSX 通过 `app/native` 可以使用的组件。
#[native_module("app/native")]
fn native(registry: &mut Registry) {
    registry.register::<App>();
}

#[component]
pub fn App() -> impl IntoElement {
    let count = signal(0_u32);
    let quote = signal(SharedString::from("…"));
    // 异步工作在任务里运行;请求走 NSH 的网络栈。
    spawn(async move {
        if let Ok(response) = fetch("https://api.example.com/quote").await {
            quote.set(response.text().into());
        }
    });
    view()
        .flex_1()
        .items_center()
        .justify_center()
        .gap_4()
        .child(text(move || format!("Count: {}", count.get())))
        .child(text(quote))
        .child(
            Button::element(ButtonProps { title: "Tap".into(), ..ButtonProps::default() })
                .on_event(move |_: &ButtonEvent| count.update(|c| *c += 1)),
        )
}

nsh.json 指定根组件,nsh 负责挂载它:

{
  "name": "My App",
  "rust": ".",
  "root": "App",
  "theme": "theme.json",
  "appearance": "system"
}

"root" 是组件在 crate 中的路径(App、shell::Gallery;有多个 crate 时在第一个中);"theme" 和 "appearance" 和 TSX 应用一样作用于它。 有了它,对应用目录执行 nsh run 和 nsh package 时,会为目标平台构建应用自己的运行器,直接挂载根组件,不链接 QuickJS:

目标运行的内容
macOS、Windows、Linux、GPUI带有你的 crate 的发布运行器,在窗口中运行;nsh run 使用 debug 构建,并在终端显示应用日志
iOS、iOS 模拟器、Mac Catalyst带有你的 crate 的 iOS 运行器
Android带有你的 crate 的应用 libnsh.so,由运行时的 Activity 启动
Web包含引擎和你的 crate 的 nsh.wasm(整个 Rust gallery 为 3.3 MB,brotli 压缩后 0.75 MB);页面直接挂载根组件

nsh run 期间,修改 crate 的 Rust 代码会重新构建运行器并重启应用;在浏览器中,已打开的页面会自动加载新的运行时。 cargo run 在桌面后端上直接启动同一个根组件(src/main.rs);嵌入 NSH 的原生应用在 macOS、Windows、Linux、GPUI 和 iOS 的 AppView 上用 AppSource::Rust 挂载 Rust 根组件。

要在包括 Web 在内的所有平台上运行的代码,使用 NSH 提供的服务:HTTP 用 fetch(网络), 等待用 timer、set_timeout 和 set_interval,时钟用 nsh_core::time::Instant 和 nsh_core::time::unix_ms()。 background 和 blocking 在原生目标的线程上运行任务。

#用 TSX 扩展 Rust 应用

crate 的 #[native_module("app/native")] 函数声明 TSX 可以导入的内容:组件(registry.register::<Chart>())、函数模块 (registry.module::<Portfolio>(),一个 #[module] impl)和资源。它们的 TypeScript 声明由 Rust 类型生成到 .nsh/types,所以 nsh check 和编辑器会据此检查 TSX:

import { mount, signal, View, Text } from "nsh";
import { Chart, Portfolio } from "app/native";   // Rust

function Overview() {
  const total = signal(0);
  Portfolio.total().then((value) => total.set(value));   // Rust 函数:在 TSX 中是 Promise
  return (
    <View className="p-4 gap-2">
      <Text>Total: {total()}</Text>
      <Chart symbol="NSH" range="1M" onSelect={(e) => console.log(e.price)} />
    </View>
  );
}

mount(() => <Overview />);

props 是组件 Props 结构体的字段,事件是 Event 枚举的变体(onSelect),都用驼峰命名。Rust 组件自己的内容、事件和状态留在 Rust 中;TSX 传入 props、接收事件。指定 TSX 入口来运行它:nsh run App.tsx 或 nsh package App.tsx --target …。TSX 的修改是热更新, Rust 的修改会重新构建并重启应用。

仓库中的 examples/gallery-rs 是一个完整的 Rust 应用,两种入口都有:每个 demo 都是 Rust,nsh run examples/gallery-rs --target web (或 ios、android 等)运行它的根组件,cargo run -p gallery-native 在桌面后端上启动它,nsh run examples/gallery-rs/App.tsx 通过 TSX 入口运行它。

#多个 crate

应用的 Rust 代码可以拆分成多个 crate,每个 crate 用自己的名称导入:"rust" 可以是 crate 路径的列表,列表中的每个 crate 都有自己的 #[native_module] 函数。

{
  "rust": ["native/charts", "native/storage"],
  "root": "App"
}
import { Chart } from "app/charts";        // native/charts:#[native_module("app/charts")]
import { Store } from "app/storage";       // native/storage:#[native_module("app/storage")]

每个运行器和宿主都链接全部这些 crate,.nsh/types 为每个名称生成声明。crate 之间通过 Cargo 路径依赖共享代码 (shared = { path = "../shared" }):一个公司组件 crate 可以被多个 crate 使用、被多个应用列出,只有 "rust" 中列出的 crate 会导出给 TSX。列出的每个 crate 都有自己的包名和导入名称;重复时 nsh 会报告共用它的两个路径。Rust 应用的 "root" 是列出的第一个 crate 中的组件。nsh run 和 nsh dev 期间,修改其中任何一个 crate,或它们通过路径依赖的 crate,都会重新构建并重启应用。

#另请参阅

  • 项目结构:nsh.json 的每个键。
  • Web:浏览器中的 Rust 应用。
  • 网络:在 Rust 中使用 fetch。
  • CLI:nsh init --rust。