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

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

Архитектура

Эта страница для разработчиков приложений, которые хотят понять, что происходит между count.set(1) и изменением надписи на экране в приложении, написанном на Rust или TSX. Она не нужна для написания приложений, но объясняет, почему NSH ведёт себя именно так: почему компоненты выполняются один раз, почему анимации остаются плавными, пока JS занят, и почему основной поток никогда не ждёт ваш код.

#Общая картина

How an nsh app runs: the nsh CLI builds App.tsx into bytecode; on the engine thread, QuickJS runs a handler, the reactive graph in nsh-js queues ops, and nsh-core commits one MountBatch; it crosses to the main thread in one hop, which applies it to UIKit or Android views; touches go back as events. Native animations run on the main thread without JavaScript. How an nsh app runs: the nsh CLI builds App.tsx into bytecode; on the engine thread, QuickJS runs a handler, the reactive graph in nsh-js queues ops, and nsh-core commits one MountBatch; it crosses to the main thread in one hop, which applies it to UIKit or Android views; touches go back as events. Native animations run on the main thread without JavaScript.

#Сборка: в JS превращается только ваш код

nsh build, nsh dev и другие команды собирают ваше приложение с помощью инструментария, написанного на Rust (без Node.js):

  1. Oxc удаляет типы TypeScript.
  2. JSX-компилятор NSH превращает JSX в прямые вызовы нативного рендерера за тот же проход. Статические свойства и дочерние элементы задаются один раз; выражения, которые могут измениться (они вызывают функцию или читают свойство), становятся привязками, за которыми следит рендерер. Компоненты превращаются в обычные вызовы функций с объектом props, в котором изменяющиеся свойства являются геттерами.
  3. rolldown собирает ваши модули в один ES-модуль. Импорты модулей NSH (nsh, nsh/mobile, nsh/base, …) остаются импортами: эти модули нативные и предоставляются средой выполнения. Бандл содержит только код вашего приложения; пример со счётчиком занимает менее 1 КБ JS.
  4. Бандл компилируется в байт-код (bundle.nshc), который загружается быстрее исходного кода, а source map собирается по всем шагам, так что трассировки стека указывают на строки вашего .tsx.

Импортируемые изображения и шрифты собираются как ресурсы, а объявления типов для вашего редактора записываются в .nsh/types.

У приложения на Rust бандла нет: Cargo собирает запускающий модуль для каждой целевой платформы с вашим крейтом, а корневой компонент, названный в nsh.json, нативно выполняется движком. Всё описанное ниже справедливо и для такого приложения, только код ваших компонентов выполняется в Rust там, где в TSX-приложении он выполняется в QuickJS.

#Выполнение: два потока на приложение

Каждое запущенное приложение (NshAppView на iOS, NshView на Android, окно упакованного приложения) — это один движок со своим потоком движка и, для TSX, своей виртуальной машиной QuickJS. Глобальных синглтонов нет, поэтому несколько приложений могут работать рядом в одном приложении-хосте.

#Поток движка

Здесь происходит всё, что относится к логике вашего приложения:

  • Ваш код выполняется здесь: компоненты, обработчики событий, таймеры, эффекты. Компоненты на Rust выполняются нативно, TSX — в QuickJS.
  • Реактивный граф (сигналы, вычисляемые значения, эффекты, владельцы, контекст) нативный. Его узлы — это объекты QuickJS нативных классов, поэтому сборщик мусора видит ссылки через них, но отслеживание зависимостей и планирование обновлений реализованы на Rust (по алгоритму Solid 1.9).
  • Рендерер, который вызывает ваш скомпилированный JSX, нативный. Он создаёт узлы и превращает каждое изменение в операцию, помещаемую прямо в нативную очередь: без JSON и без отдельных сообщений на каждое свойство.
  • Ядро хранит теневое дерево всех узлов. Когда ход JS заканчивается, оно забирает все операции из очереди сразу и выполняет коммит: рендерит компоненты на Rust (nsh/base и собственные компоненты приложения), разрешает отступы env() в padding и margin, пересобирает изменившиеся тексты, выполняет раскладку с помощью Taffy (flexbox и grid, с колонкой по умолчанию и без сжатия), сравнивает фреймы и стили и формирует один MountBatch: список операций, нужных нативным представлениям.

#Основной поток

Основной (UI) поток платформы применяет каждый MountBatch за один переход: создаёт представления, вставляет их, задаёт стили и фреймы, передаёт уже разложенный текст. Кроме этого он делает только то, что должно происходить именно в нём:

  • отрисовка текста, уже разложенного в потоке движка;
  • нативные анимации: анимированные значения из nsh управляются покадрово (CADisplayLink на iOS, аниматор UI-потока на Android) и записываются прямо в представления;
  • жесты и прокрутка: касания превращаются в события, отправляемые в поток движка; scrollValue и pan передают анимированные значения в том же кадре.

Основной поток никогда не ждёт JS. Когда системе нужен ответ немедленно (можно ли изменить этот текст, может ли кнопка «назад» закрыть этот экран, можно ли закрыть этот лист), ответ заранее смонтирован как свойство, а JS узнаёт о нём позже через событие. Занятый поток движка может задержать обновления, но не может заморозить прокрутку, анимации или системные жесты.

#От касания до экрана

 main thread               engine thread                                              main thread
 ───────────               ─────────────────────────────────────────────────────      ───────────
 tap on a view ─ event ─▶  handler runs: count.set(count() + 1)
                           reactive graph: the bindings that read count() run again
                           renderer: one op per changed binding → native queue
                           JS turn ends → core commit: render, text, layout, diff
                           → MountBatch (e.g. 1 Text item)                 ─ hop ─▶   apply: draw the new text

