Документация
Введение
Native Shell (сокращённо NSH) — нативный UI-фреймворк для всех платформ. Вы создаёте приложение один раз, с единой моделью приложения — компоненты, сигналы, стили и один и тот же API везде, — а NSH запускает его на iOS, Android, macOS, Windows, Linux и в вебе, каждый раз как приложение этой платформы: из её собственных представлений, с её собственной системой работы с текстом, с привычным для её пользователей поведением. Пишите всё приложение на Rust, пишите экраны на TSX или сочетайте оба подхода на одном движке. На том же движке уже выпущенные приложения могут получать новый UI во время работы — написанный вашей командой или сгенерированный моделью — в виде подписанных модулей, в песочнице, которую настраивает хост.
#Что значит «нативный»
Нативность в NSH состоит из двух слоёв: основы платформы и взаимодействие, принятое на платформе.
Основы платформы. У каждой платформы свой бэкенд, построенный на UI, который на ней уже есть:
| Платформа | Представления | Текст | Ввод текста |
|---|---|---|---|
| iOS | UIKit | CoreText | Системные текстовые поля и текстовые представления |
| Android | Android views | StaticLayout | EditText |
| macOS | AppKit, Core Animation | CoreText | NSTextField, NSTextView |
| Windows | Окна Win32, отрисованные через Direct2D | DirectWrite | Редактор NSH с системными методами ввода (IMM32) |
| Linux | GTK 4 | Pango | Text и TextView из GTK |
| Web | HTML-элементы со стилями CSS | Шрифты браузера | Поля ввода браузера |
Выделение текста и его меню, методы ввода, доступность и прокрутка предоставляются платформой, а всё, что приносит им новый выпуск ОС, попадает в приложение вместе с ним. Нативные компоненты — карта, веб-представление, видеоплеер, любое представление, зарегистрированное хостом, — это ещё одно представление в той же иерархии: оно размещается, обрезается и прокручивается наравне с остальными (Нативные компоненты).
Взаимодействие, принятое на платформе. Компоненты, которые NSH предоставляет в nsh/base, ведут себя так, как ожидают пользователи каждого устройства, поэтому приложению достаточно написать их один раз:
| Десктопы (и десктопные браузеры) | Телефоны и планшеты | |
|---|---|---|
Menu | Всплывающее окно у элемента-триггера; подменю раскрываются каскадом рядом со своим пунктом | Панель снизу; подменю заменяет собой пункты своего меню |
ContextMenu | Правый клик открывает его в позиции указателя | Долгое нажатие пальцем на триггере открывает его как панель |
Tooltip | Показывается, когда указатель задерживается на триггере | Показывается, когда его открывает приложение |
Popover | Нажатие снаружи закрывает его и доходит до того, что находится под указателем | Нажатие снаружи закрывает его |
Страницы, диалоги, клавиатура и строка состояния следуют той же идее: стеки навигации добавляют и убирают экраны с анимацией, принятой на каждой платформе, а кнопка «назад», Escape и жест «назад» делают то, что от них ожидают на этой платформе (Навигация).
#От телефона до десктопа и веба
Кроссплатформенность в NSH означает, что каждое устройство получает то приложение, которого от него ждут, а не один и тот же экран, увеличенный или уменьшенный:
- Одна модель приложения. Компоненты, состояние, бизнес-логика и экраны приложения общие; каждый бэкенд сопоставляет их со своей платформой.
- Раскладки, которые перестраиваются. Flexbox и CSS grid размещаются с помощью Taffy на всех платформах, а брейкпоинты Tailwind (
md:,lg:) и размер окна (window()в Rust) меняют структуру, а не только размер: чат-приложение показывает боковую панель, список бесед и сообщения рядом друг с другом в широком окне и по одному экрану за раз на телефоне (Раскладка, Tailwind). - Ввод, которого ждёт каждое устройство. Указатель, наведение, правые клики, горячие клавиши и фокус на десктопе; касания, жесты, экранная клавиатура и постраничная навигация на телефонах. Если приложению нужно что-то своё для конкретной платформы, это выражается через props
desktop={{…}}иplatform.select(Платформенно-специфичный код). - Проверка на каждой цели.
nsh testзапускает тесты приложения без графического интерфейса, а также те же тесты в окнах macOS, Windows, Linux и GPUI, в симуляторе iOS, на устройстве Android или в браузере, со штатным вводом платформы (Тестирование и трассировка).
Возьмём приложение вроде чат-клиента. Разработчик собирает его на десктопе с помощью nsh run, открывает в симуляторе iOS с помощью nsh run --target ios, чтобы проверить раскладку для телефона, затем в браузере с помощью nsh run --target web — каждый раз это один и тот же проект, — а nsh package создаёт устанавливаемые приложения и сайт. Один инженер может вытянуть то, на что раньше требовалась команда на каждую платформу, пока параллельно разрабатывается бэкенд.
#Rust прежде всего
NSH написан на Rust, и Rust — полноправный язык для создания приложений на нём:
- Целые приложения на Rust. UI, логика и компоненты — это Rust, скомпилированный в нативный код на каждой платформе, с владением, типами, потоками Rust и экосистемой crates.io. Вычисления выполняются на нативной скорости, а работа с большими данными, парсинг и системные задачи остаются в языке, лучше всего для них подходящем (Сборка на Rust).
- TSX как второй путь. TSX подходит для быстро написанных экранов, плагинов, небольших приложений и UI, доставляемого во время работы. Он выполняется на том же движке, что и Rust, с теми же примитивами, базовыми компонентами и сигналами.
- Оба вместе. Приложение на Rust может подключать экраны на TSX, а экраны на TSX импортируют Rust-компоненты, модули и классы приложения.
- Один набор инструментов. Единый исполняемый файл
nshсоздаёт, собирает, проверяет типы, запускает с горячей перезагрузкой, тестирует, упаковывает и подписывает приложения для каждой цели. Он включает собственный TSX-бандлер и проверку TypeScript, поэтому разработчику, пришедшему из Rust, Swift, Kotlin или C++, достаточно установить Rust иnsh— и можно работать (Установка, CLI).
#Приложение
Один и тот же счётчик на Rust и на TSX:
use nsh_core::prelude::*;
#[component]
pub fn Counter() -> impl IntoElement {
let count = signal(0);
view()
.flex_1()
.items_center()
.justify_center()
.gap_3()
.child(text(move || count.get().to_string()).text_5xl().font_weight(FontWeight::BOLD))
.child(
view()
.rounded_lg()
.bg(rgb(0x2563eb))
.px_5()
.py_3()
.active(|s| s.opacity(0.6))
.on_press(move || count.update(|n| *n += 1))
.child(text("Tap").text_lg().text_color(white())),
)
}import { mount, signal, Text, View } from "nsh";
function Counter() {
const count = signal(0);
return (
<View style={{ flex: 1, alignItems: "center", justifyContent: "center", gap: 12 }}>
<Text style={{ fontSize: 48, fontWeight: "700" }}>{count()}</Text>
<View
onPress={() => count.update((n) => n + 1)}
activeStyle={{ opacity: 0.6 }}
class="rounded-lg bg-blue-600 px-5 py-3"
>
<Text class="text-white text-lg">Tap</Text>
</View>
</View>
);
}
mount(Counter);Представление (view(), <View>) становится UIView на iOS, ViewGroup на Android, NSView на macOS, виджетом GTK на Linux, retained-представлением, отрисованным через Direct2D, на Windows и HTML-элементом в вебе; текст становится нативным текстовым представлением, а отклик кнопки на нажатие (.active(..), activeStyle) выполняется в главном потоке. Когда вы нажимаете, count.update изменяет один сигнал, и обновляется только тот текст, который его читает: функция компонента повторно не запускается. Приложение на Rust — нативный код от начала и до конца; в приложении на TSX JavaScript — только собственный код модуля.
#Retained-представления
NSH работает по retained-модели: каждый элемент — это представление, которое хранит платформа, а обновление затрагивает только то, что изменилось.
- Точечные обновления. Сигналы, как в Solid, но реализованные нативно: компоненты выполняются один раз, а при изменении повторно запускаются только выражения, читающие изменившийся сигнал.
- Между потоками передаётся только изменившееся. Изменения одного такта размещаются вместе и передаются в главный поток одним пакетом операций над представлениями на кадр; неизменившиеся стили и фреймы не отправляются, а представления, которых ничего не коснулось, остаются как есть.
- Анимации выполняются нативно. Анимации timing, spring и decay, жесты и значения, привязанные к прокрутке, вычисляются по тактам кадров платформы, без вызовов в JS на каждый кадр (Анимация).
Это подходит для экранов, из которых состоит большинство приложений, — чатов, почты, форм, списков, документов, настроек, ответа ИИ, поступающего потоком, — где за раз меняются несколько представлений, а остальные ждут. Такие экраны между обновлениями почти простаивают, а прокрутка и анимация держат 120 FPS на дисплеях ProMotion. В бенчмарке производительности NSH таблица из 500 строк котировок, обновляемая 1000 раз в секунду, использует 18% одного ядра на Linux в NSH Rust, против 20% для того же экрана, написанного напрямую на GPUI Fast. Тяжёлые вычисления следует выносить в Rust: TSX работает в QuickJS, интерпретаторе, а модуль на Rust выполняется на нативной скорости, даже когда его вызывает TSX, — сортировка 100 000 строк занимает около 3 мс, как и в чистом Rust (Производительность).
#Как это работает
Набор инструментов — единый бинарный файл nsh. Приложение пишется на Rust, на TSX или на обоих, и у каждого варианта свой путь к одному и тому же движку на Rust.
#Приложения на Rust
Приложение на Rust — это crate, полностью собираемый средствами Rust и Native, без среды выполнения JavaScript:
- Сборка.
nshсобирает раннер каждой цели — приложение для iOS, Android, macOS, Windows, Linux или веба — с подключённым вашим crate. Компоненты, состояние, обработчики и движок компилируются вместе в нативный код. - Запуск. Корневой компонент приложения выполняется в потоке движка как код на Rust. В раннере нет движка JavaScript: QuickJS не входит в приложение на Rust ни на одной платформе.
#Приложения на TSX
Приложение на TSX (модуль) — это набор файлов .tsx / .ts с точкой входа, обычно App.tsx:
- Сборка.
nshобъединяет файлы модуля в бандл (с помощью rolldown и Oxc) и компилирует JSX собственным компилятором. Импортыnshиnsh/*остаются импортами: среда выполнения предоставляет их нативно, поэтому в бандле только ваш код. Бандл компилируется в байткод QuickJS. - Запуск. Поток движка выполняет байткод в QuickJS, по одной виртуальной машине на каждое представление. Скомпилированный JSX напрямую вызывает нативный рендерер, а JavaScript только описывает UI и обрабатывает события; разбор стилей, разрешение классов Tailwind, сравнение деревьев, раскладка и текст — всё это происходит в Rust.
#Различия
| Приложение на Rust | Приложение на TSX | |
|---|---|---|
| Язык | Rust | TSX (TypeScript и JSX) |
| Во что собирается | Нативный код, подключённый к раннеру каждой цели | Байткод QuickJS, загружаемый средой выполнения |
| Движок JavaScript | Нет | QuickJS, одна VM на представление |
Во время разработки (nsh run) | Пересобирается и перезапускается при каждом сохранении | Горячие обновления при каждом сохранении |
| После релиза | Поставляется вместе с приложением | Также доставляется во время работы как подписанные пакеты, в песочнице |
| Подходит для | Целых приложений, тяжёлых вычислений, системных задач | Быстро написанных экранов, плагинов, UI, сгенерированного ИИ |
Эти подходы сочетаются: приложение на Rust может подключать экраны на TSX, а экраны на TSX импортируют его Rust-компоненты, модули и классы; такое приложение линкует QuickJS для их выполнения (Сборка на Rust).
#Общий движок
Начиная с движка, оба пути одинаковы. Изменения собираются за такт, размещаются с помощью Taffy (flexbox и CSS grid) и передаются в главный поток одним пакетом операций над представлениями на кадр; главный поток применяет пакет к нативным представлениям. Текст измеряется и раскладывается один раз в потоке движка (CoreText на платформах Apple, StaticLayout на Android, DirectWrite на Windows, Pango на Linux, метрики шрифтов браузера в вебе) и только отрисовывается в главном потоке.
В вебе тот же движок работает внутри страницы: браузер показывает HTML-элементы со стилями CSS, прокручивает их, выделяет их текст и обрабатывает ввод, а nsh package --target web публикует приложение как статический сайт, файлы которого названы по содержимому и готовы для любого CDN (Web).
#Динамический UI и UI, сгенерированный ИИ
NSH начинался как способ подгружать UI в нативные приложения после релиза, и это по-прежнему одно из его основных применений. Приложение встраивает NshAppView (iOS), NshView (Android), AppView (macOS, Windows, Linux и приложения на GPUI) или createPlayer (веб-страница) в том месте, где должен появиться модуль, а NSH запускает внутри него TSX-модули:
- Доставка после релиза. Модуль — это подписанный пакет (
.nshpkg), который приложение скачивает во время работы. Перед запуском приложение проверяет его подпись по доверенным ключам и по списку отзыва (Подписанные пакеты). Хост-приложение поставляет рантайм; команды создают модули под его API и доставляют их в собственном графике. - Хост сохраняет контроль. Каждый модуль работает в собственной виртуальной машине QuickJS с ограничениями по памяти, времени вызова и числу узлов и получает доступ только к тому, что разрешил хост: сетевые хосты, веб-страницы, строка состояния (Песочница и разрешения). Несколько модулей работают одновременно.
- Выглядит как остальная часть приложения. Модули отрисовываются средствами UI самой платформы, а хост может зарегистрировать собственные нативные представления — карту, плеер, компонент дизайн-системы, — чтобы модули их использовали (Нативные компоненты).
Тот же механизм позволяет приложению отвечать интерфейсом. Модель пишет небольшой TSX-модуль (форму, диаграмму, карточку бронирования), ваш сервер проверяет, собирает и подписывает его, а приложение показывает его нативно:
На iOS со стороны хоста нужно всего несколько строк:
let app = NshAppView() // a UIView: place it anywhere
try app.grantCapabilities(#"{ "network": { "hosts": ["api.example.com"] } }"#)
let info = try await app.loadPackage(at: downloadedFile, trust: trust) // verified, then run
#Удобство для ИИ
NSH проектировался для ПО, которое пишется с помощью ИИ, — как для целых приложений, так и для сгенерированных экранов:
- Одна модель приложения для всех платформ. Модель осваивает один способ писать UI, а результат работает на телефонах, десктопах и в вебе; NSH адаптирует взаимодействие под каждую платформу, так что ничего не приходится генерировать шесть раз.
- Rust или TSX — по задаче. Rust — для целых приложений и модулей, критичных к производительности, TSX — для экранов, плагинов и UI, генерируемого по запросу, — на одном движке.
- Точный сгенерированный контракт. Для каждого элемента, свойства, ключа стиля и модуля есть объявления TypeScript, сгенерированные из исходников на Rust (
.nsh/types), а API на Rust типизированы, поэтому сгенерированный код проверяется компилятором. Эта документация отдаётся простым текстом для моделей (llms.txt,llms-full.txt). - Одна командная строка на весь цикл. Агент создаёт приложение, собирает его, запускает на целевой платформе, прогоняет тесты, читает результат, исправляет и упаковывает — всё через
nsh.nsh testработает в headless-режиме, без устройства, и сообщает об ошибках и созданных представлениях; компилятор также запускается внутри процесса (nsh_bundler::compile_module) и в браузере (Playground). - Безопасный запуск недоверенного кода. Сгенерированный код, доставляемый во время работы, — это недоверенный код: он приходит подписанным, выполняется в виртуальной машине с ограничениями и получает только те возможности, которые предоставил хост.
#Где это работает
| Платформа | Требования | Бэкенд | Встраивается с помощью |
|---|---|---|---|
| iOS | iOS 15.0+ | UIKit | NativeShell.xcframework |
| Android | Android 9+ | Android views | nsh.aar |
| macOS | macOS 14+ | AppKit | AppView |
| Windows | Windows 10 1809+ | Win32, Direct2D | AppView |
| Linux | GTK 4.14+ | GTK 4 | AppView |
| Web | Современные браузеры | HTML | createPlayer, iframe |
| GPUI | Десктопы | GPUI | AppView (приложения на GPUI и GPUI Kit) |
| Mac Catalyst | macOS | UIKit | Для разработки (--target catalyst) |
#Native Shell и GPUI Shell
Longbridge создаёт NSH для собственных клиентов. Первой потребностью был слой плагинов для мобильного клиента, а отправной точкой стал GPUI Shell, который делает десктопные приложения на GPUI расширяемыми на JavaScript. На телефонах UI, отрисованному самим GPUI, пришлось бы заново воспроизводить то, что платформа уже умеет: выделение текста и его меню (Копировать, Поделиться, Перевести, Найти), методы ввода, доступность, визуальные материалы, которые приносит каждый релиз iOS, — и каждой мобильной команде пришлось бы осваивать новый UI-стек. Поэтому NSH отрисовывает интерфейс средствами UI каждой платформы и вырос из этого слоя плагинов во фреймворк для целых приложений на любой платформе.
У них разное позиционирование. GPUI Shell расширяет десктопные приложения на GPUI; Native Shell — это фреймворк для нативных приложений на телефонах, десктопах и в вебе, а его бэкенд GPUI встраивает приложения NSH в приложения на GPUI и GPUI Kit (GPUI).
#Сравнение с другими подходами
Для целых приложений NSH стоит в одном ряду с кроссплатформенными фреймворками, такими как React Native, Flutter, Qt, GPUI, Slint, Electron и Tauri; для UI, доставляемого после релиза, — в ряду с веб-представлениями, серверно-управляемым UI и контейнерами мини-приложений. Его выбор — собственный UI и собственное поведение взаимодействия каждой платформы в рамках одной модели приложения, с Rust в качестве языка приложения и TSX рядом с ним. Сравнение подходов разбирает каждый из них.
#Следующие шаги
- Установка и Начало работы: установите
nsh, напишите первое приложение на Rust или TSX и запустите его на каждой платформе. - Разработка на Rust: приложения на Rust и TSX поверх них.
- Платформы: что где работает, а также как запускать, упаковывать и встраивать на каждой платформе.
- JSX и компоненты и Реактивность: две идеи, на которых строится всё остальное, — в Rust и в TSX.
- Подписанные пакеты и Песочница и разрешения: доставляйте модули и определяйте, что им разрешено.