Phoenix模板状态管理及Elixir卡牌游戏页面内调用GenServer问询
嘿,这个问题我太熟悉了!首先得明确:模板本身是运行在前端的(不管你用的是EEx、Phoenix LiveView还是其他模板引擎),没法直接调用后端的GenServer——毕竟服务器渲染完模板后,它就变成静态内容发给前端了,前后端是分离的状态。要实现无页面跳转的状态交互,得用异步通信的方式,下面给你两种在Elixir/Phoenix生态里最常用的方案:
方案1:用Phoenix LiveView(最推荐,高效且简洁)
如果你用的是Phoenix框架,LiveView绝对是首选——它通过WebSocket在前后端建立实时连接,不用写太多JS就能实现无跳转的状态同步,完美契合GenServer的状态管理场景。
步骤1:创建LiveView模块
这个模块会负责启动/关联GenServer,处理前端事件,同步状态:
defmodule YourApp.GameLive do use Phoenix.LiveView def mount(_params, _session, socket) do # 启动GenServer(如果是已有游戏,这里可以用游戏ID关联已启动的进程) {:ok, game_pid} = GameServer.start_link() # 获取初始状态并赋值给socket initial_state = GameServer.get_state(game_pid) {:ok, assign(socket, game_state: initial_state, game_pid: game_pid)} end # 处理前端触发的事件,比如用户点击"抽卡" def handle_event("draw_card", _params, socket) do # 调用GenServer更新状态 new_state = GameServer.draw_card(socket.assigns.game_pid) # 更新socket状态,自动同步到前端模板 {:noreply, assign(socket, game_state: new_state)} end end
步骤2:修改路由指向LiveView
把原来的/games/new路由从普通控制器改成LiveView:
scope "/", YourApp do pipe_through :browser live "/games/new", GameLive, :new end
步骤3:编写LiveView模板
用Phoenix的phx-*绑定触发后端事件,不用跳转就能更新页面:
<!-- lib/your_app_web/live/game_live.html.heex --> <div class="game-container"> <h2>卡牌游戏</h2> <p>当前状态: <strong><%= @game_state.status %></strong></p> <!-- 点击按钮触发LiveView的draw_card事件 --> <button phx-click="draw_card" class="btn">抽一张卡</button> <!-- 可以根据状态动态渲染其他内容,比如手牌 --> <%= if @game_state.cards do %> <div class="hand-cards"> <%= for card <- @game_state.cards do %> <div class="card"><%= card.name %></div> <% end %> </div> <% end %> </div>
这样用户点击按钮时,事件会通过WebSocket发给LiveView,LiveView调用GenServer更新状态后,自动把新状态渲染到页面上,全程无跳转。
方案2:传统AJAX + 控制器动作(不用LiveView的情况)
如果你暂时不想用LiveView,也可以用AJAX异步请求后端控制器,再由控制器调用GenServer并返回JSON结果,最后用JS更新页面。
步骤1:扩展控制器,添加AJAX处理动作
注意:不要直接把GenServer的PID传给前端(不安全),建议生成唯一游戏ID,用ETS或数据库映射PID和ID:
defmodule YourApp.GameController do use YourAppWeb, :controller def new(conn, _params) do {:ok, game_pid} = GameServer.start_link() # 生成唯一游戏ID,并存入ETS映射PID game_id = generate_unique_game_id() :ets.insert(:game_id_to_pid, {game_id, game_pid}) initial_state = GameServer.get_state(game_pid) conn |> assign(:game_state, initial_state) |> assign(:game_id, game_id) |> render("new.html") end # 处理AJAX请求,获取最新状态 def get_state(conn, %{"game_id" => game_id}) do case :ets.lookup(:game_id_to_pid, game_id) do [{_id, game_pid}] -> state = GameServer.get_state(game_pid) json(conn, %{status: state.status, cards: state.cards}) [] -> json(conn, %{error: "游戏不存在"}, status: 404) end end # 处理抽卡的AJAX请求 def draw_card(conn, %{"game_id" => game_id}) do case :ets.lookup(:game_id_to_pid, game_id) do [{_id, game_pid}] -> new_state = GameServer.draw_card(game_pid) json(conn, %{status: new_state.status, cards: new_state.cards}) [] -> json(conn, %{error: "游戏不存在"}, status: 404) end end # 生成唯一ID的辅助函数 defp generate_unique_game_id, do: :crypto.strong_rand_bytes(16) |> Base.encode16() end
步骤2:添加AJAX路由
scope "/", YourApp do pipe_through :browser get "/games/new", GameController, :new post "/games/:game_id/state", GameController, :get_state post "/games/:game_id/draw", GameController, :draw_card end
步骤3:模板里写JS实现AJAX交互
<!-- lib/your_app_web/templates/game/new.html.eex --> <div class="game-container"> <h2>卡牌游戏</h2> <p id="game-status">当前状态: <strong><%= @game_state.status %></strong></p> <button id="draw-btn" class="btn">抽一张卡</button> <div id="hand-cards" class="hand-cards"> <%= for card <- @game_state.cards do %> <div class="card"><%= card.name %></div> <% end %> </div> </div> <script> const gameId = "<%= @game_id %>"; const drawBtn = document.getElementById('draw-btn'); const statusEl = document.getElementById('game-status'); const handCardsEl = document.getElementById('hand-cards'); // 抽卡按钮点击事件 drawBtn.addEventListener('click', async () => { const response = await fetch(`/games/${gameId}/draw`, { method: 'POST' }); const data = await response.json(); if (data.error) { alert(data.error); return; } // 更新状态显示 statusEl.innerHTML = `当前状态: <strong>${data.status}</strong>`; // 更新手牌 handCardsEl.innerHTML = data.cards.map(card => `<div class="card">${card.name}</div>`).join(''); }); // 可选:定时轮询更新状态(比如其他玩家操作后同步) setInterval(async () => { const response = await fetch(`/games/${gameId}/state`, { method: 'POST' }); const data = await response.json(); if (!data.error) { statusEl.innerHTML = `当前状态: <strong>${data.status}</strong>`; } }, 1000); </script>
关键注意点
- 绝对不要直接把GenServer的PID暴露给前端,用唯一游戏ID映射PID才是安全的做法。
- LiveView方案比AJAX更高效,状态同步更实时,代码量也更少,优先推荐。
- 确保你的GenServer API是可以通过PID(或游戏ID关联的PID)调用的,比如
GameServer.get_state/1、GameServer.draw_card/1这类函数。
内容的提问来源于stack exchange,提问作者Frederick John
相关产品推荐
相关产品推荐

