שילוב פקד ריאקט ביישום ריילס
פוסט זה כולל טיפ קצר בנושא פיתוח Front End. אם אתם רוצים ללמוד יותר לעומק על פיתוח Front End מהבסיס ועד הנושאים המתקדמים תשמחו לשמוע שבניתי קורס וידאו מקיף בנושא זה הכולל מעל 50 שיעורי וידאו והמון תרגול מעשי.
למידע נוסף והצטרפות לקורס בקרו בדף קורס Front End באתר.
React הינה ספריית צד-לקוח לבניית קוד מבוסס פקדים. היא מציעה תחביר פשוט וביצועים מעולים, כך שהשימוש בה יכול לשפר משמעותית את קוד צד הלקוח שאנו כותבים. את ריאקט קל מאוד לשלב עם ספריות צד-לקוח אחרות, וגם עם כל יישום צד-שרת. בפוסט זה נראה כיצד לשלב קוד ריאקט ביישום ריילס.
1. המשימה
בפוסט זה אדגים כיצד לשלב פקד ריאקט בתוך יישום ריילס ולהמיר ERB View לפקד ריאקט. לצורך המשימה לקחתי יישום Scaffold פשוט המציג רשימת אנשי קשר. כל איש קשר כולל שדה שם ושדה אימייל.
קוד הדוגמא המלא בפוסט זמין בגיטהאב בקישור:
https://github.com/ynonp/react-rails-demo
החיבור בין ריילס לריאקט מתבצע דרך Gem שנקרא react-rails. בעמוד הפרויקט תמצאו הוראות התקנה ושימוש מפורטות:
https://github.com/reactjs/react-rails
המרת קוד ERB לריאקט תאפשר לנו לשלב בקלות קוד JavaScript בתוך העמוד כך ששינויים ב Data יובילו לשינוי מיידי בתצוגה (ללא צורך בפניה לשרת או טעינה מחדש של העמוד). הייתרון הבולט של ריאקט כאן על פני ספריות צד-לקוח אחרות הוא שאפשר לבצע Rendering של התבנית בצד השרת. מבחינת חווית הגולש זה אומר שאין צורך להריץ JavaScript כדי לראות את תוכן העמוד הראשוני, מה שמוביל לזמני טעינה טובים ונגישות למנועי חיפוש וטכנולוגיות מסייעות.
2. המרה מ ERB לריאקט
עמוד ה Index שקיבלנו מה Scaffold נראה כך:
<p id="notice"><%= notice %></p>
<h1>Listing Contacts</h1>
<table>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th colspan="3"></th>
</tr>
</thead>
<tbody>
<% @contacts.each do |contact| %>
<tr>
<td><%= contact.name %></td>
<td><%= contact.email %></td>
</tr>
<% end %>
</tbody>
</table>
<br>
<%= link_to 'New Contact', new_contact_path %>
נתחיל בבניית פקד ריאקט שיציג את אותו המידע. את פקדי ריאקט אנו שומרים בקבצי JavaScript בתיקייה app/assets/javascripts/components. כך כדי לבנות את הפקד אנו ניצור שם תיקייה חדשה בשם contacts ובתוכה קובץ חדש בשם index.js.jsx.
אתם יכולים כמובן לבחור כל מבנה תיקיות שתרצו עבור פקדי ריאקט. אני אוהב להשתמש באותו המבנה של ה Views כך שנוח להגיע מקובץ View לפקד המתאים לו. מאחר ופקד ריאקט יכול לכלול התיחסות לפקדים אחרים, אפשר לשמור פקדים חלקיים עם תחילית _ וכך להשאר במבנה המוכר.
פקד ריאקט נשמר כמשתנה גלובלי ב JavaScript. אנו יוצרים את הפקד באמצעות הפעלת הפונקציה React.createClass לה אנו מעבירים את פרטי הפקד. המינימום שנצטרך להעביר לכל פקד הוא הפונקציה render שתפקידה לצייר את הפקד.
אנו כותבים את הפונקציה בשפה שנקראת JSX המאפשרת שילוב קוד JavaScript עם תגיות HTML. התוצאה די דומה לקוד ה ERB שהתחלנו אתו:
var ListContacts = React.createClass({
render: function() {
return (
<div>
<h1>Listing Contacts</h1>
<table>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th colSpan="3"></th>
</tr>
</thead>
<tbody>
{this.props.contacts.map(function(contact) {
return (
<tr>
<td>{contact.name}</td>
<td>{contact.email}</td>
</tr>
)
})}
</tbody>
</table>
</div>
)
}
});
החלפנו את לולאת each של רובי בפונקציית map של JavaScript. כמו כן המשתנה @contacts שהכיל את רשימת אנשי הקשר הוחלף במשתנה this.props.contacts. באופן כללי פרמטרים שנעביר מריילס לפקד ריאקט יהיו זמינים לנו דרך האובייקט this.props.
כעת ניתן להחליף את הקוד ב View המקורי. אנו נשתמש בפונקציה react_component כדי לשלב פקד ריאקט בתוך ERB View ולאחר ההחלפה כך נראה הקובץ index.html.erb:
<p id="notice"><%= notice %></p>
<%= react_component('ListContacts', :contacts => @contacts) %>
ב notice לא נגענו, אבל שאר הקוד זמין כפקד ריאקט ולכן אנו מרנדרים ישירות את הפקד. העברת הפרמטרים לפקד הופכת אותם לזמינים דרך המשתנה this.props בתוך קוד הפקד.
3. נוסיף יכולות JavaScript לפקד
הייתרון הגדול במעבר לריאקט הוא שכעת אנו יכולים להוסיף קוד JavaScript שישתלב בצורה קלה עם התבנית. נניח למשל שאנו רוצים להסתיר את כתובות המייל כברירת מחדל, ולהוסיף כפתור Toggle Details שיציג או יסתיר את פרטי המייל ברשימה.
באמצעות ריאקט אנו מוסיפים את הקוד לקובץ התבנית שלנו, וקוד ה JavaScript יורץ בצד הלקוח כך שהתגובה לאירועים מופיעה בזמן אמת. פקד ריאקט כולל בנוסף ל Properties גם אובייקט שנקרא State, בו נשמר ״המצב״ הנוכחי של הפקד. בדוגמא שלנו, פקד זה יכיל משתנה בוליאני המתאר האם עלינו להציג או להסתיר את הפרטים, ונוסיף כפתור שיעדכן את המשתנה.
בתוך קוד התבנית אנו יכולים להשתמש באובייקט ה stat. באופן כזה קוד JS לא צריך לעדכן אלמנטים על העמוד בתגובה לאירועים, אלא הקוד מעדכן משתנים שמשפיעים על התבנית הסופית (מה שהופך את הקוד להרבה יותר פשוט).
כך למשל כדי להוסיף קוד עבור הצגה או הסתרה של תיבות המייל אנו נוסיף שתי פונקציות חדשות לפקד הריאקט שלנו:
getInitialState: function() {
return { showEmail: true };
},
toggleDetails: function() {
this.setState({ showEmail: ! this.state.showEmail });
}
ובפונקציה render עלינו להוסיף התיחסות למידע החדש. בתחילת הפונקציה לקחתי את הערך למשתנה פנימי והשתמשתי בו בתוך הלולאה הפנימית (המשתנה this בלולאה הפנימית מאבד את ערכו בגלל הפונקציה map). כך זה נראה:
var showEmail = this.state.showEmail;
...
<tbody>
{this.props.contacts.map(function(contact) {
return (
<tr>
<td>{contact.name}</td>
<td>{showEmail ? contact.email : '***'}</td>
</tr>
)
})}
</tbody>
אפשר למצוא את קוד הפקד המלא בקישור:
https://github.com/ynonp/react-rails-demo/blob/master/app/assets/javascripts/components/contacts/index.js.jsx
4. מה קיבלנו
הצלחנו לקבל פקד תצוגה עם קוד צד-לקוח מוטמע. היתרון הגדול במעבר לריאקט הוא שכעת קל מאוד להוסיף יכולות אינטרקטיביות בצד-לקוח למסך:
- אנחנו מחזיקים במקום אחד את התבנית וקוד צד הלקוח הרלוונטי עבורה.
- ריאקט מספקת דרך סטנדרטית להפעיל קוד בכניסה וביציאה של הפקד (באמצעות הפונקציות componentDidMount ו componentWillUnmount).
- פעולות צד-לקוח משפיעות ישירות על התבנית. זו נקודה חשובה — חשבו על כפתור ההצגה והסתרה של כתובות המייל לדוגמא, באופן רגיל קוד JS היה צריך לשלוף את האלמנטים הרלוונטים ולעדכן את מצבם, מה שיוצר כפילות של המבנה. קוד ה JS צריך היה להתאים לתבנית המופיעה בקובץ ה ERB. במעבר לריאקט הקוד מתאר קוד והתבנית מתארת תבנית, וריאקט כבר ״שובר את הראש״ כיצד להתאים ביניהם.
5. המרה בצד שרת
הקוד שכתבנו עד כה שלח לדפדפן קובץ JS עם פרטי התבנית, אבל הפיכת התבנית ל DOM התבצעה בצד הלקוח. התנהגות זו לא מומלצת במקרה הכללי, שכן היא מעכבת את הצגת המידע לגולש ומונעת מגולשים נטולי JavaScript לראות את העמוד (בפרט זה יפגע בסריקה ממנועי חיפוש).
רינדור תבנית בצד השרת גורם לריילס להפעיל מנוע JS מקומי, להשתמש בו כדי לרנדר את התבנית ולשלוח ללקוח עמוד מוכן. כך אנו חוסכים ללקוח עבודה ובונים עמוד הידידותי למנועי חיפוש. כדי להפעיל את הרינדור בצד הלקוח יש להוסיף פרמטר prerender לפונקציית ה helper. כך נראה הקוד לאחר השינוי:
<%= react_component('ListContacts',
{ contacts: @contacts },
{ prerender: true }) %>
אם החלטתם לבחור בשיטה זו מומלץ להוסיף את הספריה therubyracer המספקת מנוע JS עם ביצועים טובים יותר מזה שמגיע כברירת מחדל.
6. תרגום
נקודה אחת שעדיין לא מצאתי לה פתרון מלא בשילוב של ריילס וריאקט היא התרגום. בריילס אנו רגילים להשתמש בפונקציית העזר t כדי לתרגם. מחרוזת טיפוסית ב ERB עשויה להיראות כך:
<h1><%=t :hello_world %></h1>
במעבר לצד הלקוח עלינו להעביר גם את קבצי התרגום. הספרייה i18n-js מאפשרת המרה אוטומטית של קבצי התרגום מריילס ל JS ושליחתם לקוד הלקוח, והיא אף מוסיפה את הפונקציה t שתהיה זמינה לקוד צד-לקוח. הבעייה שעדיין נשארת היא שקבצי התרגום נשלחים במלואם ללקוח (ולא רק המחרוזות בהן אנו משתמשים בעמוד).
אם כתבתם את קבצי ה Locale בצורה מסודרת, הספריה i18n-js תאפשר לכם לחלק את המחרוזות לקבצי תרגום שונים בצד הלקוח, וכך למשל לבנות קובץ מחרוזות נפרד לכל עמוד. אפשר לראות בעמוד התיעוד של הספריה כיצד לבצע זאת. השימוש בתרגול בצד הלקוח מוסיף עוד שלב לפיתוח, ובכל פעם שמשנים תרגום יש צורך לייצר קבצי תרגום-צד-לקוח חדשים. אם אתם מכירים פתרונות אחרים טובים יותר לנושא התרגום, אשמח לשמוע בתגובות לפוסט.
7. סיכום וקריאת המשך
הבחירה בריאקט בתור ספריית פקדים ושילובה בתוך Rails Views אפשרו לי לשפר משמעותית את קוד ה JavaScript שאני כותב, לצמצם הרבה קוד שכבר אין בו צורך ולשפר ביצועים.
במקום לכתוב JS שמתעסק באיך להציג או להסתיר רכיבים מהממשק, אני פשוט כותב בתבנית את האופן בו אני רוצה שהעמוד ייראה ונותן ליאקט לדאוג לפרטים.
ייתרון נוסף של שיטת עבודה זו הוא הסדר וחלוקת העבודה שנוצרה בקוד: קל מאוד בהיסח הדעת לכתוב קוד ERB שגורם לשליפות נוספות מבסיס הנתונים ובכך מאט את רינדור העמוד. בריאקט אין דרך לעשות זאת כי כל המשתנים עוברים מריילס בתור אובייקטי JSON נטולי מתודות. אם חסרים נתונים תצטרכו ללכת ל Controller ולשלוף אותם, וכך כל שליפה חדשה שנכנסת למערכת עוברת בקרה.
העבודה עם ריאקט לא פגעה בביצועים או בדירוג החיפוש של האתר, מאחר והרינדור נעשה כולו בצד השרת. מבחינת הגולש הוא מקבל את אותו העמוד שקיבל באמצעות ERB.