Хабрахабр

Yew — Rust&WebAssembly-фреймворк для фронтенда

Yew — аналог React и Elm, написанный полностью на Rust и компилируемый в честный WebAssembly. В статье Денис Колодин, разработчик Yew, рассказывает о том, как можно создать фреймворк без сборщика мусора, эффективно обеспечить immutable, без необходимости копирования состояния благодаря правилам владения данными Rust, и какие есть особенности при трансляции Rust в WebAssembly.

Под катом — видео и текстовая расшифровка доклада.
Пост подготовлен по материалам доклада Дениса на конференции HolyJS 2018 Piter.

Денис Колодин работает в компании Bitfury Group, которая занимается разработкой различных блокчейн-решений. Уже более двух лет он кодит на Rust — языке программирования от Mozilla Research. За это время Денис успел основательно изучить этот язык и использовать его для разработки различных системных приложений, бэкенда. Сейчас же, в связи с появлением стандарта WebAssembly, стал смотреть и в сторону фронтенда.

Agenda

Сегодня мы с вами узнаем о том, что такое Yew (название фреймворка читается так же, как английское слово «ты» — you; «yew» — это дерево тис в переводе с английского).

Немного поговорим об архитектурных аспектах, о том, на каких идеях построен фреймворк, о возможностях, которые в него заложены, а также об особенностях, которые нам дополнительно дает Rust по сравнению с другими языками.

В конце я расскажу, как начать использовать Yew и WebAssembly прямо сегодня.

Что такое Yew?

В первую очередь, это WebAssembly, т.е. исполняемый байт-код, который работает в браузерах. Он нужен для того, чтобы на стороне пользователя запускать сложные алгоритмы, например, криптографию, кодирование/декодирование. Проще реализовать это на системных языках, чем прикручивать костыли.

Он позволяет использовать различные языки программирования. WebAssembly — это стандарт, который четко описан, понятен и поддерживается всеми современными браузерами. И это интересно в первую очередь тем, что вы можете повторно применять код, созданный комьюнити на других языках.

Он необходим, чтобы подготовить WebAssembly — взять модуль (WASM), добавить к нему окружение и запустить. При желании на WebAssembly можно полностью написать приложение, и Yew это позволяет сделать, но важно не забывать, что даже в этом случае JavaScript остается в браузере. без JavaScript не обойтись. Т.е. Поэтому WebAssembly имеет смысл рассматривать скорее как расширение, а не революционную альтернативу JS.

Как выглядит разработка

Вы это все транслируете в бинарный формат и запускаете в браузере. У вас есть исходник, есть компилятор. Это, грубо говоря, эмулятор WebAssembly для браузера. Если браузер старый, без поддержки WebAssembly, то потребуется emscripten.

Yew — готовый к использованию wasm framework

Перейдем к Yew. Я разработал этот фреймворк в конце прошлого года. Тогда я писал на Elm некое криптовалютное приложение и столкнулся с тем, что из-за ограничений языка не могу создать рекурсивную структуру. И в этот момент подумал: в Rust моя проблема решилась бы очень легко. А так как 99% времени я пишу на Rust и просто обожаю этот язык именно за его возможности, то решил поэкспериментировать — скомпилировать приложение с такой же update-функцией в Rust.

Я его запустил и понял, что буквально за несколько часов заложил ядро, которое очень легко развить. Первый набросок занял у меня несколько часов, пришлось разобраться, как скомпилировать WebAssembly. Мне потребовалось еще буквально несколько дней, чтобы довести все это до минимального движка фреймворка.

Однако на сегодня он собрал более 4 тысяч звезд на GitHub. Я выложил его в open source, но не рассчитывал, что он будет сколько-нибудь популярным. Там же есть множество примеров. Посмотреть проект можно по ссылке.

Yew поддерживает компиляцию прямо в WebAssembly (wasm32-unknown-unknown target) без emscripten. Фреймворк полностью написан на Rust. При необходимости можно работать и через emscripten.

Архитектура

Теперь несколько слов о том, чем фреймворк отличается от традиционных подходов, которые существуют в мире JavaScript.

Возьмем случай, когда есть модель и есть сообщение, которое позволяет эту модель трансформировать. Для начала покажу, с какими ограничениями языка я столкнулся в Elm.

type alias Model = type Msg = Increment | Decrement

case msg of Increment -> { value = model.value + 1 } Decrement -> { value = model.value - 1 }

