Rails问题:JS事件处理器中的嵌入式Ruby行为不符合预期
我精通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(/"/g, '"'); document.getElementById('card').innerText = 'Card: ' + card; }); </script>
实际现象:页面加载时显示一张随机牌(比如["6", "♢"]),符合预期;首次点击“Card”按钮会切换另一张随机牌,也符合预期,但后续再次点击按钮,牌面不再变化。刷新页面则会显示新的随机牌。
我知道这不是Rails的常规做法,但想搞懂背后的原理,希望得到解答。
原理解析
核心问题在于服务器端代码(Ruby/ERB)和客户端代码(JavaScript)的执行时机完全分离:
服务器端渲染阶段:当你访问页面时,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(/"/g, '"'); document.getElementById('card').innerText = 'Card: ' + card; });客户端执行阶段:浏览器加载页面后,JS代码在本地运行。每次点击按钮,都是调用这段已经固定的代码,只会把牌面设置成服务器渲染时生成的那一张固定牌——这就是为什么第一次点击会换(从页面加载的随机牌换成JS里的固定牌),之后再点击就不会变了。
而刷新页面时,会重新向服务器发起请求,服务器会重新执行Ruby代码,生成新的随机牌,包括JS里的那个值也会变成新的随机结果,所以刷新后又能看到新牌。
常规实现思路(补充)
如果想每次点击都获取新的随机牌,需要让客户端向服务器发起新的请求,获取新的随机结果,常见的方式是用AJAX:
在
GamesController里新增一个动作,返回随机牌:def draw_card deck = Deck.new render json: deck.cards.sample end在
routes.rb里添加路由:get 'games/draw_card', to: 'games#draw_card'修改客户端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

