Ruby on Rails ב-WebAssembly: תהליך מלא בדפדפן

Vladimir Dementyev
Vladimir Dementyev

תאריך פרסום: 31 בינואר 2025

נסו לדמיין איך מריצים בלוג פונקציונלי לגמרי בדפדפן – לא רק את הקצה הקדמי, אלא גם את הקצה העורפי. אין צורך בשרתים או בעננים – רק אתם, הדפדפן שלכם ו… WebAssembly! WebAssembly מאפשר להריץ מסגרות צד-שרת באופן מקומי, ומטשטש את הגבולות של פיתוח אינטרנט קלאסי ופותח אפשרויות חדשות ומלהיבות. בפוסט הזה, Vladimir Dementyev (מנהל הקצה העורפי ב-Evil Martians) משתף את ההתקדמות בעבודה על הפיכת Ruby on Rails לזמינה ל-Wasm ולדפדפנים:

  • איך להביא את Rails לדפדפן תוך 15 דקות.
  • מאחורי הקלעים של הפיכת קוד Rails ל-wasm.
  • העתיד של Rails ו-Wasm.

הקוד המפורסם של Ruby on Rails ליצירת בלוג תוך 15 דקות פועל עכשיו ישירות בדפדפן

Ruby on Rails היא מסגרת אינטרנט שמתמקדת בפרודוקטיביות של המפתחים ובשיפור מהירות הפיתוח. זו הטכנולוגיה שבה משתמשים גורמים מובילים בתחום, כמו GitHub ו-Shopify. הפופולריות של המסגרת התחילה לפני שנים רבות עם פרסום הסרטון המפורסם "איך יוצרים בלוג ב-15 דקות" שפורסם על ידי David Heinemeier Hansson (או 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. וגם Ruby לא יוצאת מן הכלל. ברור ש-Rails הוא יותר מ-Ruby, אבל לפני שנתעמק בהבדלים, נמשיך את הדגמה ונעביר את האפליקציה ל-Wasm (פועל שהגתה הספרייה wasmify-rails).

צריך רק להריץ כמה פקודות כדי לקמפל את אפליקציית הבלוג למודול Wasm ולהריץ אותו בדפדפן.

קודם כול, מתקינים את ספריית wasmify-rails באמצעות Bundler (ה-npm של Ruby) ומפעילים את ה-generator שלה באמצעות ה-CLI של Rails:

$ 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-ready.

בשלב הבא, יוצרים את מודול הליבה של 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 בפורמט wasm בדפדפן. לשם כך, יש גם פקודת גנרטור:

$ bin/rails wasmify:pwa

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

הפקודה הקודמת יוצרת אפליקציית PWA מינימלית שנוצרה באמצעות Vite. אפשר להשתמש בה באופן מקומי כדי לבדוק את מודול ה-Wasm המהדר של Rails, או לפרוס אותה באופן סטטי כדי להפיץ את האפליקציה.

עכשיו, עם מרכז האפליקציות, כל מה שצריך הוא לארוז את האפליקציה כולה לקובץ בינארי יחיד של 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 ב-Wasm

כדי להבין טוב יותר את האתגרים (והפתרונות) של אריזה של אפליקציה בצד השרת לתוך מודול Wasm, בהמשך המאמר נסביר על הרכיבים שחלק מהארכיטקטורה הזו.

אפליקציית אינטרנט תלויה בהרבה יותר דברים מאשר רק בשפת התכנות שבה כתבתם את קוד האפליקציה. צריך גם להעביר כל רכיב לסביבת הפריסה המקומית שלכם – הדפדפן. הדבר המעניין בהדגמה 'בלוג ב-15 דקות' הוא שאפשר לעשות זאת בלי לכתוב מחדש את קוד האפליקציה. אותו קוד שימש להרצת האפליקציה במצב קלאסי בצד השרת ובדפדפן.

הרכיבים שמרכיבים אפליקציה של Ruby on Rails: שרת אינטרנט, מסד נתונים, תור ואחסון. בנוסף לרכיבי הליבה של Ruby: הג'מ'ס, התוספים המקוריים, כלי המערכת ו-Ruby VM.

מסגרת, כמו Ruby on Rails, מספקת ממשק, אבסוקציה לתקשורת עם רכיבי התשתית. בקטע הבא נסביר איך אפשר להשתמש בארכיטקטורה של המסגרת כדי לענות על הצרכים הספציפיים של הצגת מודעות באופן מקומי.

הבסיס: ruby.wasm

בשנת 2022, Ruby הפכה באופן רשמי למוכנה ל-Wasm (מגרסה 3.2.0 ואילך). כלומר, אפשר לקמפל את קוד המקור של C ל-Wasm ולהשתמש במכונה וירטואלית של Ruby בכל מקום שרוצים. הפרויקט ruby.wasm כולל מודולים שנאספו מראש וקישור JavaScript כדי להריץ את Ruby בדפדפן (או בכל סביבת זמן ריצה אחרת של JavaScript). הפרויקט ruby:wasm כולל גם את כלי ה-build שמאפשרים ליצור גרסה מותאמת אישית של Ruby עם יחסי תלות נוספים. זה חשוב מאוד לפרויקטים שמסתמכים על ספריות עם תוספים ל-C. כן, אפשר גם לקמפל תוספים מקומיים ל-Wasm. (טוב, עדיין לא כל התוספים, אבל רובם).

נכון לעכשיו, Ruby תומכת באופן מלא בממשק המערכת של WebAssembly, WASI 0.1. WASI 0.2, שכולל את Component Model, כבר נמצא בגרסת אלפא ונמצא כמה שלבים לפני השלמה.ברגע שתהיה תמיכה ב-WASI 0.2, לא תצטרכו יותר לבצע הידור מחדש של כל השפה בכל פעם שתצטרכו להוסיף יחסי תלות מקומיים חדשים: תוכלו לפצל אותם לרכיבים.

כתוצאה מכך, מודל הרכיבים אמור לעזור גם בהקטנת גודל החבילה. מידע נוסף על הפיתוח של ruby.wasm ועל ההתקדמות שלו זמין בהרצאה What you can do with Ruby on WebAssembly.

כך נפתר החלק של Ruby במשוואת Wasm. אבל ל-Rails, כמסגרת אינטרנט, נדרשים כל הרכיבים שמוצגים בתרשים הקודם. בהמשך מוסבר איך להוסיף רכיבים אחרים לדפדפן ולקשר אותם יחד ב-Rails.

חיבור למסד נתונים שפועל בדפדפן

SQLite3 מגיע עם הפצה רשמית של Wasm ועם עטיפה של JavaScript תואמת, ולכן הוא מוכן להטמעה בדפדפן. אפשר למצוא את PostgreSQL for 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).

