• בלוג
  • תבנית בסיס ליישומי Rails ו ClojureScript

תבנית בסיס ליישומי Rails ו ClojureScript

08/04/2020

ריילס מגיע עם תמיכה מובנית בהמון ספריות JavaScript ותבניות פרויקטים בזכות ג'ם שנקרא webpacker. הצרות התחילו כשניסיתי לחבר אותו עם ClojureScript מאחר וקלוז'רסקריפט משתמש בקומפיילר משלו. אז יצרתי תבנית פשוטה שמכסה את הנושאים הכי מרכזיים ומאפשרת להתחיל פרויקט ריילס עם ClojureScript יחסית בקלות, ותעזור גם לכם לכתוב את אפליקציית ה Rails/ClojureScript הראשונה שלכם.

1. מבנה הפרויקט

את התבנית המלאה אפשר למצוא בגיטהאב בקישור: https://github.com/ynonp/rails-clojurescript-starter.

ועכשיו להסבר-

ריילס מאחסן את כל קבצי צד-הלקוח המוכנים להגשה בתיקיית public ואת קבצי ה JavaScript בתיקיית public/javascripts. לכן אנחנו צריכים ליצור תיקיה עבור קבצי המקור של יישום ה ClojureScript ולוודא שהקומפיילר ישלח את התוצרים שלו לתיקיית public/javascripts.

אחרי יצירת פרויקט הריילס יצרתי תיקיה בשם client בתיקיה הראשית שלו, ובתוכה הרצתי את הפקודה הבאה כדי ליצור פרויקט ClojureScript חדש:

$ npx create-cljs-project acme-app

אחרי יצירת הפרויקט יש לעדכן את קובץ ההגדרות כדי שתוצרי הבניה ישמרו למקום הנכון. קובץ ההגדרות נקרא shadow-cljs.edn ואצלי הוא נראה כך:

;; shadow-cljs configuration
{:source-paths
 ["src/dev"
  "src/main"
  "src/test"]

 :dependencies [[reagent "0.8.1" :exclusions [cljsjs/react cljsjs/react-dom]]]

 :builds
 {:frontend
  {:target :browser
   :output-dir "../../public/javascripts/"
   :asset-path "/javascripts"
   :modules {:main {:init-fn acme.frontend.app/init}}
   :devtools {:before-load acme.frontend.app/stop
              :after-load acme.frontend.app/start
              :http-root "public"
              :http-port 8020}
   :release {:module-hash-names 8}}}}

ההגדרות שרלוונטיות למיקומי הקבצים הן asset-path ו output-dir.

2. טאסקים

בשביל שיהיה לנו קל לעבוד הוספתי מספר טאסקים שרלוונטים ל ClojureScript ופשוט מריצים את כלי שורת הפקודה המתאימים. הם מוגדרים בקובץ lib/tasks/cljs.rake שנראה כך:

# then re-define
namespace 'cljs' do
  desc 'compile cljs app'
  task 'compile' do
    Dir.chdir('client/acme-app') do
      sh('npx shadow-cljs release frontend')
    end
  end

  desc 'watch cljs app'
  task 'watch' do
    Dir.chdir('client/acme-app') do
      sh('npx shadow-cljs watch frontend')
    end
  end
end

Rake::Task['assets:precompile'].enhance do
  Rake::Task['cljs:compile'].invoke
end

עכשיו משורת הפקודה אנחנו יכולים להפעיל:

$ bundle exec rails cljs:compile

כדי לקמפל את קבצי ה Clojure, ו:

$ bundle exec rails cljs:watch

כדי להתחיל להסתכל על קבצים במצב פיתוח ולקמפל מחדש כל פעם שיש שינוי בקובץ.

3. עבודה עם Long Term Cache

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

גם shadow-cljs תומך במנגנון זה והאופציה בקובץ ההגדרות שגורמת לו להוסיף hash לשמות קבצים נקראת :module-hash-names. בחיבור בין ClojureScript ל Rails אנחנו צריכים להיות מסוגלים מתוך ריילס לטעון תמיד את הקובץ הנכון ש shadow-cljs יצר, אפילו שאין לנו מושג איך הוא חישב את ה Hash המתאים לכל קובץ.

בשביל זה shadow-cljs מייצר קובץ בשם manifest.edn בתיקיית היעד של הפרויקט. קובץ זה מספר איזה מודולים נוצרו ומה שם הקובץ המתאים לכל מודול.

יישום ריילס צריך דרך לחבר בין השם main (או כל שם אחר של מודול) לבין שם הקובץ האמיתי שנמצא בתיקיית public. בשביל זה התקנתי את הג'ם edn ויצרתי את ה helper הבא בקובץ application_helper.rb:

module ApplicationHelper
  def cljs_module_tag(module_id)
    modules = []
    File.open(Rails.root.join('public', 'javascripts', 'manifest.edn')) do |f|
      modules = EDN.read(f)
    end
    module_data = modules.find {|m| m[:'module-id'] == module_id }

    filename = module_data[:'output-name']
    javascript_include_tag filename, skip_pipeline: true
  rescue
    javascript_include_tag module_id, skip_pipeline: true
  end
end

ה helper מקבל שם של מודול, מסתכל בקובץ המניפסט כדי להבין מה שם הקובץ האמיתי של המודול ומחזיר תגית script שה src שלה מכוון לשם הקובץ (כולל ה Hash).

בכל דף של View אני יכול להשתמש ב Helper כדי לטעון את המודול המתאים לקומפוננטה מסוימת (או לשים את זה ב Layout אם אני בונה Single Page Application). בפרויקט הדוגמא ה HTML של ה View נראה כך:

<div id="app"></div>
<%= cljs_module_tag :main %>

וזה מבטיח לי לקבל את קובץ הסקריפט הנכון.

4. הג'ם gon ושיתוף משתנים מריילס לקלוז'ר

האתגר האחרון להיום יהיה להעביר משתנים מ Controller של Rails לקוד ה ClojureScript. הג'ם gon הוותיק עושה עבודה מצוינת ומסתיר מאתנו את כל הלכלוך של כתיבת מידע ל DOM וקריאה משם למשתנה תוך פיענוח ה JSON.

כדי להשתמש בג'ם צריך כמובן להוסיף אותו ל Gemfile, ואחרי זה ב Layout יש להוסיף:

    <%= Gon::Base.render_data %>

עכשיו אפשר ב Controller לכתוב שורה כמו:

  def index
    gon.username = "ynon"
  end

כדי לקבל את המשתנה זמין ב ClojureScript

5. קומפוננטת הריאייג'נט הראשונה שלי

ואם הגעתם עד לכאן שווה להראות גם הצצה ל ClojureScript ולספריית Reagent שעוטפת את ספריית React המוכרת לנו מ JavaScript. קומפוננטת ריאקט ב ClojureScript נראית כך:

(defn app []
  [:div
   [:h1 "Hello " js/gon.username]])

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


(defn click-counter []
  (r/with-let [clicks (r/atom 0)]
    [:div
     [:p "Clicks: " @clicks]
     [:button { :onClick #(swap! clicks inc) } "Click Me"]]))

(defn app []
  [:div
   [:h1 "Hello " js/gon.username]
   [:p "Here's a click counter..."]
   [click-counter]])

במקום useState אנחנו מגדירים משהו שנקרא atom, ובשביל לשלב קומפוננטה אחת בתוך קומפוננטה אחרת פשוט כותבים את שמה (בדיוק כמו שהיינו עושים בריאקט רגיל).

6. איך מפעילים

רוצים להתחיל לפתח ב ClojureScript ו React?

צעד ראשון הוא להוריד את הטמפלייט מגיטהאב דרך הקישור https://github.com/ynonp/rails-clojurescript-starter.

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

$ rails s

ובחלון שני נכתוב:

$ rails cljs:watch

ותוכלו להיכנס לשרת המקומי שיאזין על localhost:3000 ולראות את יישום הריאייג'נט הראשון שלכם (או לפחות שלי). אחרי שתסיימו לבנות משהו מעניין תוכלו להפעיל:

$ rails assets:precompile

ותקבלו Production Build של ה ClojureScript שיכתב לתוך תיקיית app. וכמובן אם יש לכם רעיונות לשיפור או דברים ששכחתי (בטוח יש המון) תהיו חברים ושלחו Pull Request.