В Elm мы просто создаем новую модель и отображаем ее на экране. Предыдущая версия модели остается неизменяемой. Почему я на этом делаю акцент? Потому что в Yew модель является mutable, и это один из самых частых вопросов. Далее я поясню, почему так сделано.

Но по мере развития фреймворка увидел, что нет смысла хранить предыдущую версию модели. Изначально я шел по классическому пути, когда модель создавалась заново. И поэтому я могу изменять модель безопасно, зная, что Rust контролирует отсутствие конфликта. Rust позволяет отследить время жизни всех данных, изменяемые они или нет.

struct Model { value: i64, } enum Msg { Increment, Decrement, }

match msg { Msg::Increment => { self.value += 1; } Msg::Decrement => { self.value -= 1; } }

Это первый момент. Второй момент: зачем нам нужна старая версия модели? В том же Elm вряд ли существует проблема какого-то конкурентного доступа. Старая модель нужна только для того, чтобы понять, когда производить рендеринг. Осознание этого момента позволило мне полностью избавиться от immutable и не хранить старую версию.

Есть значение, которое сохраняется, когда мы вводим в поле input данные. Посмотрите на вариант, когда у нас есть функция update и два поля — value и name. Модель изменяется.

И поэтому мы его можем изменять сколько угодно. Важно, что в рендеринге значение value не участвует. Но нам не нужно влиять на DOM-дерево и не нужно инициировать эти изменения.

Для инициации я стал использовать флаг — просто булево значение — ShouldRender, который сигнализирует о том, что модель изменилась и нужно запускать рендеринг. Это натолкнуло меня на мысль о том, что только разработчик может знать правильный момент, когда действительно нужно инициировать рендеринг. При этом нет никаких накладных расходов на постоянные сравнения, нет расхода памяти — приложения, написанные на Yew, максимально эффективны.

Модель сохранила свое состояние, а на рендеринге это отразилось только с помощью флага. В примере выше не произошло вообще никакого выделения памяти, кроме как на сообщение, которое было сгенерировано и отправлено.

Возможности

Написание фреймворка, который работает в WebAssembly, — непростая задача. У нас есть JavaScript, но он должен создать некое окружение, с которым нужно взаимодействовать, и это огромный объем работы. Первоначальная версия этих связок выглядела примерно так:

Есть много проектов, которые идут по этому пути, но он быстро заводит в тупик. Я взял демонстрацию из другого проекта. Я стал использовать в Rust библиотеки, которые называют крейтами, в частности, крейт Stdweb. Ведь фреймворк — достаточно крупная разработка и приходится писать много стыковочного кода.

Интегрированный JS

С помощью Rust-макросов можно расширять язык — в Rust-код мы можем вставлять куски JavaScript, это очень полезная фича языка.

let handle = js! { var callback = @{callback}; var action = function() { callback(); }; var delay = @{ms}; return { interval_id: setInterval(action, delay), callback: callback, }; };

Использование макросов и Stdweb позволило мне быстро и эффективно написать все нужные связки.

JSX шаблоны

Вначале я пошел по пути Elm и начал использовать шаблоны, реализованные с помощью кода.

fn view(&self) -> Html<Context, Self> { nav("nav", ("menu"), vec![ button("button", (), ("onclick", || Msg::Clicked)), tag("section", ("ontop"), vec![ p("My text...") ]) ]) }

Я никогда не был сторонником React. Но когда стал писать свой фреймворк, то понял, что JSX в React — это очень крутая штука. Тут очень удобное представление кодовых темплейтов.

В итоге я взял макрос на Rust и внедрил прямо внутрь Rust возможность писать HTML-разметку, которая сразу генерирует элементы виртуального дерева.

impl Renderable<Context, Model> for Model { fn view(&self) -> Html<Context, Self> { html! { <div> <nav class="menu",> <button onclick=|_| Msg::Increment,>{ "Increment" }</button> <button onclick=|_| Msg::Decrement,>{ "Decrement" }</button> </nav> <p>{ self.value }</p> <p>{ Local::now() }</p> </div> } } }

Можно сказать, что JSX-подобные шаблоны — это чистые кодовые шаблоны, но на стероидах. Они представлены в удобном формате. Также обратите внимание, что здесь я прямо в кнопку вставляю Rust-выражение (Rust-выражение можно вставлять внутрь этих шаблонов). Это позволяет очень тесно интегрироваться.

Компоненты с честной структурой

