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

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

Введение

Native Shell (сокращённо NSH) — нативный UI-фреймворк для всех платформ. Вы создаёте приложение один раз, с единой моделью приложения — компоненты, сигналы, стили и один и тот же API везде, — а NSH запускает его на iOS, Android, macOS, Windows, Linux и в вебе, каждый раз как приложение этой платформы: из её собственных представлений, с её собственной системой работы с текстом, с привычным для её пользователей поведением. Пишите всё приложение на Rust, пишите экраны на TSX или сочетайте оба подхода на одном движке. На том же движке уже выпущенные приложения могут получать новый UI во время работы — написанный вашей командой или сгенерированный моделью — в виде подписанных модулей, в песочнице, которую настраивает хост.

#Что значит «нативный»

Нативность в NSH состоит из двух слоёв: основы платформы и взаимодействие, принятое на платформе.

Основы платформы. У каждой платформы свой бэкенд, построенный на UI, который на ней уже есть:

ПлатформаПредставленияТекстВвод текста
iOSUIKitCoreTextСистемные текстовые поля и текстовые представления
AndroidAndroid viewsStaticLayoutEditText
macOSAppKit, Core AnimationCoreTextNSTextField, NSTextView
WindowsОкна Win32, отрисованные через Direct2DDirectWriteРедактор NSH с системными методами ввода (IMM32)
LinuxGTK 4PangoText и TextView из GTK
WebHTML-элементы со стилями 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())),
        )
}

Представление (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:

  1. Сборка. nsh собирает раннер каждой цели — приложение для iOS, Android, macOS, Windows, Linux или веба — с подключённым вашим crate. Компоненты, состояние, обработчики и движок компилируются вместе в нативный код.
  2. Запуск. Корневой компонент приложения выполняется в потоке движка как код на Rust. В раннере нет движка JavaScript: QuickJS не входит в приложение на Rust ни на одной платформе.

#Приложения на TSX

Приложение на TSX (модуль) — это набор файлов .tsx / .ts с точкой входа, обычно App.tsx:

  1. Сборка. nsh объединяет файлы модуля в бандл (с помощью rolldown и Oxc) и компилирует JSX собственным компилятором. Импорты nsh и nsh/* остаются импортами: среда выполнения предоставляет их нативно, поэтому в бандле только ваш код. Бандл компилируется в байткод QuickJS.
  2. Запуск. Поток движка выполняет байткод в QuickJS, по одной виртуальной машине на каждое представление. Скомпилированный JSX напрямую вызывает нативный рендерер, а JavaScript только описывает UI и обрабатывает события; разбор стилей, разрешение классов Tailwind, сравнение деревьев, раскладка и текст — всё это происходит в Rust.

#Различия

Приложение на RustПриложение на TSX
ЯзыкRustTSX (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-модуль (форму, диаграмму, карточку бронирования), ваш сервер проверяет, собирает и подписывает его, а приложение показывает его нативно:

From a prompt to native UI: a model writes a TSX module; your server builds, checks and signs it; across the trust boundary, the app verifies the package, runs it in a sandbox with the limits and capabilities it set, and shows it with the platform's own UI. From a prompt to native UI: a model writes a TSX module; your server builds, checks and signs it; across the trust boundary, the app verifies the package, runs it in a sandbox with the limits and capabilities it set, and shows it with the platform's own UI.

На 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).
  • Безопасный запуск недоверенного кода. Сгенерированный код, доставляемый во время работы, — это недоверенный код: он приходит подписанным, выполняется в виртуальной машине с ограничениями и получает только те возможности, которые предоставил хост.

#Где это работает

ПлатформаТребованияБэкендВстраивается с помощью
iOSiOS 15.0+UIKitNativeShell.xcframework
AndroidAndroid 9+Android viewsnsh.aar
macOSmacOS 14+AppKitAppView
WindowsWindows 10 1809+Win32, Direct2DAppView
LinuxGTK 4.14+GTK 4AppView
WebСовременные браузерыHTMLcreatePlayer, iframe
GPUIДесктопыGPUIAppView (приложения на GPUI и GPUI Kit)
Mac CatalystmacOSUIKitДля разработки (--target catalyst)
One module, many hosts: the same bundle and Rust runtime on iOS, Android, Linux, macOS with AppKit and Windows with Win32 / Direct2D, GPUI on all three desktops, and web pages drawn with HTML. One module, many hosts: the same bundle and Rust runtime on iOS, Android, Linux, macOS with AppKit and Windows with Win32 / Direct2D, GPUI on all three desktops, and web pages drawn with HTML.

#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 рядом с ним. Сравнение подходов разбирает каждый из них.

#Следующие шаги