קובץ שירות (service worker) כשרת אינטרנט

רכיב חיוני נוסף בכל אפליקציית אינטרנט הוא שרת אינטרנט. משתמשים מקיימים אינטראקציה עם אפליקציות אינטרנט באמצעות בקשות HTTP. לכן, צריך למצוא דרך לנתב לבקרת Wasm בקשות HTTP שמופעל על ידי ניווט או שליחת טפסים. למרבה המזל, לדפדפן יש תשובה לבעיה הזו – שירותי עבודה.

קובץ שירות (service worker) הוא סוג מיוחד של Web Worker שמשמש כשרת proxy בין אפליקציית 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. מאחר שרוב האפליקציות לאינטרנט ב-Ruby משתמשות ב-Rack, ההטמעה הופכת לאוניוברסלית ולא ספציפית ל-Rails. עם זאת, ההטמעה בפועל ארוכה מדי לפרסום כאן.

קובץ שירות (service worker) הוא אחד מהרכיבים המרכזיים באפליקציית אינטרנט בדפדפן. הוא לא רק שרת proxy מסוג HTTP, אלא גם שכבת מטמון ומתג רשת (כלומר, אפשר ליצור אפליקציה מקומית או עם יכולת לעבוד במצב אופליין). זהו גם רכיב שיכול לעזור לכם להציג קבצים שהמשתמשים העלו.

שמירת הקבצים שהועלו בדפדפן

אחת מהתכונות הנוספות הראשונות שאפשר להטמיע באפליקציית הבלוג החדשה היא תמיכה בהעלאת קבצים, או באופן ספציפי יותר, צירוף תמונות לפוסטים. כדי לעשות זאת, צריך דרך לאחסון קבצים ולהצגתם.

ב-Rails, החלק של המסגרת שאחראי לטיפול בהעלאות קבצים נקרא Active Storage. Active Storage מספק למפתחים ממשקים והפשטות לעבודה עם קבצים בלי לחשוב על מנגנון האחסון ברמה הנמוכה. לא משנה איפה מאחסנים את הקבצים, בכונן הקשיח או בענן, קוד האפליקציה לא יודע על כך.

בדומה ל-Active Record, כדי לתמוך במנגנון אחסון מותאם אישית, כל מה שצריך הוא להטמיע מתאם שירות אחסון תואם. איפה שומרים קבצים בדפדפן?