Дальше я стал развивать шаблоны и реализовал возможность использования компонент. Это первый issue, который был сделал в репозитории. Я реализовал компоненты, которые могут использоваться в коде шаблона. Вы просто объявляете честную структуру на Rust и пишете для нее некоторые свойства. И эти свойства можно задавать прямо из шаблона.

Поэтому любая ошибка здесь будет замечена компилятором. Еще раз отмечу важную вещь, что эти шаблоны являются честно сгенерированным Rust-кодом. вы не сможете ошибиться, как это часто бывает в JavaScript-разработке. Т.е.

Типизированные области

Другая интересная особенность заключается в том, что, когда компонент помещается внутрь другого компонента, он может видеть тип сообщений родителя.

При обработке событий сообщения, которые ожидает или может отправлять компонент, должны будут полностью соответствовать родителю. Компилятор жестко связывает эти типы и не даст вам возможности ошибиться.

Другие возможности

Из Rust прямо во фреймворк я перенес реализацию, позволяющую удобно использовать различные форматы сериализации / десериализации (снабдив ее дополнительными обертками). Ниже представлен пример: мы обращаемся в local storage и, восстанавливая данные, указываем некую обертку — что мы ожидаем тут json.

Msg::Store => { context.local_storage.store(KEY, Json(&model.clients)); } Msg::Restore => { if let Json(Ok(clients)) = context.local_storage.restore(KEY) { model.clients = clients; } }

Здесь может быть любой формат, в том числе бинарный. Соответственно, сериализация и десериализация становятся прозрачными и удобными.

Они попросили сделать фрагменты. Идея другой возможности, которую я реализовал, пришла от пользователей фреймворка. Увидев в JavaScript возможность вставлять фрагменты в DOM-дерево, я сначала решил, что реализовать такую функцию в моем фреймворке будет очень легко. И вот здесь я столкнулся с интересной вещью. Пришлось разбираться, ходить по этому дереву, смотреть, что там изменилось и т.п. Но попробовал эту опцию, и оказалось, что она не работает.

Фактически, когда появляются какие-то изменения в шаблоне, они превращаются в патчи, которые уже изменяют отрендеренное DOM-дерево. Во фреймворке Yew используется виртуальное DOM-дерево, все изначально существует в нем.

html! { <> <tr><td>{ "Row" }</td></tr> <tr><td>{ "Row" }</td></tr> <tr><td>{ "Row" }</td></tr> </> }

Дополнительные преимущества

Rust предоставляет еще много разных сильных возможностей, я расскажу лишь о самых важных.

Сервисы: взаимодействие с внешним миром

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

Фактически, вы можете создать различные API для взаимодействия с вашим сервисом, в том числе JavaScript-овые. В Rust очень качественно реализована возможность создания библиотек, их интеграции, стыковки и склейки. При этом фреймворк может взаимодействовать с внешним миром, несмотря на то, что он работает внутри WebAssembly рантайма.

Примеры сервисов:

  • TimeOutService;
  • IntervalService;
  • FetchService;
  • WebSocketService;
  • Custom Services…

Сервисы и крейты Rust: crates.io.

Контекст: заявите требования

Другая вещь, которую я реализовал во фреймворке не совсем традиционно, это контекст. В React есть Context API, я же использовал Context в ином понимании. Фреймворк Yew состоит из компонентов, которые вы делаете, а Context — это некоторое глобальное состояние. Компоненты могут не учитывать это глобальное состояние, а могут предъявлять некоторые требования — чтобы глобальная сущность соответствовала каким-то критериям.

Допустим, наш абстрактный компонент требует возможности выгрузки чего-то на S3.

отправляет данные в S3. Внизу видно, что он использует эту выгрузку, т.е. Пользователь, который этот компонент скачает и добавит внутрь шаблона в свое приложение, столкнется с ошибкой — компилятор у него спросит, где поддержка S3? Такой компонент можно выложить в виде крейта. После этого компонент автоматически начинает жить полноценной жизнью. Пользователь должен будет эту поддержку реализовать.

Представьте: вы создаете компонент с хитрой криптографией. Где это нужно? Все, что вам  нужно сделать, это добавить в шаблоне форму авторизации и в вашем контексте реализовать связь именно с вашим сервисом. У него есть требования, что окружающий контекст должен позволять ему куда-то логиниться. это будет буквально три строчки кода. Т.е. После этого компонент начинает работать.

