Ruby on Rails на WebAssembly, полнофункциональное путешествие в браузере, Ruby on Rails на WebAssembly, полнофункциональное путешествие в браузере

Владимир Дементьев
Vladimir Dementyev

Опубликовано: 31 января 2025 г.

Представьте себе, что вы ведете полнофункциональный блог в своем браузере — не только интерфейс, но и серверную часть. Никаких серверов или облаков — только вы, ваш браузер и… WebAssembly ! Позволяя локально запускать серверные платформы, WebAssembly стирает границы классической веб-разработки и открывает новые захватывающие возможности. В этом посте Владимир Дементьев (руководитель бэкенда в Evil Martians ) делится прогрессом в подготовке Ruby on Rails к Wasm и браузеру:

  • Как перенести Rails в браузер за 15 минут.
  • За кулисами васмификации Rails.
  • Будущее Rails и Wasm.

Знаменитый «блог за 15 минут» Ruby on Rails теперь работает прямо в вашем браузере

Ruby on Rails — это веб-фреймворк, ориентированный на производительность разработчиков и быструю доставку. Эту технологию используют такие лидеры отрасли, как GitHub и Shopify . Популярность фреймворка началась много лет назад с выпуском знаменитого видео «Как создать блог за 15 минут», опубликованного Дэвидом Хайнемайером Ханссоном (или DHH). В 2005 году было невозможно представить создание полностью работающего веб-приложения за такое короткое время. Это было похоже на волшебство !

Сегодня я хотел бы вернуть это волшебное ощущение, создав приложение Rails, которое полностью работает в вашем браузере. Ваше путешествие начинается с обычного создания базового приложения Rails и его последующей упаковки для Wasm.

Предыстория: «блог за 15 минут» в командной строке

Предполагая, что на вашем компьютере установлены Ruby и Ruby on Rails , вы начинаете с создания нового приложения Ruby on Rails и создания некоторых функций (как в оригинальном видео «блог за 15 минут»):


$ rails new --css=tailwind web_dev_blog

  create  .ruby-version
  ...

$ cd web_dev_blog

$ bin/rails generate scaffold Post title:string date:date body:text

  create    db/migrate/20241217183624_create_posts.rb
  create    app/models/post.rb
  ...

$ bin/rails db:migrate

== 20241217183624 CreatePosts: migrating ====================
-- create_table(:posts)
   -> 0.0017s
== 20241217183624 CreatePosts: migrated (0.0018s) ===========

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

$ bin/dev

=> Booting Puma
=> Rails 8.0.1 application starting in development
...
* Listening on http://127.0.0.1:3000

Теперь вы можете открыть свой блог по адресу http://localhost:3000/posts и начать писать сообщения!

Блог Ruby on Rails, запускаемый из командной строки браузера.

У вас есть очень простое, но функциональное приложение для блога, созданное за считанные минуты. Это полнофункциональное приложение, управляемое сервером : у вас есть база данных ( SQLite ) для хранения ваших данных, веб-сервер для обработки HTTP-запросов ( Puma ) и программа Ruby для хранения вашей бизнес-логики, предоставления пользовательского интерфейса и обработки взаимодействия с пользователем. Наконец, есть тонкий слой JavaScript ( Turbo ), упрощающий работу в Интернете.

Официальная демо-версия Rails продолжает работу по развертыванию этого приложения на «голом железе» сервера и, таким образом, делает его готовым к использованию. Ваше путешествие продолжится в противоположном направлении: вместо того, чтобы размещать свое приложение где-то далеко, вы «развернете» его локально.

Следующий уровень: «блог за 15 минут» в Wasm

С появлением WebAssembly браузеры стали способны запускать не только код JavaScript, но и любой код, компилируемый в Wasm. И Руби не исключение. Конечно, Rails — это нечто большее, чем Ruby, но прежде чем углубляться в различия, давайте продолжим демонстрацию и васмифицируем (глагол, придуманный библиотекой wasmify-rails ) приложение Rails!

Вам нужно всего лишь выполнить несколько команд, чтобы скомпилировать приложение блога в модуль Wasm и запустить его в браузере.

Сначала вы устанавливаете библиотеку wasmify-rails с помощью Bundler ( npm Ruby) и запускаете ее генератор с помощью Rails CLI:

$ bundle add wasmify-rails

$ bin/rails wasmify:install

  create  config/wasmify.yml
  create  config/environments/wasm.rb
  ...
  info   The application is prepared for Wasm-ificaiton!

Команда wasmify:rails настраивает выделенную среду выполнения «wasm» (в дополнение к средам «разработки», «тестирования» и «производственной» по умолчанию) и устанавливает необходимые зависимости. Для нового приложения Rails этого достаточно, чтобы подготовить его к использованию Wasm.

