מבנה פרויקט Rails, React ו TypeScript

08/06/2024

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

1. ריילס

אני מתחיל ביצירת פרויקט ריילס 7 חדש עם:

$ rails new  --skip-javascript .

למרות שיש בריילס מנגנון לעבודה עם JavaScript (אפילו שניים), אני מעדיף את vite ולכן אני מתקין את הג'ם vite-ruby לפי הוראות ההתקנה שלהם. מתוך תיקיית הפרויקט אני כותב:

bundle add 'vite_rails'
bundle exec vite install

ואז אפשר לנסות להפעיל את שרת הפיתוח:

$ ./bin/vite dev

ואצלי מופיע הפלט הבא:

The CJS build of Vite's Node API is deprecated. See https://vitejs.dev/guide/troubleshooting.html#vite-cjs-node-api-deprecated for more details.

  VITE v5.2.13  ready in 242 ms

  ➜  Local:   http://localhost:3036/vite-dev/
  ➜  press h + enter to show help
^C

אני עוצר אותו עם Ctrl+C כדי לתקן כמה קונפיגורציות. מריצים:

$ yarn add vite-plugin-rails typescript react react-dom @vitejs/plugin-react @types/react @types/react-dom

ואז משנים את הקובץ vite.config.ts לתוכן הבא:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import ViteRails from 'vite-plugin-rails'

export default defineConfig({
  plugins: [
    react(),
    ViteRails(),
  ],
})

ממשיכים בשורת הפקודה ויוצרים את הקונטרולר הראשון:

$ ./bin/rails g controller HomeController index

ואז בקובץ config/routes.rb אני מוסיף הפניה אליו כדי שנוכל לראות אותו בדפדפן:

Rails.application.routes.draw do
  root to: 'home#index'
end

מפעילים את השרת עם ./bin/rails s, נכנסים ל localhost:3000 ורואים את הדף שיצרנו.

2. ריאקט

קובץ ה JavaScript שנטען כשהיישום שלנו עולה הוא app/javascript/entrypoints/application.js. אני יוצר תיקייה חדשה בשם app/frontend/components ובתוכה קובץ חדש בשם Home.tsx. תוכן הקובץ הוא:

import React from 'react';
import { createRoot } from 'react-dom/client';

function Home({text}: {
  text: string
}) {
  return <h1>{text}</h1>
}

const main = document.querySelector('main')!;
const root = createRoot(main);

root.render(<Home text="Hello World" />);

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

לפני שנוכל להתקדם יש לעדכן את הקובץ app/views/layouts/application.html כך שיוכל לרענן דפי vite וגם יכיל אלמנט main אליו נרנדר את הקומפוננטה שיצרנו. עדכנו את תוכן הקובץ לקוד הבא:

<!DOCTYPE html>
<html>
  <head>
    <title>RailsReactTypescriptDemo</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">

    <% if Rails.env.development? %>
    <script type="module">
      import RefreshRuntime from 'http://localhost:3036/vite-dev/@react-refresh'
      RefreshRuntime.injectIntoGlobalHook(window)
      window.$RefreshReg$ = () => {}
      window.$RefreshSig$ = () => (type) => type
      window.__vite_plugin_react_preamble_installed__ = true
    </script>
    <% end %>

    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag "application" %>
    <%= vite_client_tag %>
    <%= vite_javascript_tag 'application' %>
    <!--
      If using a TypeScript entrypoint file:
        vite_typescript_tag 'application'

      If using a .jsx or .tsx entrypoint, add the extension:
        vite_javascript_tag 'application.jsx'

      Visit the guide for more information: https://vite-ruby.netlify.app/guide/rails
    -->

  </head>

  <body>
    <main />
    <%= yield %>

  </body>
</html>

עכשיו הפעילו מחדש את השרת בשני חלונות - בחלון אחד נפעיל ./bin/vite dev ובחלון שני ./bin/rails s. אם הכל הלך כמו שצריך תוכלו להיכנס ל localhost:3000 ולראות את הקומפוננטה ובנוסף תוכלו לשנות את קוד הקומפוננטה, לשמור ולראות את העמוד בדפדפן מתעדכן.

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

אני משנה את שם הקובץ הראשי מ application.js ל application.jsx וכותב בו את התוכן הבא:

// file: app/frontend/entrypoints/application.jsx

import React from 'react';
import { createRoot } from 'react-dom/client';

async function renderAll() {
  const reactEntrypoints = document.querySelectorAll('.react');
  for (const el of reactEntrypoints) {
    const componentName = el.dataset.component;
    const Component = (await import(`../components/${componentName}.tsx`)).default;

    const root = createRoot(el);

    const dataprops = el.dataset.props;
    const props = dataprops ? JSON.parse(dataprops) : {};

    root.render(<Component {...props} />)
  }
}

renderAll();

גם את קוד הקומפוננטה אני מעדכן כי אפשר להוריד משם את ה render (הוא קורה בקוד הראשי) ולכן הקובץ Home.tsx מכיל עכשיו רק את זה:

import React from 'react';

export default function Home({text}: {
  text: string
}) {
  return <div>
      <h1>{text}</h1>
      <a href="/about">About Page</a>
    </div>
}

ולסיום אני מעדכן את ה View בקובץ app/views/home/index.html.erb כדי ליצור את הקומפוננטה:

<div
  class="react"
  data-component="Home"
  data-props='<%= {text: "hello"}.to_json %>'
></div>

שימו לב שה Properties לקומפוננטה כתובים כאן ב View, זה אומר שנוכל בקלות להעביר אותם מה Controller.

רק בשביל המשחק אני מוסיף דף נוסף לאתר. בקובץ app/controllers./home_controller.rb אני מוסיף עוד פונקציה:

class HomeController < ApplicationController
  def index
  end

  def about    
  end
end

בתיקיית frontend/components אני יוצר קומפוננטה נוספת בשם About.tsx:

// file: app/frontend/components/About.tsx

import React from 'react';

export default function About() {
  return <p>About Us</p>
}

ובקובץ app/views/home/about.html.erb אני כותב את פרטי הקומפוננטה:

<div
  class="react"
  data-component="About"
></div>

ולסיום בקובץ config/routes.rb אני מוסיף את הנתיב:

Rails.application.routes.draw do
  root to: 'home#index'
  get '/about', to: 'home#about'
end

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

3. עוד ספריות מומלצות

יש עוד מספר ספריות שלפעמים אני משלב שהופכות את החיבור בין ריאקט לריילס לאפילו יותר מוצלח:

  1. הספריה js-from-routes מחברת בין ה Routes שמוגדרים בריילס בתור פונקציות ל JavaScript.

  2. הספריה typesfromserializers יודעת לייצר ממשקי טייפסקריפט ישירות מתוך Serializers. סריאקלייזרים למי שלא מכיר זה מנגנון ריילסי שקובע איך מודל יהפוך ל JSON. עם הספריה הזאת יש לנו אוטומטית טיפוסי טייפסקריפט שמתאימים לכל ה JSON-ים שאנחנו שולחים מריילס (בין אם בתור props לקומפוננטות או דרך ה API).