И они все имеют одно и то же требование. Представим, что у нас десятки различных компонент. Прямо из контекста. Это позволяет один раз реализовать какой-то функционал, чтобы оживить все компоненты и подтягивать данные, которые нужны. И компилятор вам не позволит ошибиться: если у вас не реализован интерфейс, который требует компонент, ничего не заработает.

Благодаря Rust и системе этих интерфейсов (они называются trait в Rust-е) появляется возможность задекларировать требования компонента. Поэтому можно легко создавать весьма привередливые кнопки, которые будут просить некоторые API или иные возможности.

Компилятор не даст вам ошибиться

Представим, что мы создаем компонент с некоторыми свойствами, одно из которых — возможность установить call back. И, например, мы установили свойство и пропустили одну букву в его названии.

Он говорит, что мы ошиблись и такого свойства нет: Пытаемся скомпилировать, Rust на это реагирует быстро.

Он подсказывает, как на самом деле должно было называться свойство. Как видите, Rust прямо использует этот шаблон и может внутри макроса делать рендеринг всех ошибок. Если вы прошли компилятор, то глупых рантайм-ошибок вроде опечаток у вас не будет.

И создаем контекст, который не реализует поддержку S3. А теперь представим, у нас есть кнопка, которая просит, чтобы наш глобальный контекст умел подключаться к S3. Посмотрим, что будет.

Компилятор сообщает, что мы вставили кнопку, но этот интерфейс не реализован для контекста.

Вы можете создать уже готовые сервисы с каким-то API, потом просто добавлять в контекст, подставлять на него ссылку — и компонент сразу же оживает. Остается только зайти в редактор, добавить в контекст связь с Amazon, и все заведется. И все это работает полностью автоматически, нужны минимальные усилия на то, чтобы это все связать. Это позволяет вам делать очень классные вещи: вы добавляете компоненты, создаете контекст, набиваете его сервисами.

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

С чего начать, если вы хотите попробовать скомпилировать WebAssembly приложение? И как это можно сделать с помощью фреймворка Yew?

Rust-to-wasm компиляция

Первое — вам потребуется установить компилятор. Для этого есть инструмент rustup:

Для чего он может быть полезен? curl https://sh.rustup.rs -sSf | sh

Плюс, вам может потребоваться emscripten. Очевидно, что в браузере многих возможностей нет. Большинство библиотек, которые написаны для системных языков программирования, особенно для Rust (изначально системного), разработаны под Linux, Windows и другие полноценные операционки.

emscripten вам пригодится, если вы хотите использовать библиотеки, которые требуют системный API. Например, генерация случайных чисел в браузере делается не так, как в Linux.

Библиотеки и вся инфраструктура потихонечку переходят на честный WebAssembly, и emscripten подкладывать уже не требуется (используются JavaScript-овые возможности для генерации случайных чисел и других вещей), но если вам нужно собрать то, что пока совсем в браузере не поддерживается, без emscripten не обойтись.

Также рекомендую использовать cargo-web:

Но cargo-web — классный инструмент, который дает сразу несколько вещей, полезных для JavaScript-разработчиков. cargo install cargo-web

Есть возможность скомпилировать WebAssembly без дополнительных утилит. В этом случае Cargo-web позволит вам ускорить разработку. В частности, он будет следить за файлами: если вы вносите какие-то изменения, он начнет сразу компилировать (компилятор таких функций не предоставляет). Есть разные системы сборки под Rust, но cargo — это 99,9% всех проектов.

Новый проект создается следующим образом:

1. cargo new --bin my-project

[package]
name = "my-project"
version = "0. 3. 0"
 
[dependencies]
yew = "0. 0"

Дальше просто стартуете проект:

Если вам нужно скомпилировать под emscripten (rust-компилятор может сам подключить emscripten), в самом последнем элементе unknown можно вставить слово emscripten, что позволит вам использовать больше крейтов. cargo web start --target wasm32-unknown-unknown

Я привел пример честного WebAssembly. Поэтому лучше писать честный WebAssembly-код. Не забывайте, что emscripten  - это достаточно большой дополнительный обвес к вашему файлу.

Существующие ограничения

Того, кто имеет опыт кодинга на системных языках программирования, существующие во фреймворке ограничения могут расстроить. Далеко не все библиотеки можно использовать в WebAssembly. Например, в JavaScript-окружении нет потоков. WebAssembly в принципе не декларирует этого, и вы, конечно, можете его использовать в многопоточной среде (это вопрос открытый), но JavaScript — это все-таки однопоточная среда. Да, есть воркеры, но это изоляция, поэтому никаких потоков там не будет.

