Документация
Начало работы
Это руководство создаёт приложение NSH, запускает его на нужных вам платформах и выпускает. Приложения NSH пишутся на Rust или TSX на одном и том же движке: Rust компилирует приложение в нативный код без виртуальной машины JavaScript, а TSX работает на QuickJS с горячими обновлениями, как модули, которые нативное приложение может загружать во время выполнения. Сначала руководство разбирает приложение на Rust, затем то же приложение на TSX. Предполагается, что nsh установлен и находится в вашем PATH (Установка); в checkout используйте вместо него ./target/debug/nsh.
import { mount, signal, Text, View } from "nsh";
import { button, buttonText, desktop, focusRing, hoverFill, ui } from "./palette";
// The app `nsh init` creates, with a reset button and a short history.
function App() {
const count = signal(0);
const history = signal<number[]>([]);
const change = (delta: number) => {
const next = count() + delta;
count.set(next);
history.update((h) => [next, ...h].slice(0, 4));
};
return (
<View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, alignItems: "center", justifyContent: "center", gap: 16 }}>
<Text style={{ fontSize: 15, color: ui().muted }}>{desktop ? "You clicked" : "You tapped"}</Text>
<Text style={{ fontSize: 56, fontWeight: "700", color: count() < 0 ? ui().down : ui().text }}>{count()}</Text>
<View style={{ flexDirection: "row", gap: 12 }}>
<View onPress={() => change(-1)} role="button" {...button()}>
<Text style={buttonText()}>−1</Text>
</View>
<View onPress={() => change(1)} role="button" {...button("accent")}>
<Text style={buttonText("accent")}>{desktop ? "+1" : "Tap"}</Text>
</View>
</View>
<If when={history().length > 0}>
<Text style={{ fontSize: 13, color: ui().muted }}>Recent: {history().join(", ")}</Text>
</If>
<View onPress={() => (count.set(0), history.set([]))} hitSlop={8} hoverStyle={hoverFill()} focusStyle={focusRing()} activeStyle={{ opacity: 0.7 }}>
<Text style={{ fontSize: 15, color: ui().accent }}>Reset</Text>
</View>
</View>
);
}
mount(App);#Создание приложения
В терминале nsh init спрашивает, на чём написано приложение: rust, ts или mixed (TSX с собственным крейтом Rust). --template выбирает без вопроса.
#На Rust
nsh init my-app --template rust
nsh init создаёт каталог и записывает проект Cargo:
| Файл | Что это |
|---|---|
src/lib.rs | Приложение: его корневой компонент, состояние и обработчики |
src/main.rs | Монтирует тот же корневой компонент на macOS, Windows или Linux для cargo run |
Cargo.toml | Крейт, зависящий от nsh-core и nsh-base версии CLI |
nsh.json | Имя, id, версия и номер сборки приложения, его крейт ("rust": ".") и корневой компонент ("root": "App") (Структура проекта) |
App.test.tsx | Тест корневого компонента на TypeScript (nsh test) с tsconfig.json и .nsh/types для вашего редактора |
README.md | Как запускать, тестировать и упаковывать приложение и как позже добавить точку входа TSX |
.gitignore | Игнорирует .nsh/ и target/ |
Для разработки на Rust нужен набор инструментов Rust (rustup); команды nsh собирают раннеры приложения из крейтов NSH на crates.io версии CLI.
#На TSX
nsh init my-app --template ts
Приложение TSX состоит из одного или нескольких файлов .tsx с точкой входа: её запускает nsh run, и её же загружает как модуль приложение-хост.
| Файл | Что это |
|---|---|
App.tsx | Модуль точки входа: отображает приложение с помощью mount |
App.test.tsx | Его тест (nsh test) |
nsh.json | Имя, id, версия и номер сборки приложения (Структура проекта) |
tsconfig.json | Расширяет .nsh/tsconfig.json, настройки TypeScript для NSH, которые разрешают nsh в объявления из .nsh/types |
.gitignore | Игнорирует .nsh/ и target/ |
.nsh/types/ | Сгенерированные объявления всех модулей NSH для вашего редактора |
Уже существующие файлы сохраняются, поэтому команду можно запускать в каталоге, где часть из них уже есть; приложение на Rust создаётся в новом каталоге.
В обоих случаях отображаемое имя по умолчанию совпадает с именем каталога; чтобы выбрать другое, передайте --name "My App". Id становится com.example.<name>: измените его в nsh.json до выпуска. nsh init my-app --template mixed создаёт приложение TSX, которое одновременно является крейтом Rust (Cargo.toml и src/lib.rs рядом с App.tsx), чьи компонент, модуль и класс импортирует App.tsx (Сборка с Rust).
#Корневой компонент
Оба шаблона создают одно и то же приложение, счётчик:
// src/lib.rs
use nsh_base::theme;
use nsh_core::prelude::*;
/// The crate's UI set: what a TSX entry may import from `my-app/native`.
#[native_module("my-app/native")]
fn native(registry: &mut Registry) {
registry.register::<App>();
}
/// The application's root component.
#[component]
pub fn App() -> impl IntoElement {
let count = signal(0_u32);
view()
.flex_1()
.bg(move || theme().background)
.text_color(move || theme().foreground)
.child(
view()
.flex_1()
.p_safe_area()
.child(
view()
.flex_1()
.items_center()
.justify_center()
.gap_4()
.child(text(move || format!("Count: {}", count.get())).text_size(px(28.0)))
.child(
view()
.px_5()
.py_3()
.rounded(px(10.0))
.bg(move || theme().primary)
.active(|s| s.opacity(0.6))
.on_press(move || count.update(|value| *value += 1))
.child(
text("Tap")
.text_size(px(17.0))
.text_color(move || theme().primary_foreground),
),
),
),
)
}// App.tsx
import { mount, signal, Text, View } from "nsh";
function App() {
const count = signal(0);
return (
<View class="flex-1 items-center justify-center gap-4 bg-background pt-[env(safe-area-inset-top)] pb-[env(safe-area-inset-bottom)]">
<Text class="text-3xl font-semibold text-foreground">{count()}</Text>
<View
onPress={() => count.update((c) => c + 1)}
role="button"
class="rounded-(--radius) bg-primary px-5 py-3 active:bg-primary/90"
>
<Text class="text-lg text-primary-foreground">Tap</Text>
</View>
</View>
);
}
mount(App);На Rust:
use nsh_core::prelude::*приносит примитивы (view(),text(),scroll(),input(),image(), …), сигналы и типизированные методы стилей (.p_safe_area()добавляет к представлению отступы по безопасной области);nsh_baseсодержит базовые компоненты без стилей (button(), клавиатура, диалоги) иtheme(), цвета и радиусы, которыми рисуются компоненты приложения (Тема).#[component] pub fn App()— компонент: функция, которая выполняется один раз и возвращает свои элементы."root": "App"изnsh.jsonназывает его, аnsh runмонтирует.signal(0_u32)создаёт значение, меняющееся со временем.text(move || format!(…, count.get()))привязывает текст к сигналу: когдаcountменяется, NSH снова выполняет это замыкание и обновляет только этот текст, ничего больше. Стили принимают замыкания так же:.bg(move || theme().primary)следует за темой в тёмный режим..on_press(..)заставляет представление отвечать на нажатия;.active(|s| s.opacity(0.6))— его стиль во время нажатия, применяемый нативно. Кнопка принадлежит самому приложению: NSH предоставляет примитивы и возможности, а приложение рисует из них свой интерфейс..px_5(),.rounded(px(10.0)),.gap_4()— типизированные методы стилей, названные так же, как утилиты Tailwind, в пунктах (px). Раскладка основана на flexbox, и дочерние элементы по умолчанию располагаются в колонку (Раскладка).#[native_module("my-app/native")]называет то, что точка входа TSX может импортировать из крейта, когда вы добавляете сверху экраны на TSX (Сборка с Rust).
На TSX:
import { mount, signal, Text, View } from "nsh":nsh— основной модуль: сигналы,mount, другие реактивные примитивы и хост-элементы. Его предоставляет нативная среда выполнения, он не входит в сборку.nsh/baseсодержит базовые компоненты, какnsh_baseв Rust.function App()— компонент, который выполняется один раз.{count()}читает сигнал внутри JSX: компилятор видит вызов и заставляет этот текст следовать за сигналом.<View onPress>— нативное представление, реагирующее на нажатия; классыactive:— его стиль во время нажатия.View,Text,Image,Scrollи другие хост-элементы импортируются изnsh(Inputизnsh/base); толькоIfиEachне требуют импорта.classпринимает классы Tailwind;style={{ … }}принимает объект со стилевыми свойствами в camelCase, числа — в пунктах.bg-background,bg-primary,text-primary-foregroundиrounded-(--radius)называют токены темы приложения (Токены темы): они сами следуют за тёмным режимом, а"theme"вnsh.jsonменяет их стиль.mount(App)отображает компонент на поверхность приложения.
#Разработка
#Запуск
nsh run собирает приложение, запускает его и пересобирает при редактировании; журналы приложения выводятся в терминал. Передайте ему каталог приложения: он запускает корневой компонент приложения на Rust или App.tsx приложения на TSX (nsh run my-app/App.tsx указывает другую точку входа).
Для приложения на Rust nsh run собирает собственный раннер приложения для целевой платформы с вашим крейтом и смонтированным корневым компонентом, без QuickJS, и запускает его; изменение кода Rust приводит к новой сборке и перезапуску приложения. cargo run в каталоге приложения запускает тот же корневой компонент сразу на десктопном бэкенде. Для приложения на TSX nsh run запускает сервер разработки и хост для разработки, который загружает приложение с него, и обновляет запущенное приложение при каждом сохранении.
Без --target команда nsh run использует машину, на которой вы работаете: окно AppKit на macOS, окно Win32 на Windows и окно GTK на Linux. Другую платформу выбирают с помощью --target:
| Цель | Команда | Где запускается |
|---|---|---|
| Симулятор iOS | nsh run my-app --target ios | macOS с Xcode |
| Android | nsh run my-app --target android | macOS, Windows, Linux |
| macOS (AppKit) | nsh run my-app | macOS (по умолчанию) |
| Windows | nsh run my-app | Windows (по умолчанию) |
| Linux (GTK 4) | nsh run my-app | Linux (по умолчанию) |
| Web | nsh run my-app --target web | macOS, Windows, Linux |
| Mac Catalyst | nsh run my-app --target catalyst | macOS |
#Симулятор iOS
nsh run my-app --target ios
nsh run my-app --target ios --device "iPhone 16 Pro"
nsh использует запущенный симулятор или запускает новейший iPhone, если ни один не работает, и устанавливает в него приложение (раннер приложения на Rust или хост для разработки). --device выбирает симулятор по имени или UDID.
#Android
nsh run my-app --target android
Сначала запустите эмулятор или подключите устройство (adb devices выводит их список; NSH использует первое). nsh устанавливает на него приложение: собственный libnsh.so приложения на Rust или APK хоста для разработки (пересобирается только при изменении среды выполнения), порт сервера разработки которого пробрасывается на устройство с помощью adb reverse, поэтому всё работает по USB без общей сети.
#macOS (AppKit)
nsh run my-app --target macos
Приложение открывается как десктопное приложение Mac (AppKit, в окне с изменяемым размером) с вводом мышью, трекпадом и клавиатурой вместо касаний. Нужна macOS 14 или новее; см. macOS.
#Windows (по умолчанию в Windows)
nsh run my-app
Приложение открывается в окне рабочего стола (1200×800 пунктов) с вводом мышью, тачпадом, касанием и клавиатурой. Нужна Windows 10 1809 или новее; см. Windows.
#Linux (по умолчанию в Linux)
nsh run my-app
Приложение открывается в окне GTK (390×844 пункта). Нужен GTK 4.14 или новее; см. Linux.
#Web
nsh run my-app --target web
nsh выводит адрес страницы; откройте его в браузере. Приложение на Rust работает как WebAssembly с вашим крейтом, а открытая страница сама загружает новую сборку после изменения; см. Web.
#Mac Catalyst
nsh run my-app --target catalyst
Приложение открывается на вашем Mac в окне размером с iPhone (390×844 пункта). Это самый быстрый способ увидеть приложение так, как его показывает телефон: не нужно запускать симулятор. Используется UIKit, как в iOS, поэтому вы видите то же, что показывает iPhone, за исключением поведения, свойственного Mac, например прокрутки колесом мыши. Для самого приложения Mac используйте --target macos.
#Редактирование
Оставьте nsh run запущенным и измените подпись кнопки:
text("Add one")
.text_size(px(17.0))
.text_color(move || theme().primary_foreground)<Text style={{ color: "white", fontSize: 17 }}>Add one</Text>Сохраните. Приложение на Rust собирается заново и перезапускается с новой подписью; счётчик начинается сначала. Приложение на TSX получает горячее обновление: CLI пересобирает и отправляет патч, а хост заново выполняет изменённый модуль и заново отображает только те компоненты, код которых изменился. Здесь это App, которому принадлежит сигнал count, поэтому счётчик тоже начинается сначала.
Части интерфейса выносятся в собственные компоненты или в обычные функции, возвращающие элементы:
/// The app's button: a view that answers presses.
fn tap_button(on_press: impl Fn() + 'static) -> impl IntoElement {
view()
.p_3()
.rounded(px(10.0))
.bg(move || theme().primary)
.active(|s| s.opacity(0.6))
.on_press(on_press)
.child(text("Tap").text_size(px(17.0)).text_color(move || theme().primary_foreground))
}
// In App: .child(tap_button(move || count.update(|value| *value += 1)))function TapButton(props: { onPress: () => void }) {
return (
<View onPress={props.onPress} activeStyle={{ opacity: 0.6 }} style={{ padding: 12, borderRadius: 10, backgroundColor: "#0969da" }}>
<Text style={{ color: "white", fontSize: 17 }}>Tap</Text>
</View>
);
}
// In App: <TapButton onPress={() => count.update((c) => c + 1)} />В TSX состояние сохраняется, когда вы редактируете компонент, которому оно не принадлежит: нажмите несколько раз, затем измените цвет TapButton, и кнопка обновится, а счётчик останется. Горячая замена работает для каждого компонента верхнего уровня: функции с именем с заглавной буквы, тело которой содержит JSX. Компонент отображается заново, когда меняется его собственный код или код модуля вне компонентов (используемая им константа, вспомогательная функция). Если правку нельзя применить на месте (например, она импортирует модуль NSH, который запущенное приложение ещё не импортировало, или меняет возможности nsh.json), хост перезагружает всё приложение.
#Ошибки
Ошибка компиляции Rust выводится в терминал вместе с выводом cargo, а nsh run собирает заново при следующем сохранении. Ошибка сборки TSX выводится в терминал и показывается на устройстве красным оверлеем с файлом, строкой и столбцом; приложение продолжает работать на последней успешной сборке. Оверлей исчезает после следующей успешной сборки (нажмите на него, чтобы закрыть раньше). Исключение, возникшее во время выполнения, показывается так же, а его стек сопоставляется с вашим исходным кодом .tsx.
#Встраивание в ваше приложение
Нативное приложение показывает NSH в собственном представлении: NshAppView (UIView) на iOS, NshView (View Android) на Android, AppView на macOS, Windows, Linux и в приложениях GPUI. Размещайте его, как любое другое представление, на любом экране и столько раз, сколько нужно.
Корневой компонент на Rust входит в сборку приложения: приложение линкует ваш крейт и монтирует корневой компонент с помощью AppSource::Rust на AppView для macOS, Windows, Linux, GPUI и iOS (Сборка с Rust). Приложение на TSX — это модуль, который загружает представление: при разработке с сервера разработки, в релизе из собранного бандла или загруженного подписанного пакета. Разделы ниже загружают модули TSX.
#iOS
Подключите NativeShellDev.xcframework в Debug и NativeShell.xcframework в Release (Встраивание в приложения iOS), затем:
import NativeShell
let app = NshAppView()
app.onError = { message in print("nsh error: \(message)") }
view.addSubview(app) // and constrain it like any view
#if DEBUG
try app.connectDevServer(URL(string: "ws://127.0.0.1:8790/ws")!) // `nsh dev my-app` on your Mac
#else
try app.loadBundle(at: Bundle.main.url(forResource: "bundle", withExtension: "nshc", subdirectory: "nsh")!)
#endif
nsh dev my-app передаёт модуль приложению и отправляет каждое сохранение, как nsh run делает это со своим хостом. Для релизной сборки nsh build my-app --release --out MyApp/nsh записывает бандл в папку, которую вы добавляете в ресурсы приложения.
#Android
Добавьте nsh-dev.aar для отладочных сборок и nsh.aar для релизных (Встраивание в приложения Android), затем:
val nsh = NshView(this)
nsh.setErrorListener { message -> Log.e("nsh", message) }
setContentView(nsh) // or add it to any layout
if (BuildConfig.DEBUG) {
nsh.connectDevServer("ws://127.0.0.1:8790/ws") // with `adb reverse tcp:8790 tcp:8790`
} else {
nsh.loadAssetBundle("nsh") // assets/nsh, from `nsh build my-app --release --out app/src/main/assets/nsh`
}
#macOS, Windows и Linux
Десктопные приложения на Rust встраивают NSH с помощью AppView из крейтов nsh-platform-macos, nsh-platform-windows, nsh-platform-linux и nsh-platform-gpui: NSView, дочернего окна, виджета GTK или представления GPUI, которое монтирует корневой компонент на Rust или загружает собранный бандл. См. macOS, Windows, Linux и GPUI, а также Платформы, где все хосты показаны рядом.
#Что модулю разрешено
На iOS и Android хост до загрузки решает, что модулю разрешено: grantCapabilities задаёт сетевые хосты и другие возможности, к которым он может обращаться, а лимиты ограничивают его память, время вызова и число узлов (Песочница и возможности). Чтобы дать модулям собственные нативные представления (карту, диаграмму, компонент дизайн-системы), зарегистрируйте их как нативные компоненты.
#Доставка обновлений в виде подписанных пакетов
Модуль TSX, который приложение загружает после релиза, поставляется как подписанный пакет. Создайте корневой ключ один раз и храните его офлайн, заверьте им релизный ключ и опубликуйте:
nsh keygen keys/root && nsh keygen keys/release
nsh certify --root keys/root.secret --key keys/release.pub --apps 'com.example.*' -o keys/release.cert
nsh publish my-app --key keys/release.secret --cert keys/release.cert --build 2
Приложение доверяет открытому ключу корня и загружает то, что скачивает; NSH проверяет подпись, сертификат и список отзыва перед тем, как что-либо выполнить:
let support = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
let trust = try NshTrust(stateDirectory: support.appendingPathComponent("nsh"))
try trust.addRoot(rootPublicKey) // the contents of keys/root.pub
let info = try await app.loadPackage(at: downloadedFile, trust: trust)
В Android тот же процесс работает с NshTrust и NshPackage. Хостинг, доставка и отзыв подробно: Подписанные пакеты.
#Или поставьте как отдельное приложение
nsh package собирает устанавливаемое приложение, на Rust или TSX, без собственного проекта Xcode или сборки Gradle:
nsh package my-app --target ios-simulator --run
nsh package my-app --target android
nsh package my-app --target macos
nsh package my-app --target linux
nsh package my-app --target windows
nsh package my-app --target web
Приложение на Rust упаковывается с собственным раннером, собранным из вашего крейта и крейтов NSH, с примонтированным корнем; приложение на TSX упаковывается с готовым рантаймом. Параметры, подпись и иконки: Упаковка.
#Попробуйте в терминале
nsh test запускает приложение в headless-режиме, без устройства и окна. Получив nsh.json приложения на Rust или точку входа приложения на TSX, он открывает приложение, даёт ему поработать и печатает то, что оно монтирует; с --trace также печатает нативные операции над представлениями и время каждого коммита (Тестирование):
nsh test my-app/nsh.json --trace # a Rust app's root
nsh test my-app/App.tsx --trace # a TSX app's entry
Если передать каталог, запускаются тесты приложения: App.test.tsx каждого шаблона открывает приложение, нажимает его кнопку и проверяет, что оно показывает (nsh test my-app). Тест приложения на Rust открывает его корень (open(".")); его также можно тестировать в cargo test с помощью nsh_testing::Headless (Разработка на Rust).
#Дальнейшие шаги
- Разработка на Rust: приложения на Rust, их тема и TSX поверх крейта на Rust.
- JSX и компоненты и Реактивность: компоненты и сигналы, на Rust и TSX.
- Платформы: что поддерживает каждая платформа и её отдельная страница (iOS, Android, macOS, Windows, Linux).
- Подписанные пакеты и Песочница и возможности: доставка и то, что разрешено модулям.
- Структура проекта: что находится в
nsh.json, ресурсы, файлы.