Затем создайте основной модуль Wasm, содержащий среду выполнения Ruby, стандартную библиотеку и все зависимости приложения:

$ bin/rails wasmify:build

==> RubyWasm::BuildSource(3.3) -- Building
...
==> RubyWasm::CrossRubyProduct(ruby-3.3-wasm32-unknown-wasip1-full-4aaed4fbda7afe0bdf4e22167afd101e) -- done in 47.37s
INFO: Packaging gem: rake-13.2.1
...
INFO: Packaging gem: wasmify-rails-0.2.0
INFO: Packaging setup.rb: bundle/setup.rb
INFO: Size: 73.77 MB

Этот шаг может занять некоторое время: вам необходимо собрать Ruby из исходного кода, чтобы правильно связать собственные расширения (написанные на C) из сторонних библиотек. Этот (временный) недостаток рассматривается далее в статье.

Скомпилированный модуль Wasm — это всего лишь основа вашего приложения. Также необходимо упаковать сам код приложения и все ресурсы (например, изображения, CSS, JavaScript). Прежде чем приступать к упаковке, создайте базовое приложение запуска, которое можно было бы использовать для запуска обновленных Rails в браузере. Для этого также есть команда генератора:

$ bin/rails wasmify:pwa

  create  pwa
  create  pwa/boot.html
  create  pwa/boot.js
  ...
  prepend  config/wasmify.yml

Предыдущая команда создает минимальное приложение PWA, созданное с помощью Vite , которое можно использовать локально для тестирования скомпилированного модуля Rails Wasm или развернуть статически для распространения приложения.

Теперь, когда есть лаунчер, все, что вам нужно, это упаковать все приложение в один бинарный файл Wasm:

$ bin/rails wasmify:pack
...
Packed the application to pwa/app.wasm
Size: 76.2 MB

Вот и все! Запустите приложение запуска и увидите, что ваше приложение для ведения блогов Rails полностью работает в браузере:

$ cd pwa/

$ yarn dev

  VITE v4.5.5  ready in 290 ms

    Local:   http://localhost:5173/

Перейдите по адресу http://localhost:5173 , подождите немного, пока кнопка «Запуск» не станет активной, и нажмите ее — наслаждайтесь работой с приложением Rails, работающим локально в вашем браузере!

Блог Ruby on Rails, запускаемый из вкладки браузера, работающей в другой вкладке браузера.

Разве это не волшебство — запускать монолитное серверное приложение не только на вашем компьютере, но и в «песочнице» браузера? Для меня (хоть я и «колдун») это всё равно похоже на фантастику. Но здесь нет никакого волшебства, а только прогресс технологий.

Демо

Вы можете протестировать демонстрационную версию, встроенную в статью, или запустить ее в отдельном окне . Посмотрите исходный код на GitHub .

За кулисами Rails on Wasm

Чтобы лучше понять проблемы (и решения) упаковки серверного приложения в модуль Wasm, в оставшейся части статьи объясняются компоненты, являющиеся частью этой архитектуры.

Веб-приложение зависит не только от языка программирования, используемого для написания кода приложения, но и от многих других вещей. Каждый компонент также необходимо перенести в вашу локальную среду развертывания — браузер. Что интересно в демо-версии «блог за 15 минут», так это то, что этого можно добиться без переписывания кода приложения. Один и тот же код использовался для запуска приложения в классическом серверном режиме и в браузере.

Компоненты, составляющие приложение Ruby on Rails: веб-сервер, база данных, очередь и хранилище. Плюс основные компоненты Ruby: драгоценные камни, собственные расширения, системные инструменты и виртуальная машина Ruby.

Фреймворк, такой как Ruby on Rails, предоставляет вам интерфейс, абстракцию для взаимодействия с компонентами инфраструктуры. В следующем разделе обсуждается, как можно использовать архитектуру платформы для удовлетворения несколько эзотерических локальных потребностей.

Основа: Ruby.wasm

Ruby стал официально готов к использованию Wasm в 2022 году (начиная с версии 3.2.0), а это означает, что исходный код C можно было скомпилировать в Wasm и использовать виртуальную машину Ruby где угодно. Проект Ruby.wasm включает предварительно скомпилированные модули и привязки JavaScript для запуска Ruby в браузере (или любой другой среде выполнения JavaScript). Проект Ruby:wasm также поставляется с инструментами сборки, которые позволяют вам создавать собственную версию Ruby с дополнительными зависимостями — это очень важно для проектов, использующих библиотеки с расширениями C. Да, вы также можете скомпилировать собственные расширения в Wasm! (Ну, пока не какие-либо расширения, но большинство из них).

