שילוב פקד ריאקט ביישום Java

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

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

נמשיך לסקירת שתי הגישות והצגת הקוד בכל אחת משתיהן.

1. יישום ה Java ממנו נתחיל

סיפורנו מתחיל ביישום Java המציג רשימת ערים ומאפשר למשתמש לבחור עיר ולראות תמונה מתוך אותה העיר. היישום מורכב מ Servlet שמקבל את הבקשה ומכין את הנתונים, עמוד JSP המציג את הנתונים וקוד JavaScript המגדיר את הטיפול בלחיצות בצד הלקוח. קוד היישום המלא זמין בגיטהאב:
https://github.com/ynonp/react-java-demo/tree/step1

כך נראה החלק מתוך קובץ ה JSP הבונה את רשימת הערים והתמונות:

<h1>React Demo</h1>

<h2>Pick a city to see its picture</h2>

<ul>
    <c:forEach var="city" items="${cities}">
        <li class="cityButton" data-img="${city.getImg()}"><a><c:out value="${city.getName()}"/></a></li>
    </c:forEach>
</ul>
<img id="canvas" />
<script>
    var img = document.querySelector('#canvas');
    var buttons = document.querySelectorAll('.cityButton');
    var prevActive;
    for (var i = 0; i < buttons.length; i++) {
        buttons[i].addEventListener('click', function () {
            img.src = this.dataset.img;
            this.classList.add('active');
            if (prevActive && prevActive !== this) {
                prevActive.classList.remove('active');
            }
            prevActive = this;
        });
    }
</script>

העמוד מציג רשימה של 3 ערים ותמונה. בכל לחיצה על עיר מוחלפת התמונה לתמונה המאפיינת את העיר עליה לחצנו. 

הקוד עובד אך קשה להרחבה. מהר מאוד נרצה להוציא את קוד ה JavaScript מעמוד ה JSP כדי להשאיר את הקוד קצר ופשוט, אבל אז נקבל תלות שקטה בין מבנה רשימת התמונות לבין קוד ה JS הקורא רשימה זו. החלפה של אלמנטים ב JSP עלולה לשבור קוד JavaScript שאולי כבר מזמן שכחנו ממנו. כמו כן הקוד הנוכחי בזבזני: הוא מייצר Event Handler לכל כפתור במקום להשתמש ב Event Handler יחיד. מעבר לריאקט יאפשר לשמור את התבנית והלוגיקה במקום אחד ולהגיע לקוד נקי יותר וקל יותר לתחזוקה. 

2. נבנה פקד ריאקט עבור בורר הערים

נתחיל בבניית פקד ריאקט על פי נתונים פיקטיביים ובמסך נפרד, רק כדי לראות שאנו מצליחים להגיע לאותו הקוד. הפקד כולל את כל תוכן ה JSP ומצייר את רשימת הערים כלולאה ב JavaScript. כמו כן הטיפול בלחיצות יבוצע באמצעות ספריית React ולכן נקבל רק Event Handler אחד לכל העמוד. כך נראה קוד הפקד. מוזמנים לעבור לטאב Result לראות את העמוד בפעולה:

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

3. נשלב את קוד הפקד ביישום ה Java שבנינו בצד הלקוח

קיימות דרכים רבות לשילוב קוד ריאקט בצד הלקוח. הדרך שאציג כאן נבחרה כדי להקל על המעבר ל Server Side Rendering. אנו הולכים לבצע את השילוב בשני שלבים: תחילה נבנה את קוד הפקד וקובץ נוסף הנקרא main לפי התבנית של CommonJS. למי שלא מכיר תבנית זו מאפשרת ניהול תלויות בין קבצים שונים, ובמקרה של ריאקט בין פקדים שונים. כך נוכל מקוד הפקד לציין שהוא תלוי בריאקט וב Underscore, ובקובץ main נוכל לציין שהקובץ תלוי בפקד הערים שלנו.
ניהול תלויות באמצעות CommonJS חוסך לנו את הצורך לעקוב אחר כל קבצי ה JavaScript ולנהל בעצמנו את התלויות ביניהם. הכלי Webpack ישמש אותנו כדי לזהות את כל פקדי ריאקט בתוכנית וכל התלויות ביניהם, להמיר את כולם מ JSX לקוד JS תקני ולשלב את כולם לקובץ JavaScript יחיד.

הצעד הראשון יהיה להוסיף תיקיה בשם web_modules בה נשמור את כל פקדי ריאקט כך ש webpack יוכל למצוא אותם ולאחד אותם. בשלב זה אפשר לשמור תיקייה זו בכל מקום בפרויקט אך עבור השלב הבא אנו נרצה שקבצים אלו יהיו זמינים ל Servlet גם לאחר התקנת היישום, ולכן אבחר לשים את התיקיה תחת תיקיית WEB-INF. 
בתיקייה זו ניצור את שני הקבצים cities.js ו main.js כאשר תוכן הקובץ cities.js הינו קוד הפקד ובקובץ main.js נמצא הקוד שמרנדר את הפקד לתוך האלמנט הראשי בעמוד. נתבונן בקוד בקובץ main.js:

var React = require('react');
var CitiesSelector = require('cities');

React.render(<CitiesSelector {...__APP_DATA} />, document.querySelector('main'));

בכתיב של CommonJS אנו מציינים את הספריות בהן אנו משתמשים באמצעות הפקודה require. כך שתי השורות הראשונות ״טוענות״ את הפקד ואת הספריה ריאקט. הכלי Webpack יודע לקרוא פקודות אלו ולפיהן לבנות קובץ JavaScript אחד עם כל התלויות מסודרות לפי הסדר הנכון.

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

החיים של משתנה __APP_DATA יתחילו בקובץ ה Servlet. שם בפונקציה doGet אנו ניקח את רשימת הערים ונהפוך אותה ל JSON String. אני משתמש בספריה Jackson כדי לבצע את ההמרה וכך נראה הקוד:

protected void doGet(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response) throws javax.servlet.ServletException, IOException {
  List<City> cities = new ArrayList<City>();

  cities.add(new City("Paris", "http://cache.graphicslib.viator.com/graphicslib/thumbs674x446/2050/SITours/eiffel-tower-paris-moulin-rouge-show-and-seine-river-cruise-in-paris-150305.jpg"));
  cities.add(new City("London", "http://i.telegraph.co.uk/multimedia/archive/02423/london_2423609k.jpg"));
  cities.add(new City("Berlin", "http://www.jobs-berlin.org/wp-content/uploads/2013/04/Fotolia_45852498_S.jpg"));

  Map<String, Object> props = new HashMap<>();
  props.put("cities", cities);

  request.setAttribute("props",  new ObjectMapper().writeValueAsString(props));

  getServletConfig().getServletContext().getRequestDispatcher(
      "/WEB-INF/cities.jsp").forward(request,response);
}

הפקודה writeValueAsString לוקחת אובייקט וממירה אותו למחרוזת JSON (מה שאנו מכירים מ JavaScript בתור JSON.stringify). כעת אפשר להמשיך לעמוד ה JSP. גם כאן הקוד השתנה וכעת במקום לייצר את קוד ה HTML עבור רשימת הערים אנו בסך הכל מאתחלים את המשתנה __APP_DATA עם אובייקט המידע ונותנים לריאקט לעשות את שאר העבודה:

<main></main>

<script>
    window.__APP_DATA = JSON.parse('${props}');
</script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore.js"></script>
<script src="js/bundle.js"></script>

הסקריפט החדש bundle.js הוא קובץ ה JavaScript המאוחד שנקבל מהפעלת webpack.

כל שנותר הוא לבנות את קבצי התצורה עבור webpack כך שיוכל לאחד את קבצי ה JSX שלנו ולהמיר אותם לפורמט JavaScript תקני. לצורך כך נצטרך שני דברים: תחילה להתקין את webpack ואת מודול העזר שלו jsx-loader ולאחר מכן לבנות את קובץ התצורה webpack.config.js.
התקנת webpack מתבצעת דרך npm. עברו לתיקייה web/WEB-INF ושם בשורת הפקודה רשמו (אחרי הקלדת npm init תשאלו המון שאלות. אפשר ללחוץ enter על כולן ולהמשיך):

npm init
npm install -g webpack
npm install jsx-loader --save

צרו בתוך התיקייה את הקובץ webpack.config.js עם התוכן הבא:

// webpack.config.js
var path = require('path');

module.exports = {
  entry: 'main.js',
  output: {
    filename: '../js/bundle.js'
  },
  module: {
    loaders: [
      { test: /\.js/, loader: 'jsx-loader' }
    ]
  },
  resolve: {
    extensions: ['', '.js']
  },
  externals: {
    "react" : "React",
    "underscore": "_"
  }
};

שימו לב לחלק האחרון שנקרא externals. הכלי webpack לא ינסה למצוא את המודולים הכתובים בחלק זה ובמקום ישתמש במשתנים הגלובליים. כך למשל במקום לחפש את הקובץ underscore.js לוקאלית בכל פעם שתבקשו את המודול underscore.js הכלי webpack יחליף את הבקשה במשתנה הגלובלי _. בקובץ ה JSP טענו את שתי הספריות React ו Underscore מ CDN בהתאמה.

מוזמנים להפעיל את הפקודה:

webpack

מתוך תיקיית web/WEB-INF ולראות כיצד הכלי מאגד את כל המודולים לקובץ bundle.js יחיד.

מאחר ואנו הולכים לבצע פעולה זאת הרבה הוספתי אותה ל build.xml של ant כך שבכל פעם שאבנה את הפרויקט יופעל גם webpack באופן אוטומטי. כך נראה הקוד הרלוונטי מתוך build.xml:

<target name="webpack">
  <exec executable="webpack" dir="${basedir}/web/WEB-INF">
  </exec>
</target>

<target name="all" depends="webpack, build.modules, build.all.artifacts" description="build all"/>

הנה תקציר של כל הפעולות שעשינו כדי שיהיה מסודר:

  1. בנינו תיקייה web_modules בתוך תיקיית web/WEB-INF.
  2. יצרנו את קבצי המודולים cities.js ו main.js בתוך התיקייה web_modules.
  3. עדכנו את קוד היישום כך שייצר משתנה גלובלי בשם __APP_DATA עם כל המידע הדרוש לבניית העמוד (המידע שעובר כ Properties לפקדים).
  4. התקנו את הכלי webpack ויצרנו קובץ הגדרות עבורו.
  5. הוספנו את הספריה Jackson באמצעות העתקת ה jar הרלוונטי לתוך תיקיית web/WEB-INF/lib.
  6. הוספנו Ant Target עבור הפעלת הכלי webpack.

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

קוד הישום המלא כפי שתואר עד כאן זמין בגיטהאב בקישור:
https://github.com/ynonp/react-java-demo/tree/step2

שם תוכלו לראות את כל קבצי המקור וגם להוריד את הפרויקט ולבנות באמצעות ant. בנוסף בניתי מכונה וירטואלית שכבר מכילה את כל קוד הפרויקט מותקן כך שאפשר להכנס ולהתנסות עליה. המכונה זמינה מתוך הדפדפן בקישור:
https://codepicnic.com/bites/java8-jetty-2

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

/app/start.sh

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

4. Server Side Rendering

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

גירסא 8 של Java שדרגה משמעותית את מנוע ה JavaScript המובנה שלה וכיום ב Java 8 אתם יכולים להריץ ריאקט מתוך קוד צד השרת שלכם כדי לבצע פענוח ראשוני של הפקד לטקסט HTML שישלח ללקוח כבר בתוך העמוד. כך נוכל להמשיך לעבוד בריאקט בלי לפגוע בחווית המשתמשים.

5. הוספת רינדור בצד השרת לישום שלנו

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

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

תחילה נוסיף את הספריה JReact באמצעות העתקת ה jar לתיקיית lib. 

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

הספריות אתן אני מאתחל את JReact הן הספריות בהן הרינדור בצד השרת יחפש את המודולים. עלינו להוסיף את התיקייה הראשית ואת התיקיה web_modules. הקוד ב JReact כבר מוסיף את תת הספריה node_modules למה שאנו כותבים.

כך נראה קוד ה Servlet לאחר המעבר ל JReact:

public class CitiesServlet extends javax.servlet.http.HttpServlet {
    private JReact _react;

    public void init(ServletConfig config) throws ServletException {
        super.init(config);
        String base = config.getServletContext().getRealPath("/");
        _react = new JReact();

        _react.addRequirePath(base + "WEB-INF");
        _react.addRequirePath(base + "WEB-INF/web_modules");
    }

    protected void doPost(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response) throws javax.servlet.ServletException, IOException {

    }

    protected void doGet(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response) throws javax.servlet.ServletException, IOException {
        List<City> cities = new ArrayList<City>();
        cities.add(new City("Paris", "http://cache.graphicslib.viator.com/graphicslib/thumbs674x446/2050/SITours/eiffel-tower-paris-moulin-rouge-show-and-seine-river-cruise-in-paris-150305.jpg"));
        cities.add(new City("London", "http://i.telegraph.co.uk/multimedia/archive/02423/london_2423609k.jpg"));
        cities.add(new City("Berlin", "http://www.jobs-berlin.org/wp-content/uploads/2013/04/Fotolia_45852498_S.jpg"));

        Map<String, Object> props = new HashMap<>();
        props.put("cities", cities);

        String result;
        synchronized (this) {
            result = _react.renderToString("./cities.js", props);
        }

        request.setAttribute("react_initial_markup", result);

        request.setAttribute("props",  new ObjectMapper().writeValueAsString(props));

        getServletConfig().getServletContext().getRequestDispatcher(
                "/WEB-INF/cities.jsp").forward(request,response);

    }


}

שימו לב למשתנה החדש שנוסף react_initial_markup. משתנה זה מחזיק בדיוק את ה HTML הראשוני אותו ״נשתול״ בעמוד ה JSP. כך נראה החלק הרלוונטי בעמוד ה JSP המתאים לאחר העדכון:

<main>${react_initial_markup}</main>

<script>
    window.__APP_DATA = JSON.parse('${props}');
</script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore.js"></script>
<script src="js/bundle.js"></script>

השלב האחרון הוא להתקין את גירסאות צד השרת של ספריות react ו underscore. אנו צריכים ספריות אלו כדי להפעיל את קוד ה JavaScript של פקד הערים, אך בניגוד לדפדפן איננו לוקחים אותן מ CDN בצד הלקוח. התקנת הספריות מבוצעת משורת הפקודה באמצעות הכלי npm. עברו לתיקיה web/WEB-INF והפעילו את הפקודות:

npm install react --save
npm install underscore --save

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

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

  1. הוספנו את הספריה JReact באמצעות העתקת ה jar לתיקיית web/WEB-INF/lib.
  2. הוספנו ל Servlet קוד שישתמש בספריה JReact כדי ליצור את טקסט ה HTML הראשוני.
  3. עדכנו את קוד ה JSP כך שישתול בתוך העמוד את טקסט ה HTML הראשוני.
  4. התקנו את הספריות react ו underscore בצד השרת במקום הנגיש ל Servlet.

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

קוד היישום המלא זמין בגיטהאב בכתובת:
https://github.com/ynonp/react-java-demo

 

6. סיכום וקריאת המשך

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

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

היכולת לנהל תלויות בצד הלקוח בתוך קבצי ה JavaScript באמצעות require תורמת לתחזוקה וחוסכת טעויות שנובעות מאי טעינת קובץ JS בו אנו תלויים או מטעינת קבצי ה JS בסדר לא נכון.

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

מתכנתי Spring מוזמנים להיעזר במאמר Isomorphic React Webapps on the JVM הכולל דוגמת קוד לשילוב ריאקט עם ספרינג. במאמר זה ההתמודדות עם בעיית הסינכרון בין תהליכונים בוצעה באמצעות ThreadLocal, כך שגישה ראשונה מכל תהליכון איטית אבל הגישות הבאות מהירות יותר.

במאמר React Rendering On Server תמצאו דוגמא לשילוב ריאקט עם קוד צד-שרת הכתוב ב Clojure בתוך ה JVM.