You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Rails问题:JS事件处理器中的嵌入式Ruby行为不符合预期

Rails中嵌入式Ruby与客户端JS的执行机制解惑

我精通Ruby与JS,正在学习Rails。最近做了一个极简Rails应用,想在JavaScript点击事件处理器里调用Ruby方法,实现从牌组抽牌并展示结果。

创建应用的命令:

rails new blackjack
cd blackjack
bin/rails generate controller Games blackjack
rails s

只修改了两个文件:./app/views/games/blackjack.html.erb 和 ./app/controllers/games_controller.rb,代码如下:

games_controller.rb

class GamesController < ApplicationController
  def blackjack
    @game = Game.new
  end
end

class Deck
  CARD_VALUES = {
    '2'  => 2,
    '3'  => 3,
    '4'  => 4,
    '5'  => 5,
    '6'  => 6,
    '7'  => 7,
    '8'  => 8,
    '9'  => 9,
    '10' => 10,
    'J'  => 10,
    'Q'  => 10,
    'K'  => 10,
    'A'  => 11
  }.freeze

  RANKS = %w(2 3 4 5 6 7 8 9 10 J Q K A).freeze

  # UTF codes for suit symbols
  SUITS = ["\u2660", "\u2661", "\u2662", "\u2663"].freeze

  attr_reader :cards

  def initialize
    new_deck
  end

  def new_deck
    @cards = RANKS.product(SUITS).shuffle
  end
end

class Game
  attr_reader :deck

  def initialize
    @deck = Deck.new
  end
end

blackjack.html.erb

<p id="card" style="font-size:48px">Card: <%= @game.deck.cards.sample %></p>

<button id="button-card">Card</button>

<script>
document.getElementById('button-card').addEventListener('click', e => {
  let card = `<%= @game.deck.cards.sample %>`;
  card = card.replace(/&quot;/g, '"');
  document.getElementById('card').innerText = 'Card: ' + card;
});
</script>

实际现象:页面加载时显示一张随机牌(比如["6", "♢"]),符合预期;首次点击“Card”按钮会切换另一张随机牌,也符合预期,但后续再次点击按钮,牌面不再变化。刷新页面则会显示新的随机牌。

我知道这不是Rails的常规做法,但想搞懂背后的原理,希望得到解答。


原理解析

核心问题在于服务器端代码(Ruby/ERB)和客户端代码(JavaScript)的执行时机完全分离:

  1. 服务器端渲染阶段:当你访问页面时,Rails服务器会执行GamesController#blackjack方法,创建@game实例,然后渲染blackjack.html.erb模板。这个过程中,所有<%= %>包裹的Ruby代码都会被执行,生成对应的字符串插入到HTML中——包括你写在<script>标签里的<%= @game.deck.cards.sample %>。

    也就是说,当页面发送到你的浏览器时,这段JS代码已经被替换成了一个固定的字符串(比如["Q", "♠"]),实际浏览器拿到的JS代码是:

    document.getElementById('button-card').addEventListener('click', e => {
      let card = `["Q", "♠"]`;
      card = card.replace(/&quot;/g, '"');
      document.getElementById('card').innerText = 'Card: ' + card;
    });
    
  2. 客户端执行阶段:浏览器加载页面后,JS代码在本地运行。每次点击按钮,都是调用这段已经固定的代码,只会把牌面设置成服务器渲染时生成的那一张固定牌——这就是为什么第一次点击会换(从页面加载的随机牌换成JS里的固定牌),之后再点击就不会变了。

    而刷新页面时,会重新向服务器发起请求,服务器会重新执行Ruby代码,生成新的随机牌,包括JS里的那个值也会变成新的随机结果,所以刷新后又能看到新牌。


常规实现思路(补充)

如果想每次点击都获取新的随机牌,需要让客户端向服务器发起新的请求,获取新的随机结果,常见的方式是用AJAX:

  1. 在GamesController里新增一个动作,返回随机牌:

    def draw_card
      deck = Deck.new
      render json: deck.cards.sample
    end
    
  2. 在routes.rb里添加路由:

    get 'games/draw_card', to: 'games#draw_card'
    
  3. 修改客户端JS,用fetch发起请求:

    document.getElementById('button-card').addEventListener('click', e => {
      fetch('/games/draw_card')
        .then(response => response.json())
        .then(card => {
          document.getElementById('card').innerText = `Card: [${card[0]}, ${card[1]}]`;
        });
    });
    

这样每次点击按钮,客户端都会向服务器请求新的随机牌,服务器重新执行Ruby代码生成结果,就能实现每次点击都换牌的效果。


内容的提问来源于stack exchange,提问作者BobRodes

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 16:53:14