Но если вы захотите использовать библиотеки, основанные на потоках, например, захотите добавить какой-то рантайм, это может не взлететь. Казалось бы, без потоков можно жить.

Поэтом многие библиотеки не перенесутся. Также здесь нет никакого системного API, кроме того, который вы из JavaScript перенесете в WebAssembly. Если вы хотите например, сделать web-socket, его надо притащить из JavaScript. Писать и читать напрямую файлы нельзя, сокеты открыть нельзя, в сеть писать нельзя.

Он пока находится в таком сыром состоянии, что вряд ли будет вам полезен. Другой недостаток заключается в том, что отладчик WASM существует, но его никто не видел. Поэтому отладка WebAssembly — это сложный вопрос.

Но очень редко появляются баги низкого уровня — например, какая-то из библиотек неправильно делает стыковки — и это уже сложный вопрос. При использовании Rust практически все рантайм-проблемы будут связаны с ошибками в бизнес-логике, их будет легко исправить. Знайте, если наткнетесь на проблему где-то в middleware на низком уровне, то починить это будет на текущий момент непросто. К примеру, на текущий момент существует такая проблема: если я компилирую фреймворк с emscripten и там есть изменяемая ячейка памяти, владение которой то забирается, то отдается, emscripten где-то посередине разваливается (и я даже не уверен, что это emscripten).

Будущее фреймворка

Как будет дальше развиваться Yew? Я вижу его основное предназначение в создании монолитных компонент. У вас будет скомпилированный WebAssembly-файл, и вы его будете просто вставлять в приложение. Например, он может предоставлять криптографические возможности, рендеринг или редактирование.

Интеграция с JS

Будет усиливаться интеграции с JavaScript. На JavaScript-е написано большое количество классных библиотек, которыми удобно пользоваться. И в репозитории есть примеры, где я показываю, как можно использовать существующую JavaScript библиотеку прямо из фреймворка Yew.

Типизированный CSS

Поскольку используется Rust, очевидно, что можно добавить типизированный CSS, который можно будет сгенерировать таким же макросом, что в примере JSX-подобного шаблонизатора. При этом компилятор будет проверять, например, не присвоили ли вы вместо цвета какой-то иной атрибут. Это сбережет тонны вашего времени.

Готовые компоненты

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

Улучшение производительности в частных случаях

Производительность — это очень тонкий и сложный вопрос. Быстрее ли работает WebAssembly по сравнению с JavaScript? У меня нет никакого пруфа, подтверждающего положительный или отрицательный ответ. По ощущениям и по некоторым совсем простым тестам, которые я проводил, WebAssembly работает очень быстро. И у меня есть полная уверенность, что его производительность будет выше, чем у JavaScript, только потому, что это низкоуровневый байт-код, где не требуется выделение памяти и много других требующих ресурсы моментов.

Больше контрибьюторов

Я бы хотел привлечь больше контрибьюторов. Двери для участия во фреймворке всегда открыты. Каждый, кто хочет что-то модернизировать, разобраться в ядре и трансформировать тот инструментарий, с которым работает большое число разработчиков, может легко подключаться и предлагать свои правки.

Но Core-контрибьюторов на текущий момент нет, потому что для этого нужно понимать вектор развития фреймворка, а он пока четко не сформулирован. В проекте поучаствовало уже много контрибьюторов. Если вы тоже захотите что-то добавить во фреймворк, всегда пожалуйста, отправляйте pull request. Но есть костяк, ребята, кто сильно разбирается в Yew — порядка 30 человек.

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

Обязательный пункт в моих планах — создание большого количества документации о том, как писать приложения на Yew. Очевидно, что подход к разработке в данном случае отличается от того, что мы видели в React и Elm.

Все-таки создать фреймворк — не то же самое, что профессионально на нем писать. Мне иногда ребята показывают интересные кейсы, как можно использовать фреймворк. Практики использования фреймворка еще только формируются.

Освоение WebAssembly будет полезно каждому из нас, потому что создание очень сложных приложений — тот момент, которого мы уже давно ждем. Попробуйте, установите Rust, расширьте свои возможности как разработчика. Иными словами, WebAssembly — это не только про веб-браузер, а это вообще рантайм, который точно развивается и будет развиваться еще активнее.

Уже известная информация о программе — на сайте, и билеты можно приобрести там же. Если доклад понравился, обратите внимание: 24-25 ноября в Москве состоится новая HolyJS, и там тоже будет много интересного.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть