ביי ביי ריאקט-ראוטר

19/09/2016

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

ריאן ומייקל, אני לא עומד בקצב שלכם. בפעם הראשונה ששיניתם את כל ה API במעבר מ 0.13 ל 1.0 עוד זרמנו עם זה, ברור שעוד לא היתה גירסא 1 ושום דבר עוד לא סופי. כשפחות משנה אחרי באתם עם גירסא 2 שמשנה את כל ה API שדרגתי, אבל ההודעה האחרונה על שינוי נוסף ב API בגירסא 4.0 היא כבר שדרוג אחד יותר מדי.

אומנם אין הרבה חלופות ל react-router, אבל אחרי עצה טובה מבנג'מן הצלחתי לחבר את ה router של express כתחליף. תשמעו איך זה עובד.

1. קוד הדוגמא

כל הקוד בפוסט זמין כיישום Rails לדוגמא: https://github.com/ynonp/react-rails-express-router

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

סך הכל הספריות להתקנה בנוסף ל react_on_rails הינן:

npm install --save nighthawk isomorphic-fetch es6-promise router

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

2. הגדרת נתיבים

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

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

import HelloWorld from '../containers/HelloWorld';
import Home from '../containers/home';

export default function routes(router) {
  router.get('/', respondWith(Home));
  router.get('/hello_world', respondWith(HelloWorld));
  router.get('/hello_world/:name', respondWith(HelloWorld));
}

function respondWith(component) {
  return function(req, res) {
    res.component = component;
    res.props = Object.assign({}, { routerParams: req.params }, res.props);
  }
}

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

3. איך זה עובד

הקובץ routes.js נטען מתוך קובץ אתחול שנקרא בדוגמא HelloWorldApp.jsx. כאשר מבוצע רינדור בצד השרת קובץ האתחול נטען מתוך הקובץ serverHelloWorldApp.jsx וכאשר הרינדור בצד הלקוח קובץ האתחול נקרא מתוך clientHelloWorldApp.jsx. כל אחד מהם אחראי על הוספת רכיבים הרלוונטים לרינדור בצד השרת או בצד הלקוח בלבד.

בקובץ המשותף HelloWorldApp נראה את הגדרת הפקד הראשי של היישום:

import React from 'react';
import routes from './routes';

export default function(router) {
  const HelloWorldApp = (props, railsContext) => {
    routes(router);

    const res = { props: props };

    router.handle({ url: railsContext.href, method: 'get', init: true }, res, function() { });
    return React.createElement(res.component, props);
  };

  return HelloWorldApp;
}

הפקד הראשי ביישום מיוצר על ידי react_on_rails ולכן מקבל בנוסף ל props גם משתנה בשם railsContext. משתנה זה כולל למשל את הנתיב הנוכחי כפי שהגיע מריילס, ופרמטרים נוספים של הבקשה והסביבה. הקריאה ל routes מאתחלת את הגדרות הניתוב והקריאה ל router.handle מייצרת ניווט לנתיב שהגיע מאותו railsContext. בסיום תהליך הניווט נקבל במשתנה res את הרכיב אותו צריך להציג, וזה הרכיב שמוחזר בתור הרכיב הראשי של היישום.

החלק בקוד ששם את הרכיב המתאים ב res הוא בדיוק הפונקציה שהופיעה בקובץ routes.js.

בצד השרת קובץ ה ERB היחיד שאני משתמש בו (לכל הנתיבים) נקרא app/views/layout/application.html.erb ונראה כך:

<!DOCTYPE html>
<html>
  <head>
    <title>RouterDemo</title>
    <%= csrf_meta_tags %>

    <%= stylesheet_link_tag    'application', media: 'all' %>
    <%= javascript_include_tag 'application' %>
<style>
nav a {
  padding: 10px;
  background: #a8a8a8;
  margin-right: 10px;
}

</style>
  </head>

  <body>
    <div id="app">
      <%= react_component("HelloWorldApp", props: @appstate, prerender: true) %>
    </div>
  </body>
</html>

לכן מבחינת השרת תמיד יופיע על המסך הרכיב HelloWorldApp, ורכיב זה משתמש ב router כדי לדעת איזה רכיב להציג.

4. מעבר בין דפים

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

1. האחד הוא פניה ב Ajax לשרת כדי לקבל את המידע של העמוד הבא.
2. השני הוא זיהוי לחיצה על קישור ומעבר לעמוד הבא.

שני המנגנונים מיושמים בקובץ clientHelloWorldApp.jsx. קובץ זה משתמש בספריה nighthawk שעוטפת את ה router הרגיל של express ומתאימה אותו לעבודה בדפדפן. הפקודה:

router.listen({ dispatch: false });

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

router.use(function(req, res, next) {
  if (req.init) {
    return next();
  }

  fetch(req.url, {
    method: 'GET',
    redirect: 'follow',
    headers: new Headers({
      'Accept': 'application/json',
      'X-Requested-With': 'XMLHttpRequest',
    }),
  }).then(function(response) {
    return response.json();
  }).then(function(appstate) {
    res.props = appstate;
    next();
  });
});

router.use(function(req, res, next) {
  next();
  if (res.component) {
    ReactDOM.render(React.createElement(res.component, res.props), document.querySelector('#app'));
  }
});

 

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

בצד השרת יישום Rails משתמש בקוד אחיד כדי לשלוח JSON או HTML לפי בקשת הלקוח. מאחר ובכל מקרה רכיבי ריאקט מקבלים את אותו סט משתנים אני שומר סט משתנים זה במשתנה שנקרא appstate וכל נקודת כניסה בצד השרת מחזירה את appstate כ JSON או מרנדרת עמוד HTML עם הרכיב הרלוונטי ועם appstate בתור ה properties שלו. פונקציית רובי שאחראית לזה נקראת `render_appstate` ונראית כך:

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  respond_to :html, :json

  def render_appstate
    response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate'
    response.headers['Pragma'] = 'no-cache'
    response.headers['Expires'] = 'Fri, 01 Jan 1990 00:00:00 GMT'

    respond_with(@appstate) do |format|
      format.html do
        render file: 'app/views/layouts/application.html.erb', layout: false
      end
      format.json do
        render json: @appstate
      end
    end
  end
end

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

class HomeController < ApplicationController
  layout 'application'

  def index
    @appstate = {
      msg: "hello world. Server time is: #{Time.zone.now.strftime('%s')}"
    }
    render_appstate
  end
end

כשלקוח פונה ל index בפניה ראשונה לקבלת HTML המשתנה appstate יועבר ישירות כ props לפקד שנקבל מה router. כשלקוח פונה בהמשך לאותו נתיב כדי לנווט לשם אחרי שהעמוד נטען, הנתיב יחזיר את אותו המידע כ JSON.

 

5. ניהול Layout

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

ניקח לדוגמא את הקוד הבא של הרכיב layout:

import React from 'react';

export default function(component) {
  return function(props) {
    return (<div>
      <nav>
        <a href="/">Home</a>
        <a href="/hello_world">Hello World</a>
        <a href="/hello_world/ynonp">Hello Ynon</a>
      </nav>
      <main>
        {React.createElement(component, props)}
      </main>
    </div>);
  }
}

ועכשיו במקום שהיחס בין ה Layout לתוכן שלו יוגדר ברמת ה router, אנו טוענים את ה layout מתוך קוד הפקד באופן הבא:

import React from 'react';
import layout from './layout';

class Home extends React.Component {
  render() {
    return (
      <div>
        <h1>Welcome Home</h1>
        <p>{this.props.msg}</p>
        <a href="/hello_world">Page 2</a>
      </div>
    );
  }
}

export default layout(Home);

6. ניווט יזום מתוך JS

הספריה nighthawk כוללת פונקציה בשם changeRoute המייצרת אירוע ניווט. אני אוהב שהפונקציה זמינה מכל מקום ולכן הוספתי לקובץ clientHelloWorldApp את השורה:

window.navigateTo = router.changeRoute.bind(router);

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

window.navigateTo('/hello_world');

7. מה הרווחנו (או: היה שווה?)

הרווח המרכזי הוא יציבות. ניתוב צד-לקוח זה דבר חשוב מאחר וזה מקצר את זמני מעבר העמוד בין הדפים השונים באתר. הסתמכות על react-router הוכחה בשנה האחרונה כבעייתית: הספריה מאלצת High Coupling בין ממשק הספריה לקוד שלנו, וכותבי הספריה נוטים לשנות את הממשק כל מספר חודשים. התוצאה שהשדרוגים קשים ומתכנתים מעדיפים להשאר עם גרסאות ישנות מאשר לשדרג.

הפרידה מ react-router מאפשרת להגיע לפתרון שדורש הרבה פחות coupling עם הקוד שלנו. שימו לב שכל הקשר ל router מסוים מתבטא אך ורק בקבצי האתחול. הממשק של ה router מאוד פשוט ומורכב מ 2-3 פונקציות לכל היותר, ולכן החלפתו אם בעתיד יעלה הצורך תהיה פשוטה.

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