Эта страница переведена машинным способом и может содержать ошибки. Читать оригинал на английском

Документация

Начало работы

Это руководство создаёт приложение NSH, запускает его на нужных вам платформах и выпускает. Приложения NSH пишутся на Rust или TSX на одном и том же движке: Rust компилирует приложение в нативный код без виртуальной машины JavaScript, а TSX работает на QuickJS с горячими обновлениями, как модули, которые нативное приложение может загружать во время выполнения. Сначала руководство разбирает приложение на Rust, затем то же приложение на TSX. Предполагается, что nsh установлен и находится в вашем PATH (Установка); в checkout используйте вместо него ./target/debug/nsh.

guide-getting-started.tsx
guide-getting-started.tsxLive
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),
                                ),
                        ),
                ),
        )
}

На 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:

ЦельКомандаГде запускается
Симулятор iOSnsh run my-app --target iosmacOS с Xcode
Androidnsh run my-app --target androidmacOS, Windows, Linux
macOS (AppKit)nsh run my-appmacOS (по умолчанию)
Windowsnsh run my-appWindows (по умолчанию)
Linux (GTK 4)nsh run my-appLinux (по умолчанию)
Webnsh run my-app --target webmacOS, Windows, Linux
Mac Catalystnsh run my-app --target catalystmacOS

#Симулятор 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)

Сохраните. Приложение на 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)))

В 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).

#Дальнейшие шаги