В настоящее время Ruby полностью поддерживает системный интерфейс WebAssembly, WASI 0.1 . WASI 0.2, который включает в себя модель компонентов , уже находится в альфа-состоянии и находится в нескольких шагах от завершения. Как только WASI 0.2 будет поддерживаться, это устранит текущую необходимость перекомпиляции всего языка каждый раз, когда вам нужно добавить новые собственные зависимости: они могут быть компонентизованы.

В качестве побочного эффекта модель компонентов также должна помочь уменьшить размер пакета. Вы можете узнать больше о разработке и прогрессе в Ruby.wasm из доклада «Что вы можете сделать с Ruby в WebAssembly» .

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

Подключиться к базе данных, работающей в браузере

SQLite3 поставляется с официальным дистрибутивом Wasm и соответствующей оболочкой JavaScript , поэтому его можно встроить в браузер. PostgreSQL для Wasm доступен через проект PGlite . Поэтому вам нужно только разобраться, как подключиться к внутрибраузерной базе данных из приложения Rails on Wasm.

Компонент или подфреймворк Rails, отвечающий за моделирование данных и взаимодействие с базой данных, называется Active Record (да, назван в честь шаблона проектирования ORM ). Active Record абстрагирует фактическую реализацию базы данных, использующую SQL, от кода приложения через адаптеры базы данных. В стандартной комплектации Rails предоставляет адаптеры SQLite3, PostgreSQL и MySQL. Однако все они предполагают подключение к реальным базам данных, доступным по сети. Чтобы преодолеть эту проблему, вы можете написать свои собственные адаптеры для подключения к локальным базам данных в браузере!

Вот как создаются адаптеры SQLite3 Wasm и PGlite, реализованные в рамках проекта Wasmify Rails:

  • Класс адаптера наследуется от соответствующего встроенного адаптера (например, class PGliteAdapter < PostgreSQLAdapter ), поэтому вы можете повторно использовать фактическую логику подготовки запроса и анализа результатов.
  • Вместо низкоуровневого подключения к базе данных вы используете объект внешнего интерфейса , который находится в среде выполнения JavaScript — мост между модулем Rails Wasm и базой данных.

Например, вот реализация моста для SQLite3 Wasm:

export function registerSQLiteWasmInterface(worker, db, opts = {}) {
  const name = opts.name || "sqliteForRails";

  worker[name] = {
    exec: function (sql) {
      let cols = [];
      let rows = db.exec(sql, { columnNames: cols, returnValue: "resultRows" });

      return {
        cols,
        rows,
      };
    },

    changes: function () {
      return db.changes();
    },
  };
}

С точки зрения приложения переход от реальной базы данных к базе данных в браузере — это всего лишь вопрос конфигурации:

# config/database.yml
development:
  adapter: sqlite3

production:
  adapter: sqlite3

wasm:
  adapter: sqlite3_wasm
  js_interface: "sqliteForRails"

Работа с локальной базой данных не требует больших усилий. Однако если требуется синхронизация данных с каким-то центральным источником истины, вы можете столкнуться с проблемой более высокого уровня. Этот вопрос выходит за рамки данного поста (подсказка: посмотрите демо-версию Rails on PGlite и ElectricSQL ).

Сервисный работник как веб-сервер

Еще одним важным компонентом любого веб-приложения является веб-сервер. Пользователи взаимодействуют с веб-приложениями с помощью HTTP-запросов. Таким образом, вам нужен способ маршрутизации HTTP-запросов, вызванных навигацией или отправкой форм, в ваш модуль Wasm. К счастью, у браузера есть ответ на этот вопрос — сервис-воркеры .

Сервис-воркер — это особый вид веб-воркера, который действует как прокси-сервер между приложением JavaScript и сетью. Он может перехватывать запросы и манипулировать ими, например: обслуживать кэшированные данные, перенаправлять на другие URL-адреса или… на модули Wasm! Вот набросок службы, обслуживающей запросы с использованием приложения Rails, работающего в Wasm:

// The vm variable holds a reference to the Wasm module with a
// Ruby VM initialized
let vm;
// The db variable holds a reference to the in-browser
// database interface
let db;

const initVM = async (progress, opts = {}) => {
  if (vm) return vm;
  if (!db) {
    await initDB(progress);
  }
  vm = await initRailsVM("/app.wasm");
  return vm;
};

const rackHandler = new RackHandler(initVM});

self.addEventListener("fetch", (event) => {
  // ...
  return event.respondWith(
    rackHandler.handle(event.request)
  );
});

«Извлечение» запускается каждый раз, когда браузер делает запрос. Вы можете получить информацию о запросе (URL, заголовки HTTP, тело) и создать свой собственный объект запроса.

Rails, как и большинство веб-приложений Ruby, использует интерфейс Rack для работы с HTTP-запросами. Интерфейс Rack описывает формат объектов запроса и ответа, а также интерфейс базового обработчика HTTP (приложения). Вы можете выразить эти свойства следующим образом:

request = {
   "REQUEST_METHOD" => "GET",
   "SCRIPT_NAME"    => "",
   "SERVER_NAME"  => "localhost",
   "SERVER_PORT" => "3000",
   "PATH_INFO"      => "/posts"
}

handler = proc do |env|
  [
    200,
    {"Content-Type" => "text/html"},
    ["<!doctype html><html><body>Hello Web!</body></html>"]
  ]
end

handler.call(request) #=> [200, {...}, [...]]

Если формат запроса вам знаком, то вы, вероятно, когда-то работали с CGI .

Объект JavaScript RackHandler отвечает за преобразование запросов и ответов между областями JavaScript и Ruby. Учитывая, что Rack используется большинством веб-приложений Ruby, реализация становится универсальной, а не специфичной для Rails. Однако фактическая реализация слишком длинная, чтобы публиковать ее здесь.

Сервис-воркер — одна из ключевых составляющих браузерного веб-приложения. Это не только HTTP-прокси, но также уровень кэширования и сетевой коммутатор (то есть вы можете создать локальное или автономное приложение). Это также компонент, который может помочь вам обслуживать файлы, загруженные пользователем.

Сохраняйте загрузку файлов в браузере

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

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

Как и в случае с Active Record, для поддержки пользовательского механизма хранения все, что вам нужно, — это реализовать соответствующий адаптер службы хранения . Где хранить файлы в браузере?

Традиционный вариант — использовать базу данных. Да, вы можете хранить файлы в виде больших двоичных объектов в базе данных, никаких дополнительных компонентов инфраструктуры не требуется. И для этого в Rails уже есть готовый плагин Active Storage Database . Однако обслуживание файлов, хранящихся в базе данных, через приложение Rails, работающее в WebAssembly, не является идеальным, поскольку оно включает в себя этапы (де)сериализации, которые не являются бесплатными.

Лучшим и более оптимизированным для браузера решением было бы использовать API-интерфейсы файловой системы и обрабатывать загрузку файлов и файлы, загруженные на сервер, непосредственно из сервис-воркера. Идеальным кандидатом для такой инфраструктуры является OPFS (исходная частная файловая система), новейший API браузера, который определенно сыграет важную роль для будущих браузерных приложений.

Чего Rails и Wasm могут достичь вместе

Я почти уверен, что вы задавали себе этот вопрос, когда начали читать статью: зачем запускать серверную среду в браузере? Идея о том, что фреймворк или библиотека находятся на стороне сервера (или клиента), — это всего лишь ярлык. Хороший код и особенно хорошая абстракция работают везде. Ярлыки не должны мешать вам исследовать новые возможности и расширять границы фреймворка (например, Ruby on Rails), а также границы среды выполнения (WebAssembly). Оба могут извлечь выгоду из таких нетрадиционных вариантов использования.

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

Во-первых, перенос фреймворка в браузер открывает огромные возможности для обучения и создания прототипов . Представьте себе, что вы можете играть с библиотеками, плагинами и шаблонами прямо в браузере и вместе с другими людьми. Stackblitz сделал это возможным для фреймворков JavaScript. Другой пример — игровая площадка WordPress , которая позволяет играть с темами WordPress, не покидая веб-страницу. Wasm мог бы сделать нечто подобное для Ruby и его экосистемы.

Есть особый случай написания кода в браузере, особенно полезный для разработчиков открытого исходного кода — проблемы сортировки и отладки . Опять же, StackBlitz сделал это возможным для проектов JavaScript: вы создаете минимальный сценарий воспроизведения, указываете ссылку в выпуске GitHub и экономите время сопровождающих на воспроизведении вашего сценария. И, кстати, это уже начало происходить в Ruby благодаря проекту RunRuby.dev (вот пример решения проблемы с воспроизведением в браузере).

Другой вариант использования — приложения, работающие в автономном режиме (или поддерживающие автономный режим). Автономные приложения, которые обычно работают через сеть, но при отсутствии соединения остаются работоспособными. Например, почтовый клиент, который позволяет выполнять поиск в папке «Входящие» в автономном режиме. Или приложение музыкальной библиотеки с возможностью «Сохранить на устройстве», чтобы ваша любимая музыка продолжала звучать даже при отсутствии подключения к сети. Оба примера зависят от данных, хранящихся локально , а не только от использования кеша, как в классических PWA.

Наконец, создание локальных (или настольных) приложений с помощью Rails также имеет смысл, поскольку производительность, которую дает вам фреймворк, не зависит от среды выполнения. Полнофункциональные платформы хорошо подходят для создания приложений с большим объемом персональных данных и логики. И использование Wasm в качестве переносимого формата распространения также является жизнеспособным вариантом.

Это только начало пути Rails on Wasm. Вы можете узнать больше о проблемах и решениях в электронной книге Ruby on Rails on WebAssembly (которая, кстати, сама по себе является автономным приложением Rails).

