Telegram WebApp SDK на Rust: как я полюбил фронтенд

Telegram WebApp (MiniApp) with Rust
Telegram WebApp (MiniApp) with Rust

Предисловие

Это моя первая статья на Хабре. На первый взгляд может показаться, что она хорошо структурирована и грамотна, но не переживайте — Илон Маск до меня не добрался, это не очередной AI-спам.

Просто я такой. Можете зайти в мои open source проекты и посмотреть на код — сами убедитесь. При желании можете связаться и я расскажу про свои загоны. Короче, я педант, люблю красиво. Не надо думать что это "AI spam" — это просто я такой дотошный.

Структура статьи:

  1. Как и зачем я начал делать SDK

  2. Почему Rust на фронтенде имеет смысл

  3. Технические детали и архитектура

  4. Реальный опыт использования

  5. Честные минусы и проблемы

  6. Практические примеры и туториалы

А теперь поехали по тексту.


Как всё началось

Три месяца назад я делал очередной Telegram бот и подумал: "А на *** мне опять тащить npm, React и весь этот зоопарк зависимостей?" У меня бэкенд на Rust, модели данных на Rust, валидаторы на Rust, API на Rust... А фронтенд — на JavaScript. Приходится дублировать типы, поддерживать два разных стека, синхронизировать зависимости.

Это реально бесило.

Я открыл документацию Telegram WebApp API, посмотрел на официальный JS SDK из проекта telegram-apps и решил: "Ладно, попробую обернуть это в Rust через wasm-bindgen. Сколько там может быть? Пара методов?"

Через неделю у меня был рабочий прототип. Ещё через месяц — полноценный SDK на 6300+ строк кода с поддержкой всех методов API. Теперь это опубликованный крейт на crates.io с растущим числом скачиваний.

И знаете что? Это реально работает. Не "работает в теории", не "можно использовать для toy-проектов", а именно работает в продакшене.

Ссылка на проект: github.com/RAprogramm/telegram-webapp-sdk


Зачем вообще Rust на фронтенде?

Все мне говорили:

  • "Мало библиотек, экосистема сырая"

  • "Toolchain тяжелый, сборка долгая"

  • "Всё равно будут мосты с JS — смысла нет"

  • "Developer Experience не такой удобный"

"Sooo, wha’chu gon git, maaan?"

1. Compile-time безопасность

// Это не скомпилируется
let button_color: u32 = "red"; // ❌ error[E0308]: mismatched types

// А это в JavaScript упадёт в runtime
Telegram.WebApp.MainButton.setColor(undefined); // 💥

Вызовы API Telegram проверяются на этапе компиляции. Нет места для undefined is not a function, Cannot read property of null, и прочих JS-радостей.

2. Единый стек

Одна и та же структура данных работает и на сервере, и на клиенте. Одна кодовая база, одни валидаторы, один набор тестов. Никаких TypeScript-интерфейсов, которые нужно держать в синхронности с бэкендом.

3. Производительность под контролем

WASM предсказуем. Нет garbage collection пауз, нет "внезапных" тормозов из-за Vue/React reconciliation. Всё детерминировано и измеряемо.

4. Независимость от npm

Помните истории про left-pad, про colors и faker? С Rust крейтами такого не происходит. cargo.io имеет политику immutability — опубликованные версии нельзя удалить или изменить.


Что внутри SDK: цифры и факты

Без воды: 6,284 строк кода, 55 файлов, 2,469 строк документации. Покрывает полностью весь Telegram WebApp API версии 9.2. За 3 месяца вышло 15 релизов.

SDK поддерживает:

  • MainButton, SecondaryButton, BackButton

  • Theme и Viewport управление

  • Cloud Storage (для хранения данных в облаке Telegram)

  • Biometric Authentication

  • Haptic Feedback

  • Device Sensors (акселерометр, гироскоп)

  • Location Manager

  • Invoice Payments

  • И ещё куча всего


Архитектура: как это работает

Я не изобретал велосипед. Я взял официальный API и обернул его в типобезопасную Rust-обёртку через wasm-bindgen.

JavaScript API (Telegram.WebApp)
         ↓
   wasm-bindgen биндинги
         ↓
    Rust API (типобезопасный)
         ↓
   Yew/Leptos интеграция

Модули SDK:

  • telegram_webapp_sdk::webapp — базовые методы, initData, события

  • main_button, secondary_button, back_button — управление кнопками

  • theme, viewport — внешний вид и размеры

  • haptic_feedback — вибрация устройства

  • cloud_storage — хранилище в облаке Telegram

  • biometric — биометрическая аутентификация

  • location_manager, accelerometer, gyroscope — датчики

  • Макросы: telegram_app!, telegram_page!, telegram_router!

  • Интеграции с Yew и Leptos

  • Mock-окружение для локальной разработки


