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

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

dcrowtesfnjfmpr7wl9j0suzjug.jpeg

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


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

Agenda


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

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

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

Что такое Yew?


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

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

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

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


9ef8d5b7ffb36029d2a4d840f9432980.png

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

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


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

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

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

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

Архитектура


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

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

type alias Model = 
    { value : Int 
    } 
  
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 и не хранить старую версию.

c98ff5d946992d46ba910e36375b4fc7.png

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

140a12d4106fb15b17204d547de29b82.png

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

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

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

Возможности


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

3ab5aaf257e0a6a9f611e885dba4bd8d.png

Я взял демонстрацию из другого проекта. Есть много проектов, которые идут по этому пути, но он быстро заводит в тупик. Ведь фреймворк — достаточно крупная разработка и приходится писать много стыковочного кода. Я стал использовать в 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 { 
    nav("nav", ("menu"), vec![ 
        button("button", (), ("onclick", || Msg::Clicked)), 
        tag("section", ("ontop"), vec![ 
            p("My text...") 
        ]) 
    ]) 
}


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

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

impl Renderable for Model { 
    fn view(&self) -> Html { 
        html! { 
            
                               

{ self.value }

               

{ Local::now() }

           
       }    } }


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

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


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

9b93d0d3797e46f73650b00689315009.png

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

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


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

9338314cb1560b2172c6ab11bbff7977.png

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

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


Из 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-дерево, я сначала решил, что реализовать такую функцию в моем фреймворке будет очень легко. Но попробовал эту опцию, и оказалось, что она не работает. Пришлось разбираться, ходить по этому дереву, смотреть, что там изменилось и т.п.

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

html! {
   <>
       { "Row" }
       { "Row" }
       { "Row" }
   
}

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


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

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


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

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

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

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


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

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


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

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

3f6477776d227c4b7abcf64be2f17f5c.png

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

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

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

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

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


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

829d8493d52ccff0299dc030661c5b37.png

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

02d08def74b01670bc6b4b0695f5cc2f.png

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

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

a508d6c7e4470c16f7890f36f0bd4135.png

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

63b4f97902e2460ced9cbbdd0f820f23.png

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

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


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

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


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

curl https://sh.rustup.rs -sSf | sh

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

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

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

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

cargo install cargo-web

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

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

cargo new --bin my-project

[package]
name = "my-project"
version = "0.1.0"
 
[dependencies]
yew = "0.3.0"

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

cargo web start --target wasm32-unknown-unknown

Я привел пример честного WebAssembly. Если вам нужно скомпилировать под emscripten (rust-компилятор может сам подключить emscripten), в самом последнем элементе unknown можно вставить слово emscripten, что позволит вам использовать больше крейтов. Не забывайте, что emscripten — это достаточно большой дополнительный обвес к вашему файлу. Поэтому лучше писать честный WebAssembly-код.

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


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

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

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

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

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

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


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

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


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

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


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

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


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

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


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

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


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

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

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


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

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

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

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

© Habrahabr.ru