,

Владимир Дементьев
Vladimir Dementyev

Опубликовано: 31 января 2025 г.

Представьте себе, что вы ведете полнофункциональный блог в своем браузере — не только интерфейс, но и серверную часть. Никаких серверов или облаков — только вы, ваш браузер и… WebAssembly ! Позволяя локально запускать серверные платформы, WebAssembly стирает границы классической веб-разработки и открывает новые захватывающие возможности. В этом посте Владимир Дементьев (руководитель отдела бэкенда в Evil Martians ) делится прогрессом в подготовке Ruby on Rails к Wasm- и браузерному использованию:

  • Как перенести Rails в браузер за 15 минут.
  • За кулисами васмификации Rails.
  • Будущее Rails и Wasm.

Знаменитый «блог за 15 минут» Ruby on Rails теперь работает прямо в вашем браузере

Ruby on Rails — это веб-фреймворк, ориентированный на производительность разработчиков и быструю доставку. Эту технологию используют такие лидеры отрасли, как GitHub и Shopify . Популярность фреймворка началась много лет назад с выпуском знаменитого видео «Как создать блог за 15 минут», опубликованного Дэвидом Хайнемайером Ханссоном (или DHH). В 2005 году было невозможно представить создание полностью работающего веб-приложения за такое короткое время. Это было похоже на волшебство !

Сегодня я хотел бы вернуть это волшебное ощущение, создав приложение Rails, которое полностью работает в вашем браузере. Ваше путешествие начинается с обычного создания базового приложения Rails и его последующей упаковки для Wasm.

Предыстория: «блог за 15 минут» в командной строке

Предполагая, что на вашем компьютере установлены Ruby и Ruby on Rails , вы начинаете с создания нового приложения Ruby on Rails и создания некоторых функций (как в оригинальном видео «блог за 15 минут»):


$ rails new --css=tailwind web_dev_blog

  create  .ruby-version
  ...

$ cd web_dev_blog

$ bin/rails generate scaffold Post title:string date:date body:text

  create    db/migrate/20241217183624_create_posts.rb
  create    app/models/post.rb
  ...

$ bin/rails db:migrate

== 20241217183624 CreatePosts: migrating ====================
-- create_table(:posts)
   -> 0.0017s
== 20241217183624 CreatePosts: migrated (0.0018s) ===========

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

$ bin/dev

=> Booting Puma
=> Rails 8.0.1 application starting in development
...
* Listening on http://127.0.0.1:3000

Теперь вы можете открыть свой блог по адресу http://localhost:3000/posts и начать писать сообщения!

Блог Ruby on Rails, запускаемый из командной строки браузера.

У вас есть очень простое, но функциональное приложение для блога, созданное за считанные минуты. Это полнофункциональное приложение, управляемое сервером : у вас есть база данных ( SQLite ) для хранения ваших данных, веб-сервер для обработки HTTP-запросов ( Puma ) и программа Ruby для хранения вашей бизнес-логики, предоставления пользовательского интерфейса и обработки взаимодействия с пользователем. Наконец, есть тонкий слой JavaScript ( Turbo ), упрощающий работу в Интернете.

Официальная демо-версия Rails продолжает работу по развертыванию этого приложения на «голом железе» сервера и, таким образом, делает его готовым к использованию. Ваше путешествие продолжится в противоположном направлении: вместо того, чтобы размещать свое приложение где-то далеко, вы «развернете» его локально.

Следующий уровень: «блог за 15 минут» в Wasm

С появлением WebAssembly браузеры стали способны запускать не только код JavaScript, но и любой код, компилируемый в Wasm. И Руби не исключение. Конечно, Rails — это нечто большее, чем Ruby, но прежде чем углубляться в различия, давайте продолжим демонстрацию и васмифицируем (глагол, придуманный библиотекой wasmify-rails ) приложение Rails!

Вам нужно всего лишь выполнить несколько команд, чтобы скомпилировать приложение блога в модуль Wasm и запустить его в браузере.

Сначала вы устанавливаете библиотеку wasmify-rails с помощью Bundler ( npm Ruby) и запускаете ее генератор с помощью Rails CLI:

$ bundle add wasmify-rails

$ bin/rails wasmify:install

  create  config/wasmify.yml
  create  config/environments/wasm.rb
  ...
  info   The application is prepared for Wasm-ificaiton!

Команда wasmify:rails настраивает выделенную среду выполнения «wasm» (в дополнение к средам «разработки», «тестирования» и «производственной» по умолчанию) и устанавливает необходимые зависимости. Для нового приложения Rails этого достаточно, чтобы подготовить его к использованию Wasm.

Затем создайте основной модуль Wasm, содержащий среду выполнения Ruby, стандартную библиотеку и все зависимости приложения:

$ bin/rails wasmify:build

==> RubyWasm::BuildSource(3.3) -- Building
...
==> RubyWasm::CrossRubyProduct(ruby-3.3-wasm32-unknown-wasip1-full-4aaed4fbda7afe0bdf4e22167afd101e) -- done in 47.37s
INFO: Packaging gem: rake-13.2.1
...
INFO: Packaging gem: wasmify-rails-0.2.0
INFO: Packaging setup.rb: bundle/setup.rb
INFO: Size: 73.77 MB

Этот шаг может занять некоторое время: вам необходимо собрать Ruby из исходного кода, чтобы правильно связать собственные расширения (написанные на C) из сторонних библиотек. Этот (временный) недостаток рассматривается далее в статье.

Скомпилированный модуль Wasm — это всего лишь основа вашего приложения. Также необходимо упаковать сам код приложения и все ресурсы (например, изображения, CSS, JavaScript). Прежде чем приступать к упаковке, создайте базовое приложение запуска, которое можно было бы использовать для запуска обновленных Rails в браузере. Для этого также есть команда генератора:

$ bin/rails wasmify:pwa

  create  pwa
  create  pwa/boot.html
  create  pwa/boot.js
  ...
  prepend  config/wasmify.yml

Предыдущая команда создает минимальное приложение PWA, созданное с помощью Vite , которое можно использовать локально для тестирования скомпилированного модуля Rails Wasm или развернуть статически для распространения приложения.

Теперь, когда есть лаунчер, все, что вам нужно, это упаковать все приложение в один бинарный файл Wasm:

$ bin/rails wasmify:pack
...
Packed the application to pwa/app.wasm
Size: 76.2 MB

Вот и все! Запустите приложение запуска и увидите, что ваше приложение для ведения блогов Rails полностью работает в браузере:

$ cd pwa/

$ yarn dev

  VITE v4.5.5  ready in 290 ms

    Local:   http://localhost:5173/

Перейдите по адресу http://localhost:5173 , подождите немного, пока кнопка «Запуск» не станет активной, и нажмите ее — наслаждайтесь работой с приложением Rails, работающим локально в вашем браузере!

Блог Ruby on Rails, запускаемый из вкладки браузера, работающей в другой вкладке браузера.

Разве это не волшебство — запускать монолитное серверное приложение не только на вашем компьютере, но и в «песочнице» браузера? Для меня (хоть я и «колдун») это всё равно похоже на фантастику. Но здесь нет никакого волшебства, а только прогресс технологий.

Демо

Вы можете протестировать демонстрационную версию, включенную в статью, или запустить ее в отдельном окне . Посмотрите исходный код на GitHub .

За кулисами Rails on Wasm

Чтобы лучше понять проблемы (и решения) упаковки серверного приложения в модуль Wasm, в оставшейся части статьи объясняются компоненты, являющиеся частью этой архитектуры.

Веб-приложение зависит не только от языка программирования, используемого для написания кода приложения, но и от многих других вещей. Каждый компонент также необходимо перенести в вашу локальную среду развертывания — браузер. Что интересно в демо-версии «блог за 15 минут», так это то, что этого можно добиться без переписывания кода приложения. Один и тот же код использовался для запуска приложения в классическом серверном режиме и в браузере.

Компоненты, составляющие приложение Ruby on Rails: веб-сервер, база данных, очередь и хранилище. Плюс основные компоненты Ruby: драгоценные камни, собственные расширения, системные инструменты и виртуальная машина Ruby.

Фреймворк, такой как Ruby on Rails, предоставляет вам интерфейс, абстракцию для взаимодействия с компонентами инфраструктуры. В следующем разделе обсуждается, как можно использовать архитектуру платформы для удовлетворения несколько эзотерических локальных потребностей.

Основа: Ruby.wasm

Ruby стал официально готов к использованию Wasm в 2022 году (начиная с версии 3.2.0), а это означает, что исходный код C можно было скомпилировать в Wasm и использовать виртуальную машину Ruby где угодно. Проект Ruby.wasm включает предварительно скомпилированные модули и привязки JavaScript для запуска Ruby в браузере (или любой другой среде выполнения JavaScript). Проект Ruby:wasm также поставляется с инструментами сборки, которые позволяют вам создавать собственную версию Ruby с дополнительными зависимостями — это очень важно для проектов, использующих библиотеки с расширениями C. Да, вы также можете скомпилировать собственные расширения в Wasm! (Ну, пока не какие-либо расширения, но большинство из них).

В настоящее время Ruby полностью поддерживает системный интерфейс WebAssembly, WASI 0.1 . WASI 0.2, который включает в себя модель компонентов , уже находится в альфа-состоянии и находится в нескольких шагах от завершения. Как только WASI 0.2 будет поддерживаться, это устранит текущую необходимость перекомпиляции всего языка каждый раз, когда вам нужно добавить новые собственные зависимости: они могут быть компонентизированы.

В качестве побочного эффекта модель компонентов также должна помочь уменьшить размер пакета. Вы можете узнать больше о разработке и прогрессе в Ruby.wasm из доклада «Что вы можете сделать с Ruby в WebAssembly» .

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

Подключиться к базе данных, работающей в браузере

SQLite3 поставляется с официальным дистрибутивом Wasm и соответствующей оболочкой JavaScript , поэтому его можно встроить в браузер. PostgreSQL для Wasm доступен через проект PGlite . Поэтому вам нужно только разобраться, как подключиться к внутрибраузерной базе данных из приложения Rails on Wasm.

Компонент или подфреймворк Rails, отвечающий за моделирование данных и взаимодействие с базой данных, называется Active Record (да, назван в честь шаблона проектирования ORM ). Active Record абстрагирует фактическую реализацию базы данных, использующую SQL, от кода приложения через адаптеры базы данных. В стандартной комплектации Rails предоставляет адаптеры SQLite3, PostgreSQL и MySQL. Однако все они предполагают подключение к реальным базам данных, доступным по сети. Чтобы преодолеть эту проблему, вы можете написать свои собственные адаптеры для подключения к локальным базам данных в браузере!

Вот как создаются адаптеры SQLite3 Wasm и PGlite, реализованные в рамках проекта Wasmify Rails:

  • Класс адаптера наследуется от соответствующего встроенного адаптера (например, class PGliteAdapter < PostgreSQLAdapter ), поэтому вы можете повторно использовать фактическую логику подготовки запроса и анализа результатов.
  • Вместо низкоуровневого подключения к базе данных вы используете объект внешнего интерфейса , который находится в среде выполнения JavaScript — мост между модулем Rails Wasm и базой данных.

Например, вот реализация моста для SQLite3 Wasm:

export function registerSQLiteWasmInterface(worker, db, opts = {}) {
  const name = opts.name || "sqliteForRails";

  worker[name] = {
    exec: function (sql) {
      let cols = [];
      let rows = db.exec(sql, { columnNames: cols, returnValue: "resultRows" });

      return {
        cols,
        rows,
      };
    },

    changes: function () {
      return db.changes();
    },
  };
}

С точки зрения приложения переход от реальной базы данных к базе данных в браузере — это всего лишь вопрос конфигурации:

# config/database.yml
development:
  adapter: sqlite3

production:
  adapter: sqlite3

wasm:
  adapter: sqlite3_wasm
  js_interface: "sqliteForRails"

Работа с локальной базой данных не требует больших усилий. Однако если требуется синхронизация данных с каким-то центральным источником истины, вы можете столкнуться с проблемой более высокого уровня. Этот вопрос выходит за рамки данного поста (подсказка: посмотрите демо-версию Rails on PGlite и ElectricSQL ).

Сервисный работник как веб-сервер

Еще одним важным компонентом любого веб-приложения является веб-сервер. Пользователи взаимодействуют с веб-приложениями с помощью HTTP-запросов. Таким образом, вам нужен способ маршрутизации HTTP-запросов, вызванных навигацией или отправкой форм, в ваш модуль Wasm. К счастью, у браузера есть ответ на этот вопрос — сервис-воркеры .

Сервис-воркер — это особый вид веб-воркера, который действует как прокси-сервер между приложением JavaScript и сетью. Он может перехватывать запросы и манипулировать ими, например: обслуживать кэшированные данные, перенаправлять на другие URL-адреса или… на модули Wasm! Вот набросок службы, обслуживающей запросы с использованием приложения Rails, работающего в Wasm:

// The vm variable holds a reference to the Wasm module with a
// Ruby VM initialized
let vm;
// The db variable holds a reference to the in-browser
// database interface
let db;

const initVM = async (progress, opts = {}) => {
  if (vm) return vm;
  if (!db) {
    await initDB(progress);
  }
  vm = await initRailsVM("/app.wasm");
  return vm;
};

const rackHandler = new RackHandler(initVM});

self.addEventListener("fetch", (event) => {
  // ...
  return event.respondWith(
    rackHandler.handle(event.request)
  );
});

«Извлечение» запускается каждый раз, когда браузер делает запрос. Вы можете получить информацию о запросе (URL, заголовки HTTP, тело) и создать свой собственный объект запроса.

Rails, как и большинство веб-приложений Ruby, использует интерфейс Rack для работы с HTTP-запросами. Интерфейс Rack описывает формат объектов запроса и ответа, а также интерфейс базового обработчика HTTP (приложения). Вы можете выразить эти свойства следующим образом:

