מבנה פרויקט Rails, React ו TypeScript
אנחנו ב 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. עוד ספריות מומלצות
יש עוד מספר ספריות שלפעמים אני משלב שהופכות את החיבור בין ריאקט לריילס לאפילו יותר מוצלח:
הספריה js-from-routes מחברת בין ה Routes שמוגדרים בריילס בתור פונקציות ל JavaScript.
הספריה typesfromserializers יודעת לייצר ממשקי טייפסקריפט ישירות מתוך Serializers. סריאקלייזרים למי שלא מכיר זה מנגנון ריילסי שקובע איך מודל יהפוך ל JSON. עם הספריה הזאת יש לנו אוטומטית טיפוסי טייפסקריפט שמתאימים לכל ה JSON-ים שאנחנו שולחים מריילס (בין אם בתור props לקומפוננטות או דרך ה API).