האפשרות המסורתית היא להשתמש במסד נתונים. כן, אפשר לאחסן קבצים כ-blobs במסד הנתונים, בלי צורך ברכיבי תשתית נוספים. ויש כבר פלאגין מוכן לכך ב-Rails,‏ Active Storage Database. עם זאת, הצגת קבצים שמאוחסנים במסד נתונים דרך אפליקציית Rails שפועלת ב-WebAssembly היא לא אידיאלית, כי היא כוללת סבבים של סריאליזציה (או דה-סריאליזציה) שאינם בחינם.

פתרון טוב יותר שמותאם יותר לדפדפן הוא להשתמש ב-File System API ולעבד העלאות של קבצים וקבצים שהועלו על ידי השרת ישירות מה-Service Worker. OPFS (מערכת קבצים פרטית של מקור) היא תשתית מושלמת לכך. מדובר ב-API דפדפן חדש מאוד שישחק תפקיד חשוב באפליקציות העתידיות בדפדפן.

מה אפשר להשיג באמצעות שילוב של Rails ו-Wasm

סביר להניח ששאלתם את עצמכם את השאלה הזו כשהתחלתם לקרוא את המאמר: למה להריץ מסגרת צד-שרת בדפדפן? הרעיון של מסגרת או ספרייה בצד השרת (או בצד הלקוח) הוא רק תווית. קוד טוב, ובמיוחד הפשטה טובה, פועלים בכל מקום. התוויות לא אמורות למנוע מכם לבדוק אפשרויות חדשות ולחקור את הגבולות של המסגרת (לדוגמה, Ruby on Rails) וגם את הגבולות של סביבת זמן הריצה (WebAssembly). שני השירותים יכולים להפיק תועלת מתרחישי שימוש לא קונבנציונליים כאלה.

יש גם הרבה תרחישים לדוגמה רגילים או מעשיים.

ראשית, הוספת המסגרת לדפדפן פותחת הזדמנויות רבות ללמידה וליצירת אב טיפוס. נסו לדמיין איך זה יכול להיות לשחק עם ספריות, יישומי פלאגין ודפוסים ישירות בדפדפן, יחד עם אנשים אחרים. Stackblitz אפשרה לעשות זאת למסגרות של JavaScript. דוגמה נוספת היא WordPress Playground, שאפשרה לשחק עם עיצובים של WordPress בלי לצאת מדף האינטרנט. Wasm יכול לאפשר משהו דומה ל-Ruby ולסביבה העסקית שלה.

יש מקרה מיוחד של תכנות בדפדפן שמועיל במיוחד למפתחים של קוד פתוח – סיווג בעיות וניפוי באגים. שוב, StackBlitz מאפשרת לעשות זאת בפרויקטים של JavaScript: יוצרים סקריפט מינימלי לשחזור, מפנים לקישור בבעיה ב-GitHub ומחסכים למנהלי הפרויקט את הזמן הנדרש לשחזור התרחיש. למעשה, התהליך הזה כבר התחיל ב-Ruby בזכות הפרויקט RunRuby.dev (כאן אפשר למצוא בעיה לדוגמה שנפתרה באמצעות הדגימה בדפדפן).

תרחיש לדוגמה נוסף הוא אפליקציות עם יכולת לעבוד אופליין (או עם תמיכה במצב אופליין). אפליקציות שפועלות במצב אופליין, בדרך כלל באמצעות הרשת, אבל אפשר להשתמש בהן גם כשאין חיבור. לדוגמה, תוכנת אימייל שמאפשרת לחפש בתיבת הדואר הנכנס במצב אופליין. לחלופין, אפליקציית ספריית מוזיקה עם האפשרות 'אחסון במכשיר', כדי שהמוזיקה האהובה תמשיך לפעול גם אם אין חיבור לרשת. שתי הדוגמאות האלה תלויות בנתונים שמאוחסנים באופן מקומי, ולא רק בשימוש במטמון כמו ב-PWA קלאסיים.

לבסוף, כדאי גם לפתח אפליקציות מקומיות (או למחשב) באמצעות Rails, כי הפרודוקטיביות שהמסגרת מספקת לא תלויה בסביבת זמן הריצה. מסגרות עם תכונות מלאות מתאימות לפיתוח אפליקציות שמבוססות על נתונים אישיים ועל לוגיקה כבדה. אפשרות נוספת היא להשתמש ב-Wasm כפורמט להפצה ניידת.

זו רק ההתחלה של המסע שלנו עם Rails on Wasm. מידע נוסף על האתגרים והפתרונות זמין בספר האלקטרוני Ruby on Rails on WebAssembly (שגם הוא, דרך אגב, הוא אפליקציית Rails שפועלת אופליין).