
本文详细介绍了在Rails应用中,如何结合Turbo Streams和Stimulus实现客户端的权限控制。当通过Turbo Streams实时更新列表项时,由于服务器端Pundit策略无法在客户端上下文执行,导致按钮显示逻辑失效。解决方案是利用Stimulus监听Turbo Stream事件,通过额外的API请求获取资源权限,并动态调整操作按钮(如编辑、删除)的可见性,确保用户界面权限的准确性。
在Rails应用中,使用Turbo Streams实现实时更新列表项功能时,通常会遇到一个挑战:如何在通过Turbo Streams推送更新时,动态地控制客户端操作按钮(如“编辑”、“删除”)的可见性,使其符合用户的权限。传统的服务器端权限管理库(如Pundit)依赖于请求上下文中的Warden::Proxy实例来判断用户权限。然而,当Turbo Stream响应在客户端渲染时,此上下文不可用,导致服务器端策略判断失效。直接在服务器端渲染时根据策略隐藏按钮,会导致通过Turbo Streams更新的内容无法正确显示权限。
为了解决这一问题,我们需要一种机制,在Turbo Stream内容到达客户端并渲染后,能够执行自定义的JavaScript逻辑来检查并更新按钮的可见性。
本教程将介绍一种结合Rails后端、JSON API和Stimulus前端控制器的方法,实现客户端动态权限控制。核心思路是:
为了在视图中区分普通HTTP请求和Turbo Stream请求,我们可以在ApplicationController中添加一个辅助方法:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# ... 其他代码 ...
def turbo_stream?
formats.any?(:turbo_stream)
end
helper_method :turbo_stream?
end这个turbo_stream?方法会检查当前请求的格式是否包含:turbo_stream。在视图中,我们可以利用这个方法来决定是否在初始渲染时隐藏按钮。
接下来,我们需要修改资源的局部视图(例如_resource.html.erb),以适应客户端权限控制的需求。
<!-- app/views/resource/_resource.html.erb -->
<%= turbo_frame_tag resource do %>
<div id="<%= dom_id resource %>"
data-resource-url="<%= resource_path(resource, format: :json) %>">
<!-- 资源的其他显示内容 -->
<% # 如果是Turbo Stream请求,默认隐藏按钮,待客户端JS处理 %>
<% # 否则,使用Pundit策略判断是否显示 %>
<% if turbo_stream? || policy(resource).edit? %>
<%= link_to edit_resource_path(resource),
class: "btn btn-primary #{'d-none' if turbo_stream?}",
data: { resource_action: :edit } do %>
<i class="las la-edit"></i>
<span class="d-none d-lg-inline">
<%= t("buttons.edit") %>
</span>
<% end %>
<% end %>
<% if turbo_stream? || policy(resource).destroy? %>
<%= link_to resource,
class: "btn btn-danger #{'d-none' if turbo_stream?}",
data: {
resource_action: :destroy,
turbo_confirm: t("confirm.short"),
turbo_method: :delete
} do %>
<i class="las la-trash-alt"></i>
<span class="d-none d-lg-inline">
<%= t("buttons.remove") %>
</span>
<% end %>
<% end %>
</div>
<% end %>关键点说明:
为了让客户端能够获取资源的权限信息,我们需要修改资源的JSON模板(例如_resource.json.jbuilder),使其包含当前用户的权限数据。
# app/views/resources/_resource.json.jbuilder json.extract! resource, :id, :name, :description # 假设资源有这些属性 # ... 其他资源属性 ... json.permissions do json.edit policy(resource).edit? json.destroy policy(resource).destroy? end
这样,当客户端通过data-resource-url访问该资源的JSON表示时,就可以获取到permissions.edit和permissions.destroy这两个布尔值。
现在,我们创建一个Stimulus控制器来处理Turbo Stream事件,并根据获取到的权限更新按钮的可见性。
// app/javascript/controllers/turbostream_controller.js
import Rails from "@rails/ujs" // 引入Rails UJS用于AJAX请求
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
connect() {
// 监听全局的 turbo:before-stream-render 事件
// 这个事件在Turbo Stream内容被渲染到DOM之前触发
addEventListener("turbo:before-stream-render",
(e) => { this.beforeStreamRender(e) })
}
beforeStreamRender(event) {
// 捕获Turbo Stream的默认渲染行为
const defaultAction = event.detail.render
// 重写渲染方法,在默认渲染之后执行我们的自定义处理
event.detail.render = (streamElement) => {
defaultAction(streamElement) // 首先执行Turbo Stream的默认渲染
try {
this.processStream(streamElement) // 然后执行我们的自定义逻辑
} catch(error) {
console.error("Error processing Turbo Stream:", error)
}
}
}
processStream(streamElement) {
// 只处理 'prepend', 'append', 'update' 类型的Turbo Stream操作
if (["prepend", "append", "update"].includes(streamElement.action)) {
// Turbo Stream元素通常包含一个 <template> 标签
// 我们需要获取 template 的内容,并找到带有 data-resource-url 的元素
const template = streamElement.children[0].content
const templateDiv = template.querySelector('[data-resource-url]')
if (templateDiv != null) {
const id = templateDiv.getAttribute('id')
this.setActionButtonVisibility(id) // 调用函数更新按钮可见性
}
}
}
setActionButtonVisibility(id) {
// 根据ID找到新渲染的资源元素
const div = document.querySelector(`div#${id}`)
if (!div) {
console.warn(`Resource div with id ${id} not found.`)
return
}
const url = div.getAttribute('data-resource-url')
const editButton = div.querySelector('[data-resource-action="edit"]')
const destroyButton = div.querySelector('[data-resource-action="destroy"]')
if (!url) {
console.warn(`Resource div with id ${id} missing data-resource-url.`)
return
}
// 使用Rails UJS发送AJAX GET请求获取权限
Rails.ajax({
type: "GET",
url: url,
success: (data, _status, _xhr) => {
try {
// 根据API响应中的权限数据,切换按钮的 'd-none' 类
if (editButton) {
editButton.classList.toggle('d-none', !data.permissions.edit)
}
if (destroyButton) {
destroyButton.classList.toggle('d-none', !data.permissions.destroy)
}
} catch(error) {
console.error("Error updating button visibility:", error)
}
},
error: (xhr, status, error) => {
console.error(`Failed to fetch permissions for ${url}:`, status, error)
}
})
}
}关键点说明:
最后一步是将Stimulus控制器应用到包含资源列表的视图上。只需将资源列表包裹在一个带有data-controller="turbostream"属性的div中。
<!-- app/views/resource/index.html.erb -->
<div data-controller="turbostream">
<!-- 你的原始资源列表代码,例如: -->
<%= turbo_stream_from "resources" %>
<div id="resources">
<% @resources.each do |resource| %>
<%= render resource %>
<% end %>
</div>
</div>通过这种方式,turbostream控制器将会在页面加载时被激活,并开始监听Turbo Stream事件。
通过结合Rails的turbo_stream?辅助方法、数据属性、JSON API以及Stimulus控制器,我们成功地在Turbo Streams环境下实现了客户端的动态权限控制。这种方法允许在不牺牲实时更新能力的前提下,确保操作按钮的可见性始终与用户的实际权限相匹配,从而提升了用户体验和应用的健壮性。虽然引入了额外的API请求,但在许多场景下,这是处理复杂客户端权限逻辑的有效且必要的权衡。
以上就是使用Turbo Streams在客户端动态处理权限控制的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号