В примере со счётчиком касание обходится примерно в 0,12 мс в релизной сборке на Mac, включая JS и коммит вместе, и порождает небольшое число элементов монтирования (см. Тестирование).

В приложении на Rust обработчик — это замыкание Rust (count.set(count.get() + 1)), а далее следуют те же граф, коммит и пакет, без интерпретатора на пути.

Из этого устройства следуют три свойства:

  • Один ход — один коммит. Сколько бы сигналов ни установил обработчик, представления изменятся один раз, вместе.
  • Компоненты выполняются один раз. Повторно выполняются только привязки; нет повторного рендера компонента и сравнения виртуального DOM. Остающееся сравнение выполняет ядро, над фреймами и стилями, на Rust.
  • Передаются только изменения. Неизменившиеся стили и фреймы не отправляются; новые представления не получают значений по умолчанию.

#Текст: раскладывается один раз, вне основного потока

Текст часто оказывается самой дорогой частью UI. NSH раскладывает его в потоке движка штатным текстовым движком платформы, поэтому он выглядит в точности как нативный текст:

  1. подготовка: <Text> и его вложенные спаны превращаются в одну строку с атрибутами (CoreText на платформах Apple, PrecomputedText / StaticLayout на Android);
  2. измерение при ширинах, которые запрашивает раскладка; измерения кешируются по содержимому и ширине, поэтому изменение только цвета не приводит к повторному измерению;
  3. раскладка в итоговом размере.

Основной поток получает неизменяемый результат и только рисует его. Текст, вложенный в <Text>, — это спан, а не представление; строки вне <Text> ничего не отображают.

#Списки

List виртуализируется нативно. Ядро решает, какие строки находятся рядом с областью просмотра, вычисляет позиции, колонки и закреплённые заголовки и переиспользует ячейки. Когда ячейке нужен другой элемент, среда выполнения привязывает его (меняются акцессоры item() и index() вашей функции строки); кадры, не открывающие новых строк, вообще не вызывают JS. См. Списки.

#Всё остальное, что делает среда выполнения

  • Сеть: fetch и другие глобальные API — нативные классы. Запросы идут в HTTP-стек платформы (NSURLSession, HttpURLConnection) с потоковой передачей в обоих направлениях.
  • Изображения загружаются и декодируются вне основного потока, в размере представления, с кешем в памяти.
  • Ограничения и разрешения обеспечиваются средой выполнения на Rust: см. Песочница и разрешения.
  • Горячая замена модулей тоже нативная: патчи применяются средой выполнения значительно быстрее миллисекунды (см. Сервер разработки и отладка).

#Три слоя

UI в NSH построен из трёх слоёв, как и в GPUI Kit:

СлойNSHGPUI KitЧто это
Примитивыnsh-core и среда выполнения (nsh, nsh/mobile, nsh/desktop)gpuiХост-элементы, стили, раскладка, рендеринг, реактивность, окружение
Базовые компонентыnsh-base (nsh/base)gpui-baseКомпоненты без стилей, которые отвечают за поведение, состояние, фокус, клавиатуру и доступность, а внешний вид берут из приложения
Библиотека компонентовваша собственная или сообществаgpui-componentСтилизованные компоненты: кнопки, карточки, строки с внешним видом

Первые два слоя — это Native Shell. Третий — ваш: приложение строит собственную библиотеку компонентов на nsh/base и примитивах, в TSX или как компоненты Rust в собственном крейте, использует библиотеку, опубликованную сообществом, или стилизует свой UI прямо в приложении.

#Крейты

NSH — это рабочее пространство Rust. Для написания приложений собирать его не нужно (дистрибутив содержит CLI и готовые среды выполнения), но эти имена встречаются в логах и документации:

КрейтЧто это
nsh-coreПлатформонезависимое ядро: теневое дерево, стили, раскладка Taffy, слияние текста, виртуализация списков, элементы монтирования; значения CSS и Tailwind; нативный граф анимации (значения, интерполяция, timing / spring / decay); фреймворк и тема компонентов на Rust; модули среды выполнения на Rust (BackHandler из nsh/mobile, …).
nsh-basensh/base, базовые компоненты без стилей (gpui-base из NSH): навигация, диалоги, элементы управления и строка состояния; приложения на Rust используют его как nsh_base.
nsh-jsХост QuickJS и среда выполнения NSH: реактивный граф, рендерер, все нативные модули, глобальные API, горячая замена модулей, ограничения; а также объявления TypeScript для всего этого.
nsh-engineПоток движка: управляет nsh-js и nsh-core и передаёт платформе один пакет за кадр.
nsh-packageПодписанные пакеты: ключи, сертификаты, отзыв, хранилище доверия.
nsh-bundlerСборщик: rolldown, Oxc, JSX-компилятор NSH, source map, проверки классов Tailwind.
nsh-devСервер разработки и его клиент.
nsh-platform-iosБэкенд UIKit (iOS, симулятор, Mac Catalyst): монтирование представлений, текстовый движок CoreText, ввод; SDK для встраивания.
nsh-platform-androidБэкенд Android: libnsh.so и среда выполнения на Java (NshView, представления, изображения, сеть).
nsh-cliКоманда nsh.

Направление зависимостей: nsh-core ← nsh-js ← nsh-engine ← платформы; nsh-base строится на nsh-core. Ядро не зависит ни от платформы, ни от JS-движка, что позволяет nsh test запускать настоящую среду выполнения без интерфейса на любой машине.

#См. также