---
url: https://nsh.dev/ru/docs/web
description: "Создавайте веб-приложения на Rust или TSX с компонентами Native Shell, которые отображаются как HTML и CSS, разрабатывайте в браузере и публикуйте на любом статическом хостинге или в CDN."
demo: https://nsh.dev/play/index.html#counter
---

# Web

Создавайте веб-приложения на Native Shell с теми же компонентами, стилями и логикой приложения, что и на нативных платформах. NSH отображает интерфейс в виде HTML-элементов и CSS: текст, изображения, поля ввода, прокрутка, визуальные стили и взаимодействия используют DOM браузера и веб-API.

`nsh package --target web` публикует приложение как статический сайт для любого веб-сервера или CDN ([Публикация приложения](#publish-an-app)), а `nsh run --target web` запускает его в браузере, пока вы его редактируете ([Разработка в браузере](#develop-in-the-browser)). Все живые демо на этом сайте используют этот веб-бэкенд.

Опубликованные TSX-приложения используют JavaScript-движок браузера. При упаковке каждому приложению подключается собственная среда выполнения, содержащая только те компоненты `nsh/base` и стандартные модули, которые приложение импортирует: приложение, собранное из базовых компонентов, загружает около 1,1 МБ. Данные для подсветки синтаксиса загружаются при первой подсветке кода.

<a id="what-runs-where"></a>

## Что где выполняется

Страница строится из HTML-элементов, стилизованных с помощью CSS. NSH превращает обновления компонентов в пакеты изменений DOM, а браузер отображает интерфейс и обрабатывает прокрутку, выделение текста и ввод. WebAssembly содержит общий движок на Rust для состояния компонентов, раскладки и обновлений: либо с подключённым крейтом Rust-приложения, либо рядом с JavaScript-движком браузера, который выполняет код TSX-приложения как ES-модули: так работают опубликованные приложения, страница dev-сервера, демо на этом сайте и playground.

На каждой нативной платформе у движка есть собственный поток, и он передаёт главному потоку один `MountBatch` на каждый коммит. У страницы браузера один поток, поэтому веб-сборка управляет тем же движком пошагово из цикла событий страницы (`nsh_engine::LocalEngine`):

```text
pointer / keyboard / scroll events ─┐
setTimeout (the engine's deadline) ─┼─▶ nsh.wasm: engine turn (Rust or JS, core, layout) ─▶ one JSON batch ─▶ nsh.js: DOM
requestAnimationFrame ──────────────┘                                                 ▲
                                                          native animation outputs ───┘
```

- **Раскладку выполняет ядро.** Каждое представление — абсолютно позиционированный элемент с рамкой, вычисленной Taffy, поэтому flexbox, grid, `aspectRatio` и проценты ведут себя точно так же, как на устройствах. Собственная раскладка браузера не участвует.
- **Стили превращаются в CSS в Rust.** Фон, границы, радиусы, тени (`boxShadow`, `textShadow`), градиенты, фильтры, `backdropFilter`, трансформации, `transformOrigin`, `outline`, `mixBlendMode`, переходы и встроенные keyframe-анимации становятся свойствами CSS. Классы Tailwind, состояния `active:`, `hover:` и `focus:`, а также `dark:` полностью разрешаются нативно, как и на устройствах.
- **Текст раскладывается в Rust** с использованием измерений шрифтов из браузера (`CanvasRenderingContext2D.measureText`) и рисуется фрагмент за фрагментом в позициях, выбранных раскладкой, поэтому нарисовано именно то, что измерено. Строки переносятся после пробелов, вокруг CJK-символов и в `\n`; `numberOfLines` обрезает текст многоточием.
- **Нативные анимации выполняются в Rust** на `requestAnimationFrame`: тайминг `Animated`, spring и decay, `interpolate`, анимированные значения, привязанные к `opacity`, `translate`, `rotate`, `scale`, шаги `transform` и `backgroundColor`, перетаскивания, привязанные через `pan`, значения, связанные с прокруткой, и аргументы canvas. JS не участвует в каждом кадре, как и на устройствах. `LayoutAnimation`, переходы стилей, keyframe-анимации `animate-*` и переходы между экранами работают так же, как на десктопных бэкендах, а `prefers-reduced-motion` отключает их (экраны плавно появляются).
- **Ввод:** нажатия с обратной связью `activeStyle` / `active:` и долгие нажатия, система респондеров (`onTouch*`, `PanResponder`), нативные перетаскивания и щипок и поворот двумя пальцами, прокручиваемые представления (прокрутка браузера), pull to refresh, масштабируемые прокручиваемые представления (щипок, ctrl+колесо и щипки на тачпаде, `zoomTo`), `<Input>` с управляемыми значениями, фокус и отправка, `Canvas`, `NavStack` и `NavPage` со страничными `header`, `Dialog`.
- **Системный UI рисуется внутри страницы:** `AlertDialog` выглядит как диалоги десктопных бэкендов в десктопных браузерах и как диалоги iOS на телефонах (с идиомой `desktop`, см. ниже). Пользовательские шрифты измеряются заново после их загрузки браузером, а приложение ожидает шрифты, которые оно импортирует, прежде чем показать первый кадр.
- **Диалоги** — это слой поверх всего приложения, показывающий всплывающее окно и подложку приложения, как на десктопных бэкендах. Escape, кнопка «Назад» браузера и нажатие на подложку закрывают их (`closeOnEscape`, `closeOnBackdropPress`), а `animation`, `onOpen`, `onClose`, `onCancel` и `onOpenChange` работают как на устройствах. Страница, которая [встраивает приложение](#embed-it-in-a-page) в часть своей раскладки, может вместо этого показывать диалоги и оповещения поверх всего окна браузера (`dialogs: "viewport"`), как iOS и Android показывают их поверх всего экрана.

<a id="keyboard-mouse-and-touch"></a>

## Клавиатура, мышь и касания

Опубликованные приложения ведут себя как десктопные в десктопных браузерах и как мобильные в браузерах телефонов:

- **Колёса и тачпады** прокручивают самое вложенное прокручиваемое представление по своей оси, которое ещё может двигаться (вертикальное колесо прокручивает горизонтальный список, если по вертикали ничего не прокручивается), и остаются на нём до конца прокрутки; щелчки колеса прокручивают плавно, а постраничные и привязывающиеся представления перемещаются на одну страницу за прокрутку. Ctrl + колесо и щипки на тачпаде управляют представлениями через нативные значения щипка вместо масштабирования страницы. Перетаскивание мышью не прокручивает, как и на десктопных платформах: перетаскивания мышью уходят в pan-жесты, систему респондеров и выделение текста. Прокручиваемые представления сообщают `onScrollDragStart` / `onScrollDragEnd`, `onMomentumStart` / `End` и `onScrollEnd` как на десктопных платформах.
- **Клавиатура**: Tab и Shift+Tab перемещают между нажимаемыми элементами и полями ввода (стили `focus:` приложения показывают клавиатурный фокус); Return нажимает элемент в фокусе, Space нажимает его при отпускании; стрелки, Page Up / Page Down, Space, Home и End прокручивают прокручиваемое представление вокруг фокуса или под мышью; Escape выходит из поля, закрывает диалог, затем возвращает назад. Сочетания клавиш `keymap` работают в браузере и сопоставляются на месте (`mod` — это Command в Safari и Chrome на Mac, Control в остальных случаях).
- **Возврат назад**: Escape, Alt+Left (Cmd+[ на Mac) и кнопка «Назад» мыши закрывают самый новый диалог или снимают самый вложенный стек, как кнопка «Назад» в Android; `popOnBack` и `onBackPress` работают так же. Когда приложение занимает всю страницу, каждая добавленная страница и каждый открытый диалог становятся записью в истории браузера, поэтому кнопка «Назад» браузера возвращает назад внутри приложения.
- **Касания**: панорамируются только прокручиваемые представления; сама страница никогда не прокручивается, не масштабируется и не обновляется жестом под приложением, а у нажатий нет задержки. В iframe свайпы, которые приложение не обрабатывает, прокручивают встраивающую страницу. Перетаскивания на представлениях с нативными pan-жестами, щипками и системой респондеров принадлежат приложению.
- **Доступность**: `role`, `accessibilityLabel`, `accessibilityHint`, `accessibilityState`, `accessibilityValue`, `accessibilityLiveRegion` и `accessibilityElementsHidden` становятся ARIA; `announce` озвучивается через live region, а `setAccessibilityFocus` перемещает фокус. Представления под диалогом или оповещением становятся инертными.
- **Выделение и меню**: в десктопных браузерах текст по умолчанию можно выделять, как на веб-странице, кроме интерактивных элементов (представления с обработчиками нажатия или касания: кнопки, нажимаемые строки, вкладки) и текста с `userSelect: "none"` или `select-none`; перетаскивание мыши по нему выделяет его, Ctrl/Cmd+C копирует его построчно, а Ctrl/Cmd+A выделяет весь текст, в котором находится выделение. В браузерах телефонов и планшетов можно выделять только текст с `userSelect: "text"`, как в нативных приложениях. Контекстное меню браузера появляется только в выделяемом тексте и полях ввода.

<a id="apps-with-rust-components"></a>

## Приложения с компонентами на Rust

Приложение, написанное на Rust ([Сборка с Rust](https://nsh.dev/translations/ru/docs/rust.md)), публикуется и запускается в браузере из своего каталога:

```sh
nsh package --target web           # .nsh/out/<Name>/: the app's own nsh.wasm, with its crate
nsh run --target web               # the dev server's page; editing the Rust code builds it again
```

Когда `nsh.json` указывает корневой компонент приложения (`"root"`), страница состоит из Rust-кода приложения и скрипта страницы NSH, и больше ничего: `nsh.wasm` содержит движок, компоненты и крейт приложения, у сайта нет `app.js`, а страница монтирует корневой компонент, как только загружены среда выполнения и шрифты. Вся галерея на Rust занимает 3,3 МБ (1,06 МБ с gzip, 0,75 МБ с brotli), что меньше среды выполнения TSX-приложений. Под своей TSX-точкой входа (`nsh package App.tsx --target web`) то же приложение работает как TSX-приложения, на JavaScript-движке браузера, и импортирует компоненты и модули крейта.

В обоих случаях ресурсы крейта (`assets!()`) публикуются вместе с ресурсами приложения, а его шрифты загружаются до первого кадра. Во время `nsh run --target web` изменение Rust-кода крейта заново собирает среду выполнения, и открытая страница загружает её сама; правки TSX остаются горячими обновлениями. Среда выполнения для разработки собирается инкрементально, поэтому изменение крейта готово примерно за секунду; `nsh package` собирает публикуемую версию, оптимизированную по размеру.

Rust-код, работающий на всех платформах, включая веб, использует сервисы движка: `fetch(url).await` выполняет HTTP-запросы через тот же сетевой стек, что и `fetch` в JS (здесь это стек браузера с его правилами CORS); `timer`, `set_timeout` и `set_interval` ожидают; а `nsh_core::time::Instant` и `nsh_core::time::unix_ms()` читают часы. `background` и `blocking` выполняют работу в потоках нативных целей.

Для сборки нужны цель Rust и clang, компилирующий в WebAssembly, как и для сборки самой среды выполнения: `rustup target add wasm32-unknown-unknown`, а также `clang` и `llvm-ar` (на macOS — набор инструментов LLVM, например `llvm` из Homebrew, через `CC_wasm32_unknown_unknown` и `AR_wasm32_unknown_unknown`).

<a id="browser-apis-from-rust"></a>

### API браузера из Rust

Крейт может вызывать собственные API браузера через [wasm-bindgen](https://wasm-bindgen.github.io/wasm-bindgen/) и его крейты `web-sys` и `js-sys` за `#[cfg(target_arch = "wasm32")]`: WebSocket, `IndexedDB`, буфер обмена, любой Web API, а также собственный JS крейта (`#[wasm_bindgen(inline_js = …)]`, `module = "/js/…"`). Когда веб-среда выполнения приложения вызывает JS таким образом (включая преобразования `JsValue`), `nsh` запускает на ней CLI `wasm-bindgen` и размещает его glue-код и JS крейта рядом со средой выполнения, как при разработке, так и в опубликованных сайтах. Версия CLI совпадает с версией крейта `wasm-bindgen`, который подключает среда выполнения и который указывает `nsh`: `cargo install wasm-bindgen-cli --version <version> --locked` устанавливает его (`NSH_WASM_BINDGEN` указывает CLI в другом месте, а не на `PATH`). Среда выполнения, которая подключает wasm-bindgen, но не вызывает через него JS (через возможности зависимости по умолчанию), собирается так же, как любая другая.

Крейт указывает используемые API как возможности `web-sys`, а закрепление `wasm-bindgen` сохраняет его версию, а значит и версию CLI, одинаковой от сборки к сборке:

```toml
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen = "=0.2.129"
web-sys = { version = "0.3", features = ["WebSocket", "MessageEvent"] }
futures-channel = "0.3"
```

Обратные вызовы браузера (пришедшее сообщение, таймер страницы) пробуждают задачи приложения, которые выполняются сразу:

```rust
#[cfg(target_arch = "wasm32")]
async fn echo(url: &str, text: String) -> Result<String, String> {
    use wasm_bindgen::{JsCast, prelude::*};
    let ws = web_sys::WebSocket::new(url).map_err(|e| format!("{e:?}"))?;
    let (tx, rx) = futures_channel::oneshot::channel();
    let tx = std::cell::RefCell::new(Some(tx));
    let on_message = Closure::<dyn FnMut(web_sys::MessageEvent)>::new(move |e: web_sys::MessageEvent| {
        if let (Some(tx), Some(data)) = (tx.borrow_mut().take(), e.data().as_string()) {
            let _ = tx.send(data);
        }
    });
    ws.set_onmessage(Some(on_message.as_ref().unchecked_ref()));
    on_message.forget();
    let open = ws.clone();
    let on_open = Closure::<dyn FnMut()>::new(move || {
        let _ = open.send_with_str(&text);
    });
    ws.set_onopen(Some(on_open.as_ref().unchecked_ref()));
    on_open.forget();
    rx.await.map_err(|_| "closed".to_owned())
}
```

Вызовы API браузера из крейта выполняются с собственными разрешениями страницы, так же как нативный код крейта выполняется с разрешениями приложения на других платформах: возможности `nsh.json` управляют тем, что проходит через движок (`fetch`, JS приложения), а правила браузера (CORS, Content-Security-Policy страницы) управляют каждым запросом, который выполняет страница.

<a id="publish-an-app"></a>

## Публикация приложения

```sh
# A Rust app, from its directory
nsh package --target web                   # .nsh/out/<Name>/: a static site
nsh package --target web --run             # and serve it on this machine (open the address it prints)

# A TSX app
nsh package App.tsx --target web
nsh package App.tsx --target web --run
```

`nsh package --target web` записывает `.nsh/out/<Name>/` (`--out` для другого каталога) со структурой, как у сборки фронтенда: одна страница и файлы, названные по их содержимому.

| Файл | Что это |
|---|---|
| `index.html` | Страница приложения: его название в качестве заголовка, значки, цвета его `appearance`, индикатор загрузки и сообщение, если приложение не удалось загрузить или запустить |
| `manifest.webmanifest` | Название, id, значки и цвета, с которыми браузеры устанавливают приложение |
| `nsh.<digest>.js` | Среда выполнения DOM в минифицированном виде (79 КБ, 22,5 КБ с brotli) |
| `nsh.<digest>.wasm` | Движок на Rust, раскладка и компоненты, а также собственный крейт Rust-приложения. TSX выполняется в JavaScript браузера; при упаковке подключаются только те компоненты и стандартные модули, которые импортирует первый экран приложения. |
| `nsh-<name>.<digest>.wasm` | Компоненты, стандартные модули и шейдеры, которые приложение использует за пределами первого экрана: загружаются в фоне после его показа |
| `<file>.br`, `<file>.gz` | Каждый файл, сжатый заранее, для хостов, отправляющих предварительно сжатые файлы |
| `nsh-syntaxes.<digest>.bin` | Синтаксисы, по которым подсвечивается код (набор bat: 0,92 МБ, 0,8 МБ с brotli), загружаются при первом показе кода на каком-либо языке в блоке кода, `Editor` или `DiffView` |
| `nsh-bindgen.<digest>.js` | Минифицированный glue-код wasm-bindgen для приложения, крейты которого вызывают [API браузера из Rust](#browser-apis-from-rust) |
| `snippets.<digest>/<crate>-<hash>/…` | JS этих крейтов, который импортирует glue-код (`inline_js`, `module = "/js/…"`), по путям, которые задаёт wasm-bindgen |
| `app.<digest>.js` | Скомпилированная TSX-точка входа в виде ES-модуля, загружаемого браузером. |
| `host.<digest>.js` | Хост страницы, `nsh.json` `web.host` ([функции хоста](#the-hosts-functions)) |
| `<name>.<digest>.js` | ES-модули, загружаемые по требованию через `import()`. |
| `assets/<path>.<digest>.<ext>` | Каждый ресурс, который импортирует приложение, загружается по своему URL при использовании (шрифты — до выполнения кода, который их регистрирует, так как текст измеряется с их помощью: до запуска приложения или вместе с чанком, который их импортирует) |
| `icons/<size>.<digest>.png` | Значок `nsh.json` размерами 32 (favicon), 180 (`apple-touch-icon`), 192 и 512 пикселей |

`<digest>` — это начало SHA-256 содержимого файла: имя файла меняется ровно тогда, когда меняется его содержимое, поэтому новый релиз меняет только `index.html` и действительно изменившиеся файлы. Везде используются относительные URL, поэтому сайт работает из любого каталога на любом хосте (`https://example.com/apps/counter/`).
<a id="runtime-size"></a>

### Размер рантайма

При упаковке каждое приложение получает собственный `nsh.<digest>.wasm` с движком и компонентами `nsh/base`, стандартными модулями и шейдерами, которые импортирует первый экран приложения. То, что импортируют остальные экраны, загружается в фоне после показа первого экрана. Приложение, чьи крейты вызывают [API браузера из Rust](#browser-apis-from-rust), получает в своём `nsh.<digest>.wasm` весь рантайм, включая эти крейты. Измерено с помощью `nsh package --target web` (МБ = 10^6 байт):

| Приложение | Использует | `nsh.<digest>.wasm` | gzip | Brotli |
|---|---|---|---|---|
| `examples/counter` | Основные компоненты | 3,96 МБ | 1,32 МБ | 0,97 МБ |
| `examples/gallery` | Все компоненты, `zlib` и шейдеры | 4,13 МБ | 1,36 МБ | 1,01 МБ |

<a id="deploy-it"></a>

### Развёртывание

Загрузите каталог на любой статический хост: объектное хранилище за CDN, Cloudflare Pages, Netlify, GitHub Pages, nginx. Два правила обеспечивают быструю загрузку и надёжное обновление:

| Файлы | `Cache-Control` |
|---|---|
| `index.html`, `manifest.webmanifest` | `no-cache` (перепроверяются при каждой загрузке, поэтому релиз появляется сразу) |
| всё остальное | `public, max-age=31536000, immutable` |

Отдавайте `.wasm` как `application/wasm` (тогда браузеры компилируют его во время загрузки), а браузерам, которые их поддерживают, отправляйте предварительно сжатые копии: `brotli_static on; gzip_static on;` в nginx, `file_server {
precompressed br gzip }` в Caddy или CDN со сжатием Brotli. С Brotli рантайм примерно на четверть меньше, чем с gzip. `nsh package --target web --run` раздаёт сайт именно так на `http://127.0.0.1:4173/`.

Приложение, опубликованное для веба, работает как упакованное нативное: как релиз, с возможностями, которые объявляет его `nsh.json` ([Песочница и возможности](https://nsh.dev/translations/ru/docs/sandbox-and-capabilities.md)); TSX-приложение выполняется на JavaScript-движке браузера. Если они ограничивают `network`, страница также содержит `connect-src` Content-Security-Policy с объявленными хостами (и собственным origin страницы), поэтому сам браузер отклоняет запросы и переадресации на любой другой хост. Ошибка, из-за которой приложение не запускается, показывается на странице; последующие ошибки выводятся в консоль браузера.

<a id="embed-it-with-an-iframe"></a>

### Встраивание через iframe

На вебе нет API для встраивания: встраивайте опубликованное приложение с помощью `<iframe>`. Его размер — это окно приложения:

```html
<iframe src="https://apps.example.com/counter/" title="Counter"
        style="width: 390px; height: 844px; border: 0"></iframe>
```

<a id="develop-in-the-browser"></a>

## Разработка в браузере

```sh
nsh run --target web               # a Rust app, from its directory
nsh run App.tsx --target web       # a TSX app
```

запускает dev-сервер и выводит адрес его страницы, `http://127.0.0.1:8790/`: откройте его в любом браузере. Страница — такой же хост для разработки, как и остальные: она запускает каждую сборку приложения, показывает ошибки сборки и ошибки выполнения поверх приложения и отправляет логи приложения в терминал. Изменение крейта Rust-приложения заново собирает его рантайм, и открытая страница загружает его сама; правки TSX применяются на месте (горячие обновления). Телефоны в той же сети открывают адрес, который `nsh run` печатает под ним (`on this network: http://192.168.…:8790/`). `nsh dev` раздаёт ту же страницу, если веб-рантайм установлен (в дистрибутиве — всегда).

<a id="build-the-websites-player"></a>

## Сборка плеера для сайта

```sh
cargo xtask web                       # nsh.wasm, nsh.js, the player and website/demos/*.tsx
cargo xtask web --out target/play     # somewhere else
```

Выходной каталог содержит:

| Файл | Что это |
|---|---|
| `gallery/nsh.<digest>.wasm` | Рантайм галереи, скомпонованный для неё `nsh package` (использует все компоненты и `zlib`: около 11,8 МБ, 3,6 МБ в gzip) |
| `gallery/nsh-syntaxes.<digest>.bin` | Синтаксисы, по которым рантайм подсвечивает код; загружаются, когда демо впервые показывает код на соответствующем языке |
| `nsh-playground.wasm` | То же плюс TSX-компилятор NSH, для [Playground](#the-playground) (около 8 МБ, 2,4 МБ в gzip) |
| `gallery/nsh.<digest>.js` | DOM-рантайм (минифицированный): загружает wasm, применяет пакеты изменений, превращает ввод в события |
| `index.html` | Страница плеера: запускает `demos/<name>.js` для `#<name>` |
| `nsh.wasm`, `nsh.js`, `browser/` | Рантайм, который загружает плеер: движок, DOM-рантайм и модули хоста браузера |
| `demos/<name>.js` | Каждое демо, скомпилированное `nsh build --release`: ES-модуль |
| `gallery/` | Приложение-галерея, опубликованное с помощью `nsh package --target web`, показывается на странице [Gallery](/gallery) |

Раздайте каталог любым статическим сервером (для потоковой компиляции ему нужен MIME-тип `application/wasm`) и откройте `index.html#counter`.

<a id="the-playground"></a>

## Playground

[Playground](/playground) позволяет редактировать и запускать приложение в браузере: страница компилирует TSX собственным JSX-компилятором NSH (встроен в `nsh-playground.wasm`, без rolldown) и запускает результат на JavaScript-движке браузера после каждой паузы в наборе. У каждого демо на этом сайте есть ссылка **Edit in Playground**, а **Share** помещает код в саму ссылку, поэтому на сервере ничего не хранится.

Приложение в Playground — это один модуль: он импортирует модули NSH (`nsh` и `nsh/*`), но не другие файлы или пакеты. Вывод `console.log` и ошибки появляются под телефоном.

Компилятор также доступен как Rust-функция `nsh_bundler::compile_module(name, source, ui_sets)` — без фичи `bundle` крейта.

<a id="embed-it-in-a-page"></a>

## Встраивание в страницу

Rust-приложение встраивается как опубликованный сайт, в [iframe](#embed-it-with-an-iframe). Для TSX `nsh.js` — это ES-модуль. `createPlayer` загружает рантайм в контейнер; `runModule(url)` запускает бандл, который записывает `nsh build` (`bundle.js`, ES-модуль), на JavaScript-движке браузера, в собственном realm приложения, заменяя то, что работало. Метод возвращает промис, который разрешается после загрузки модуля и отклоняется с ошибкой, остановившей его; эту ошибку сообщает и плеер (`onError`):

```js
import { createPlayer } from "./nsh.js";

const player = await createPlayer(document.querySelector("#phone"), {
  wasm: "./nsh.wasm",
  assetBase: "./assets/",   // where `asset:/…` URIs (image imports) are served from
  // dark: true,            // force an appearance; otherwise it follows the system
  // idiom: "phone",        // force "phone", "pad" or "desktop"; otherwise the pointer and the size decide
  // history: false,        // pages and dialogs as browser history entries; by default only when the app is the page, not in an iframe
  // dialogs: "viewport",   // Dialog, Sheet and AlertDialog over the whole browser window; by default over the container
});
await player.runModule(new URL("./bundle.js", location.href).href);
```

`assets` (отображение путей `asset:/` на URL) заменяет `assetBase`, `readAsset(uri)` возвращает байты ресурса, который нужен движку сразу (шрифта) или `null`, `data` сопоставляет файлы, которые рантайм загружает при первой необходимости, с их URL (`syntaxes`: `nsh-syntaxes.<digest>.bin`; по умолчанию `nsh-<name>.bin` рядом с `nsh.js`), а `glue` — это URL wasm-bindgen-обвязки рантайма, чьи крейты вызывают [API браузера из Rust](#browser-apis-from-rust) (`nsh-bindgen.<digest>.js`), которую плеер загружает вместе с рантаймом; `player.runModule(url, { capabilities, appId })` запускает приложение с объявленными им возможностями (его `capabilities.json`) и хранит данные под его идентификатором `nsh.json` (без него — по пути страницы). Страница, которую записывает `nsh package --target web`, использует всё это.

`runModule(url, { chunks })` также принимает расположение чанков [разделённого бандла](#split-bundles).

`player.runSource(code, { name, sourceMap, capabilities, appId })` запускает ES-модуль, заданный исходным текстом, с теми же параметрами. С `nsh-playground.wasm` `player.compileAndRun(tsx, name, options)` сначала компилирует TSX, запускает его с параметрами `runSource` и разрешается ошибкой компиляции или `null`; `player.compile(tsx)` возвращает `{ code }` или `{ error }`.

Размер контейнера — это окно приложения (`windowSize()`); изменение размера контейнера меняет размер приложения. `player.setDark(true)` переключает оформление во время выполнения, а приложения наблюдают это через классы `colorScheme()` и `dark:`.

Диалоги и алерты перекрывают контейнер. С `dialogs: "viewport"` они перекрывают всё окно браузера, как собственные модальные диалоги страницы, при этом окном приложения остаётся контейнер: `Dialog` раскладывается по размеру окна и безопасной области и выезжает от его нижнего края, его подложка закрывает всю страницу, а колесо мыши над ним не прокручивает страницу. Диалог показывается в верхнем слое браузера (Chrome 114, Safari 17, Firefox 125 и новее), поэтому он находится поверх остальной страницы, где бы ни был контейнер; в более ранних браузерах он перекрывает контейнер.


`Sheet` следует той же настройке показа, что и `Dialog` и `AlertDialog`. В iframe того же origin показ во viewport автоматически перекрывает внешнюю страницу, а приложение сохраняет исходный размер раскладки. Когда закрывается последнее всплывающее окно, iframe возвращается в исходное положение и размер; его движок и состояние остаются живыми. Демо в документации включают такой показ.

Для iframe с другим origin или в песочнице (sandbox) встраивающая страница регистрирует хост до загрузки плеера:

```js
import { hostPlayerFrame } from "./nsh.js";
const frame = document.querySelector("iframe");
const disconnect = hostPlayerFrame(frame);
frame.src = "https://apps.example.com/player.html"; // player uses dialogs: "viewport"
// When removing the embedding: disconnect();
```

Пока всплывающее окно видно, хост принимает запросы от окна этого iframe, делает окружающую страницу неактивной (inert) и блокирует её прокрутку, а при закрытии восстанавливает и то и другое. Каждая граница встраивания между разными origin требует содействия своего хоста; без него показ остаётся в пределах этого документа, а плеер сообщает предупреждение.

<a id="the-hosts-functions"></a>

## Функции хоста

В вебе страница — это хост приложения: она отвечает на `host.call(name, args)` [`host`](https://nsh.dev/translations/ru/modules/nsh.md#host) и отправляет события, которые слушает `host.on`. `host` у `createPlayer` — объект, чей `call(name, args)` возвращает результат или промис с ним; выброшенное исключение или отклонение отклоняет вызов приложения с сообщением об ошибке. `player.hostEvent(event, payload)` отправляет событие с JSON-данными (если данные не являются JSON, выбрасывается исключение). При `host` `host.available()` равно `true`.

```js
const player = await createPlayer(container, {
  wasm: "./nsh.wasm",
  host: {
    async call(name, args) {
      if (name === "user.info") return { name: "Ada" };
      throw new Error(`no host function ${name}`);
    },
  },
});
player.hostEvent("theme", { dark: true });
```

Опубликованное приложение получает свой хост из `nsh.json`: `web.host` указывает JS-модуль в каталоге приложения, относительно `nsh.json`, который экспортирует `createHost(player)`. `nsh package --target web` копирует его в сайт как `host.<digest>.js`, а страница запускает `createHost` вместе с плеером до старта приложения; то, что он возвращает (или чем завершается), должно иметь функцию `call`, иначе страница сообщает, что приложение не удалось загрузить. `createHost`, который получает плеер, с этого момента отправляет события:

```json
{
  "capabilities": {
    "host": ["user.*"]
  },
  "web": { "host": "./host.js", "connect": ["api.example.com"] }
}
```

```js
// host.js
const post = (url) => (args) =>
  fetch(url, { method: "POST", body: JSON.stringify(args) }).then((r) => r.json());

// The functions the app may call, by their exact names.
const functions = new Map([
  ["user.info", post("https://api.example.com/user/info")],
  ["user.settings", post("https://api.example.com/user/settings")],
]);

export async function createHost(player) {
  addEventListener("online", () => player.hostEvent("online", {}));
  return {
    call(name, args) {
      const f = functions.get(name);
      if (!f) throw new Error(`no host function ${name}`);
      return f(args);
    },
  };
}
```

Хост ищет каждое имя как есть в таблице своих функций, как показано выше, а свои URL и пути определяет сам: `capabilities.host` разрешает имена по их началу (`user.*`), поэтому имя, которое хост превратил в путь, могло бы дотянуться до того, что разрешение не охватывает. Рантайм пропускает имена длиной от 1 до 128 символов из `A`–`Z`, `a`–`z`, `0`–`9`, `_`, `.`, `:` и `-`, без `..`, а вызов любого другого имени отклоняет. Разрешение по префиксу заканчивается разделителем (`bridge.*`): `bridge*` тоже совпадает с `bridgeAdmin`.

События, отправленные до запуска приложения, например самим `createHost`, доставляются по порядку, как только оно стартует, так что слушатели `host.on` верхнего уровня приложения их получают. Событие, отправленное до регистрации слушателя, ему повторно не доставляется: приложение читает начальное состояние через вызов (`host.call("user.info")`), а его изменения отслеживает через события.

Модуль хоста выполняется на странице приложения и разделяет её Content-Security-Policy: когда возможности приложения ограничивают `network`, `connect-src` страницы содержит origin страницы, хосты, перечисленные в `capabilities.network`, и хосты, перечисленные в `web.connect`. `web.connect` называет хосты, до которых доходят собственные запросы модуля хоста (`fetch`, `XMLHttpRequest`, `WebSocket`), в виде шаблонов, как у `capabilities.network.hosts` (`api.example.com`, `*.example.com`, `"*"`), как `api.example.com` выше; собственные запросы приложения по-прежнему проверяются по его `capabilities`.

Модуль копируется как есть, поэтому другой код он импортирует по абсолютному URL. Для каждой вызываемой функции приложению по-прежнему нужна его [возможность](https://nsh.dev/translations/ru/docs/sandbox-and-capabilities.md) `host`. `nsh run --target web` запускает его на странице разработки, а `nsh test` запускает тот же модуль для каждого приложения, которое открывают его тесты, в headless-режиме или в браузере (`--target web`), так что приложение и его хост тестируются вместе ([Тестирование](https://nsh.dev/translations/ru/docs/testing-and-trace.md#the-pages-host)).

<a id="the-browser-as-a-platform"></a>

## Браузер как платформа

`platform.os` — это `"web"`. Тип устройства, на котором работает браузер, определяет поведение приложения, как `platform.idiom`:

- `"desktop"`: мышь (точный указатель с наведением) и контейнер шириной не менее 600 пунктов. `platform.desktop` равно `true`, `platform.select` выбирает `desktop`, и применяются свойства `desktop={{…}}`.
- `"pad"`: сенсорный экран, короткая сторона которого не менее 600 пунктов; иначе `"phone"`. Это мобильные устройства: `platform.select` выбирает `mobile`.

Идиома интерфейса определяется при запуске приложения и не меняется, пока оно работает, как и на любой платформе; узкое окно на десктопе остаётся десктопом, с раскладкой под его `windowSize()`. Свойства `web={{…}}`, классы `platform.select({ web })` и `web:` применяются только в браузерах; `ios={{…}}`, `native` и `ios:` там не применяются.

Страница сообщает остальные сведения об окружении и поддерживает их актуальными:

| Значение | Из браузера |
|---|---|
| Локаль (по умолчанию для `Intl`) | `navigator.languages[0]` |
| Часовой пояс (локальное время `Date`, значение `timeZone` по умолчанию для `Intl`) | пояс браузера (`Intl.DateTimeFormat().resolvedOptions().timeZone`), включая летнее время |
| Направление раскладки (`rtl:` / `ltr:`) | `<html dir>`, если задан, иначе письменность локали |
| `reduceMotion()` | `prefers-reduced-motion` |
| `fontScale()` | размер шрифта корневого элемента / 16 px (настройка размера текста в браузере) |
| `pixelRatio()` | `devicePixelRatio` (меняется при масштабировании и смене экранов) |
| `colorScheme()` | `prefers-color-scheme` |
| `safeArea()` | `env(safe-area-inset-*)` там, где они перекрывают контейнер (страницы `viewport-fit=cover` на телефонах) |
| `keyboardFrame()` | какую часть контейнера закрывает экранная клавиатура (`visualViewport`) |

`platform.version` равно `"0.0"`, а `screenReaderEnabled()` остаётся `false` (браузеры этого не сообщают). Панели браузера вокруг страницы принадлежат браузеру; [`statusBar`](https://nsh.dev/translations/ru/modules/nsh-base.md#status-bar) страницы нужен для телефонов.
<a id="networking"></a>

## Сеть

`fetch` (со стримами, `AbortController` и `FormData`) и URL `<Image>` проходят через собственный сетевой стек браузера после того, как NSH проверит [разрешения (capabilities)](https://nsh.dev/translations/ru/docs/sandbox-and-capabilities.md) приложения. Поскольку запросы выполняет браузер, действуют его правила:

- **Относительные URL** разрешаются относительно URL страницы, как в браузере: опубликованное приложение обращается к своему API через `fetch("/api/items")`, а `<Image src="/photos/a.jpg" />` загружается с собственного сервера. (На нативных платформах страницы нет, поэтому там нужны абсолютные URL.)
- **CORS.** Запрос к источнику, отличному от источника страницы, успешен, только если сервер разрешает источник страницы (`Access-Control-Allow-Origin`); иначе `fetch` завершается сетевой ошибкой. Видны только те заголовки ответа, которые сервер явно раскрывает.
- **Cookies** (и HTTP-аутентификация) отправляются только на собственный источник страницы (`credentials: "same-origin"`), а заголовки, запрещённые браузерами (`Cookie`, `User-Agent`, `Host`, ...), отбрасываются.
- **Редиректы** выполняются браузером. Запрос, который не должен следовать редиректу, завершается ошибкой: `redirect: "manual"` и `"error"`, а также запросы приложения, чьи capabilities ограничивают `network` (NSH проверяет каждый переход, а браузеры не сообщают о редиректе до того, как последуют ему).
- **Тела запросов**, являющиеся `ReadableStream`, сначала собираются целиком и отправляются одним куском; тела ответов передаются потоком по мере поступления.

<a id="split-bundles"></a>

## Разделённые бандлы

TSX-приложению для веба не нужно загружать весь свой код до показа первого экрана (приложение на Rust — это один `nsh.wasm`, который браузер компилирует по ходу загрузки). `nsh package --target web` публикует TSX-приложения разделёнными: каждый чанк лежит в виде `<name>.<digest>.js`, а страница сопоставляет чанки с этими файлами. Чтобы разделить бандл самостоятельно, `nsh build --split` помещает модули, которые загружает `import()`, в отдельные чанки рядом с `bundle.js`:

```sh
nsh build --release --split -o site/app
# bundle.js        the app up to its first screen
# Screen.js        import("./Screen") and what only it uses
# detail.js        what two chunks share, when they do (made by the bundler)
```

Первый `import("./Screen")` загружает `Screen.js` и все чанки, которые тот импортирует, одновременно, затем выполняет их; последующие импорты разрешаются сразу. Страница сообщает, где находятся чанки: по умолчанию рядом с бандлом, либо по одному URL на чанк — для файлов, переименованных с добавлением хеша содержимого, чтобы CDN мог кешировать их бессрочно:

```js
await player.runModule(entry, {
  chunks: { "Screen.js": "/app.3f2a9c.js", "detail.js": "/app.81bb07.js" },
});
```

Чанк, который не удалось загрузить, отклоняет `import()` с `TypeError`, указывающей на него; следующий `import()` попробует снова. Имена чанков — это фиксированные имена файлов из бандла: приложение не может заставить страницу загрузить что-либо другое.

[Галерея](/gallery) загружает каждое демо таким способом: `lazy` (в `examples/gallery/ui.tsx`) создаёт компонент, который запускает импорт при первом рендере, до его завершения показывает заглушку, затем отображает загруженное. Его первая загрузка — это индекс и демо, показываемое первым; шрифт, который импортирует демо, поставляется вместе с чанком этого демо. Нативные сборки содержат все модули, поэтому там импорт разрешается сразу:

```tsx
const Stocks = lazy(() => import("./demos/Stocks").then((m) => m.Stocks));
```

<a id="limits"></a>

## Ограничения

Эти ограничения определяет браузер, а веб-бэкенд им следует:

- **Веб-представления других сайтов**: `WebView` — это `<iframe>`. Встроенный HTML (`html`) получает всё: `onMessage`, `postMessage`, `injectJavaScript`, внедряемые скрипты и события загрузки (он выполняется с собственным источником, отдельным от страницы). Страница другого сайта получает только `postMessage` и события загрузки с переданным ей URL: браузеры скрывают от окружающей страницы её переходы, заголовок, историю и ошибки, а `userAgent` задать нельзя.
- **Видео со звуком, запускающиеся автоматически**: `Video` — это `<video>` со всеми свойствами и событиями, но браузеры запускают видео со звуком только после действия пользователя (видео без звука воспроизводятся).
- **Нативные компоненты**: зарегистрированные нативные компоненты принадлежат нативным приложениям, встраивающим NSH; в вебе их представление остаётся пустым, а в консоль выводится предупреждение.
- **Платформенные сервисы**: чтение буфера обмена (`Clipboard.readText`, а также `hasText`) требует безопасного контекста (HTTPS) и может запросить разрешение у пользователя. Изображения не сообщают `onProgress`.
- **Детали обработки текста**: разбиение на строки реализует подмножество UAX #14 (пробелы, CJK, дефисы, а также правила начала и конца строки для CJK-пунктуации: закрывающие знаки, такие как `。`, `，` и `」`, не переносятся в начало строки, открывающие, такие как `（` и `「`, — в её конец), без переносов слов и без двунаправленного переупорядочивания сверх направления абзаца.

Плеер и dev-сервер запускают приложения с настройками разработки; опубликованные приложения работают как релизные. Оба выполняют код TSX-приложения на JavaScript-движке браузера, в собственном realm приложения.

<a id="see-also"></a>

## См. также

- [Packaging](https://nsh.dev/translations/ru/docs/packaging.md): другие цели `nsh package`.
- [Architecture](https://nsh.dev/translations/ru/docs/architecture.md): движок, ядро и батчи монтирования, которые переиспользует веб-сборка.
- [Testing](https://nsh.dev/translations/ru/docs/testing-and-trace.md): `nsh test` запускает тот же движок без интерфейса, с приблизительным текстовым движком.