Проблема, которая чуть не убила проект

Самый сложный момент был с обработкой событий. В JavaScript это просто:

Telegram.WebApp.onEvent('mainButtonClicked', () => {
  console.log("Clicked!");
});

Но в Rust через wasm-bindgen нельзя просто так передать замыкание — оно должно жить достаточно долго. Первая версия приводила к segfault при клике на кнопку. Вот это было весело: "Рустовая безопасность памяти", ага.

// ❌ Это НЕ РАБОТАЛО — замыкание умирает сразу
app.on_event("mainButtonClicked", || {
    console::log_1(&"Clicked!".into());
})?;

Проблема в том, что wasm-bindgen требует, чтобы замыкание было 'static. Мне пришлось делать обёртку EventHandle, которая управляет временем жизни через Box::leak() и держит указатель на замыкание.

// ✅ Это РАБОТАЕТ
let handle = app.on_event("mainButtonClicked", |_| {
    console::log_1(&"Clicked!".into());
})?;
// handle автоматически управляет памятью

Эта одна проблема заняла 3 дня дебага с gdb, чтения исходников wasm-bindgen и проклятий в адрес lifetime анализатора. Три. Дня.

Но зато теперь это работает надёжно. И когда я это починил, �� понял всю мощь Rust: проблемы жёсткие, но когда решаешь — они решены навсегда. В JS я бы словил этот баг в проде через неделю, потратил бы час на поиски, пофиксил бы... и забыл через месяц, откуда он вообще взялся.


Сравнение: JS SDK vs Rust SDK

Покажу на живом примере. Управление главной кнопкой:

JavaScript (официальный SDK)

Telegram.WebApp.MainButton.setText("Pay $10");
Telegram.WebApp.MainButton.setParams({
  color: "#FF0000",
  text_color: "#FFFFFF"
});
Telegram.WebApp.MainButton.show();
Telegram.WebApp.MainButton.onClick(() => {
  console.log("Processing payment...");
  // тут может прилететь что угодно
});

Rust

use telegram_webapp_sdk::main_button::{set_text, show, on_click, set_params};
use telegram_webapp_sdk::webapp::MainButtonParams;

fn setup_payment_button() {
    set_text("Pay $10").unwrap();
    set_params(&MainButtonParams {
        color: Some("#FF0000".into()),
        text_color: Some("#FFFFFF".into()),
        ..Default::default()
    }).unwrap();
    show().unwrap();

    on_click(|| {
        console::log_1(&"Processing payment...".into());
        // типы проверены на компиляции
    }).unwrap();
}

В JavaScript вы можете передать null, неправильный тип, забыть поле — и узнаете об этом в runtime. В Rust код просто не скомпилируется, если вы передадите что-то неверное.


Интеграция с UI-фреймворками

SDK работает с Yew (React-like) и Leptos (Solid.js-like). Вот как выглядит код на Yew:

use telegram_webapp_sdk::yew::{use_telegram_context, BottomButton};
use yew::prelude::*;

#[function_component(PaymentPage)]
fn payment_page() -> Html {
    let ctx = use_telegram_context().expect("Telegram context");

    html! {
        <div>
            <p>{ format!("User ID: {:?}", ctx.init_data.user) }</p>
            <BottomButton
                text="Pay $10"
                color="#FF0000"
                on_click={Callback::from(|_| { /* обработка */ })}
            />
        </div>
    }
}

Если вы предпочитаете Leptos — он тоже поддерживается из коробки.


Mock окружение для разработки

Одна из проблем разработки Telegram WebApp — нужно запускать приложение внутри Telegram. Дебажить неудобно.

Поэтому я добавил mock-окружение. Создаёте конфиг файл с тестовыми данными пользователя и темы, подключаете фичу mock — и всё, можно запускать приложение локально в браузере без Telegram. Mock автоматически эмулирует весь Telegram.WebApp API.

Это сэкономило мне недели времени на разработку.


Макросы для удобства

Чтобы не писать boilerplate, я добавил макросы telegram_app!, telegram_page! и telegram_router!. Они автоматически инициализируют WebApp, загружают mock в debug-режиме, настраивают роутинг и запускают приложение. Меньше шаблонного кода — больше пользы.


Что не получилось (честно о минусах)

Не всё идеально. Вот с чем я столкнулся:

1. Sourcemaps плохие

В JavaScript у вас отличные sourcemaps, и вы видите точную строку ошибки. В WASM sourcemaps работают плохо. Дебажить сложнее.

Решение: я использую console_error_panic_hook для более читаемых panic-сообщений.

2. Нет Hot Module Replacement

В Vite/Webpack есть HMR — изменения применяются мгновенно. В Rust нужно пересобирать WASM. Это занимает ~5-10 секунд.

Решение: использую trunk с --watch, но это всё равно медленнее чем HMR.

3. Размер бинаря

Даже после оптимизации WASM весит больше чем минифицированный JS. Минимальное Yew приложение ~150KB (gzipped ~50KB) против React ~80KB (gzipped ~30KB).

Решение: для больших приложений разница нивелируется. Плюс есть wasm-opt для дополнительной оптимизации. Но да, "Hello World" на Rust тяжелее.

4. Экосистема UI компонентов

В React есть Material-UI, Ant Design, Chakra. В Rust есть... ну, yew-components и пара библиотек. Всё.

Решение: пишу компоненты сам или использую CSS-фреймворки (Tailwind работает отлично). В итоге даже хорошо — меньше зависимостей, больше контроля.

5. IDE поддержка

rust-analyzer иногда тупит на сложных макросах. Автокомплит не всегда работает идеально.

Решение: терпение и cargo check.


Производительность: цифры

Детальных бенчмарков не делал, но вот что заметил:

  • Сборка prod: React+TS ~45 сек, Rust+Yew ~12 сек

  • Размер бандла: React ~245 KB (gzip), Rust ~156 KB (gzip)

  • Runtime ошибки (первая неделя в проде): React — 3 бага, Rust — 0

Самое главное — все баги ловятся на компиляции, не в проде.


Пример реального использования

Использую SDK в нескольких проектах. Один из них — бот для управления подписками: список активных подписок, оплата через Telegram Invoice API, история транзакций, биометрическая аутентификация для критичных операций.

Стек: Rust везде. Backend (Axum), Frontend (Yew + telegram-webapp-sdk), PostgreSQL, Docker + Fly.io.

Что работает отлично: единые типы между бэком и фронтом, валидация на compile-time, Cloud Storage для кеша, Haptic Feedback для UX.

Что было сложно: настройка CI/CD для WASM, интеграция с Telegram Payments, дебаг в Safari на iOS (sourcemaps бесполезны).


Как начать использовать

Добавляете в Cargo.toml:

telegram-webapp-sdk = { version = "0.2", features = ["yew", "mock"] }

Пишете компонент:

use telegram_webapp_sdk::yew::use_telegram_context;
use yew::prelude::*;

#[function_component(App)]
fn app() -> Html {
    let ctx = use_telegram_context().expect("Telegram context");
    html! {
        <div>
            <h1>{ "Hello, Telegram!" }</h1>
            <p>{ format!("User: {:?}", ctx.init_data.user) }</p>
        </div>
    }
}

Собираете через trunk serve — и всё работает.

Полная документация в README и на docs.rs.


Что дальше?

Продолжаю использовать SDK в продакшене и активно развиваю проект.

Планы:

  • Поддержка Telegram Stars API

  • Улучшить mock-окружение (визуальный конфигуратор)

  • Больше примеров и туториалов

  • Интеграция с Dioxus

Если вы пишете на Rust, делаете Telegram боты, устали от npm-зоопарка и хотите типобезопасность на фронтенде — попробуйте: telegram-webapp-sdk = "0.2"


Ссылки


Заключение

Rust на фронтенде — это не "будущее", это настоящее. Да, экосистема ещё развивается. Да, есть шероховатости. Но если вы хотите типобезопасность, единый стек с бэкендом, предсказуемую производительность и меньше runtime-багов — WASM + Rust это реальная альтернатива JavaScript-стеку.

Я написал SDK, опубликовал его, использую в проде. И это работает. Не в теории, а прямо сейчас.

Если вы дочитали до конца — поздравляю! Значит, AI не вложил в в��с дурные мысли о том, что Rust на фронтенде — это безумие 😄

Кстати, изначально хотел сделать краткую структурированную статейку, чисто информативную. Но видимо не ценится такое... Поэтому получилось как получилось — с подробностями, примерами и честным рассказом о проблемах.

Просто попробуйте. Предсказуемый фронтенд на Rust — это круто. Возможно, вам зайдёт так же, как зашло мне.

Буду рад PR, issue и просто фидбеку в GitHub репозитории. Особенно если найдёте баги — их точно ещё есть :-)

Спасибо за внимание!