• בלוג
  • ניסוי ריילס: משחק איקס עיגול חלק 2

ניסוי ריילס: משחק איקס עיגול חלק 2

11/10/2024

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

1. איך זה עובד

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

format.turbo_stream { render turbo_stream: turbo_stream.replace(@game) }

שגורמת לשרת להחזיר את ה HTML של המשחק, ושוב באופן אוטומטי קוד ה Ajax בדפדפן לוקח את הטקסט שהוחזר ומחליף בזה את תוכן ה div של המשחק.

בשביל להוסיף תמיכה ב Web Sockets נשתמש עדיין בטורבו אבל בצורה קצת אחרת:

  1. נפתח ערוץ תקשורת Web Socket מהדפדפן לשרת.

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

  3. בדפדפן נקשיב להודעות בערוץ ונתייחס אליהן כמו לתשובות של טופס, כלומר נחליף את תוכן ה div של המשחק במידע שהגיע בערוץ.

2. קוד

בתור התחלה אני מעדכן את הקוד ב games_controller.rb לקוד הזה:

  def play
    GameMove.create!(game_move_params)
    @game.broadcast_replace_to(
      "game_#{@game.id}",
      partial: "games/game",
      locals: { game: @game }
    )

    respond_to do |format|
      format.turbo_stream { render turbo_stream: turbo_stream.replace(@game) }
      format.html         { redirect_to show_game_url(@game.id) }
    end
  end

השורה החדשה היא השורה השניה בפונקציה. היא שולחת הודעת "החלפה" לערוץ ששמו מורכב מהמילה game, קו תחתי ואז מזהה המשחק. בשביל תוכן ההודעה היא מפעילה את התבנית games/_game.html.erb ומעבירה לה בתור משתנה את המשחק הנוכחי. אגב גם את התבנית קצת שיניתי כדי שתעבוד עם שם המשתנה game ולא @game. אבל השינוי הכי גדול בקובץ הוא השורה הראשונה שמקשיבה לעדכונים מהערוץ ומטפלת בהם:

<%= turbo_stream_from dom_id(@game) %>

<div id="<%= dom_id game %>">
  <p>Now playing: <%= game.next_player %></p>
  <table>
    <tbody>
      <% (0..2).each do |row_index| %>
      <tr>
        <% (0..2).each do |column_index| %>
          <% text = game.at(row_index, column_index) %>    
          <td
            class="<%= class_names(playable: text == '.') %>">
            <%= form_for :game_move, url: play_game_path(game.id) do |f| %>
              <%= f.hidden_field :row, {value: row_index} %>
              <%= f.hidden_field :column, {value: column_index} %>
              <%= f.hidden_field :game_id, {value: game.id} %>
              <%= f.hidden_field :player, {value: game.next_player } %>
              <%= f.submit text %>    
            <% end %>            
          </td>
        <% end %>
      </tr>
      <% end %>
    </tbody>
  </table>
</div>

וכן זה כל מה שהיה צריך.

3. מה קיבלנו

התוצאה די מרשימה:

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

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

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

נ.ב. קוד הפרויקט המעודכן נמצא בקישור: https://github.com/ynonp/rails-demo-tic-tac-toe