0

0

使用Turbo Streams在客户端动态处理权限控制

霞舞

霞舞

发布时间:2025-11-11 23:07:16

|

150人浏览过

|

来源于php中文网

原创

使用turbo streams在客户端动态处理权限控制

本文详细介绍了在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前端控制器的方法,实现客户端动态权限控制。核心思路是:

  1. 后端辅助判断: 在服务器端判断请求是否为Turbo Stream,并据此决定是否在初始渲染时隐藏按钮。
  2. 数据属性传递: 在渲染的HTML中嵌入资源URL和操作类型,供客户端JavaScript使用。
  3. JSON API暴露权限: 提供一个JSON API端点,用于客户端获取特定资源的权限信息。
  4. Stimulus控制器监听与处理: 创建一个Stimulus控制器,监听turbo:before-stream-render事件,在Turbo Stream内容渲染后,发送API请求获取权限,并根据响应动态显示或隐藏按钮。

详细实现步骤

1. 服务器端Turbo Stream请求检测辅助方法

为了在视图中区分普通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。在视图中,我们可以利用这个方法来决定是否在初始渲染时隐藏按钮。

2. 修改资源局部视图

接下来,我们需要修改资源的局部视图(例如_resource.html.erb),以适应客户端权限控制的需求。



<%= turbo_frame_tag resource do %>
  
<% # 如果是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 %> <%= t("buttons.edit") %> <% 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 %> <%= t("buttons.remove") %> <% end %> <% end %>
<% end %>

关键点说明:

瑞志企业建站系统(ASP版)2.2
瑞志企业建站系统(ASP版)2.2

支持模板化设计,基于标签调用数据 支持N国语言,并能根据客户端自动识别当前语言 支持扩展现有的分类类型,并可修改当前主要分类的字段 支持静态化和伪静态 会员管理功能,询价、订单、收藏、短消息功能 基于组的管理员权限设置 支持在线新建、修改、删除模板 支持在线管理上传文件 使用最新的CKEditor作为后台可视化编辑器 支持无限级分类及分类的移动、合并、排序 专题管理、自定义模块管理 支持缩略图和图

下载
  • data-resource-url: 这个数据属性存储了获取当前资源JSON表示的URL。Stimulus控制器将使用这个URL来查询权限。
  • data-resource-action: 这个数据属性用于标记不同的操作按钮(如edit和destroy),方便Stimulus控制器识别和操作。
  • 条件隐藏: #{'d-none' if turbo_stream?} 这段代码确保当内容通过Turbo Stream推送时,操作按钮默认是隐藏的。d-none 是Bootstrap的CSS类,用于隐藏元素。

3. 暴露权限的JSON API端点

为了让客户端能够获取资源的权限信息,我们需要修改资源的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这两个布尔值。

4. Stimulus控制器处理客户端逻辑

现在,我们创建一个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元素通常包含一个