request = {
   "REQUEST_METHOD" => "GET",
   "SCRIPT_NAME"    => "",
   "SERVER_NAME"  => "localhost",
   "SERVER_PORT" => "3000",
   "PATH_INFO"      => "/posts"
}

handler = proc do |env|
  [
    200,
    {"Content-Type" => "text/html"},
    ["<!doctype html><html><body>Hello Web!</body></html>"]
  ]
end

handler.call(request) #=> [200, {...}, [...]]

Если формат запроса вам знаком, то вы, вероятно, когда-то работали с CGI .

Объект RackHandler JavaScript отвечает за преобразование запросов и ответов между областями JavaScript и Ruby. Учитывая, что Rack используется большинством веб-приложений Ruby, реализация становится универсальной, а не специфичной для Rails. Однако фактическая реализация слишком длинная, чтобы публиковать ее здесь.

Сервис-воркер — одна из ключевых составляющих браузерного веб-приложения. Это не только HTTP-прокси, но также уровень кэширования и сетевой коммутатор (то есть вы можете создать локальное или автономное приложение). Это также компонент, который может помочь вам обслуживать файлы, загруженные пользователем.

Сохраняйте загрузку файлов в браузере

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

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

Как и в случае с Active Record, для поддержки пользовательского механизма хранения все, что вам нужно, — это реализовать соответствующий адаптер службы хранения . Где хранить файлы в браузере?

Традиционный вариант — использовать базу данных. Да, вы можете хранить файлы в виде больших двоичных объектов в базе данных, никаких дополнительных компонентов инфраструктуры не требуется. И для этого в Rails уже есть готовый плагин Active Storage Database . Однако обслуживание файлов, хранящихся в базе данных, через приложение Rails, работающее в WebAssembly, не является идеальным, поскольку оно включает в себя этапы (де)сериализации, которые не являются бесплатными.

Лучшим и более оптимизированным для браузера решением было бы использовать API-интерфейсы файловой системы и обрабатывать загрузку файлов и файлы, загруженные на сервер, непосредственно из сервис-воркера. Идеальным кандидатом для такой инфраструктуры является OPFS (исходная частная файловая система), новейший API браузера, который определенно сыграет важную роль для будущих браузерных приложений.

Чего Rails и Wasm могут достичь вместе

Я почти уверен, что вы задавали себе этот вопрос, когда начали читать статью: зачем запускать серверную среду в браузере? Идея о том, что фреймворк или библиотека находятся на стороне сервера (или клиента), — это всего лишь ярлык. Хороший код и особенно хорошая абстракция работают везде. Ярлыки не должны мешать вам исследовать новые возможности и расширять границы фреймворка (например, Ruby on Rails), а также границы среды выполнения (WebAssembly). Оба могут извлечь выгоду из таких нетрадиционных вариантов использования.

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

Во-первых, перенос фреймворка в браузер открывает огромные возможности для обучения и создания прототипов . Представьте себе, что вы можете играть с библиотеками, плагинами и шаблонами прямо в браузере и вместе с другими людьми. Stackblitz сделал это возможным для фреймворков JavaScript. Другой пример — игровая площадка WordPress , которая позволяет играть с темами WordPress, не покидая веб-страницу. Wasm мог бы сделать нечто подобное для Ruby и его экосистемы.

Есть особый случай написания кода в браузере, который особенно полезен разработчикам с открытым исходным кодом — проблемы сортировки и отладки . Опять же, StackBlitz сделал это возможным для проектов JavaScript: вы создаете минимальный сценарий воспроизведения, указываете ссылку в выпуске GitHub и экономите время сопровождающих на воспроизведении вашего сценария. И, кстати, это уже начало происходить в Ruby благодаря проекту RunRuby.dev (вот пример решения проблемы с воспроизведением в браузере).

Другой вариант использования — приложения, работающие в автономном режиме (или поддерживающие автономный режим). Автономные приложения, которые обычно работают через сеть, но при отсутствии соединения остаются работоспособными. Например, почтовый клиент, который позволяет выполнять поиск в папке «Входящие» в автономном режиме. Или приложение музыкальной библиотеки с возможностью «Сохранить на устройстве», чтобы ваша любимая музыка продолжала звучать даже при отсутствии подключения к сети. Оба примера зависят от данных, хранящихся локально , а не только от использования кеша, как в классических PWA.

Наконец, создание локальных (или настольных) приложений с помощью Rails также имеет смысл, поскольку производительность, которую дает вам фреймворк, не зависит от среды выполнения. Полнофункциональные платформы хорошо подходят для создания приложений с большим объемом персональных данных и логики. И использование WASM в качестве портативного формата распределения также является жизнеспособным вариантом.

Это только начало этих рельсов на Wasm Journey. Вы можете узнать больше о проблемах и решениях в электронной книге Ruby On Rails On Webassembly (которая, кстати, является самостоятельным применением Rails).