Rails动作控制器模式Skill rails-action-controller-patterns

这个技能涵盖Ruby on Rails框架中的动作控制器模式,包括路由配置、过滤器使用、强参数处理和RESTful设计,用于构建稳健的Web应用程序控制器,支持后端Web开发和RESTful API开发,关键词包括Rails、动作控制器、路由、过滤器、强参数、RESTful、后端开发、Web应用。

后端开发 0 次安装 0 次浏览 更新于 3/25/2026

name: rails-action-controller-patterns user-invocable: false description: 当使用动作控制器模式时,包括路由、过滤器、强参数和REST约定。 allowed-tools:

  • Read
  • Write
  • Edit
  • Grep
  • Glob
  • Bash

Rails动作控制器模式

掌握动作控制器模式,以构建稳健的Rails控制器,包括正确的路由、过滤器、参数处理和RESTful设计。

概述

动作控制器是Rails中处理Web请求的组件。它处理传入请求、与模型交互并渲染响应。控制器遵循MVC模式,并默认实现REST约定。

安装和设置

生成控制器

# 生成资源控制器
rails generate controller Posts index show new create edit update destroy

# 生成命名空间控制器
rails generate controller Admin::Posts index show

# 生成API-only控制器
rails generate controller Api::V1::Posts --no-helper --no-assets

路由配置

# config/routes.rb
Rails.application.routes.draw do
  # RESTful资源
  resources :posts

  # 嵌套资源
  resources :posts do
    resources :comments
  end

  # 命名空间路由
  namespace :admin do
    resources :posts
  end

  # 自定义路由
  get 'about', to: 'pages#about'
  root 'posts#index'
end

核心模式

1. 基本控制器结构

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  before_action :authenticate_user!, except: [:index, :show]
  before_action :set_post, only: [:show, :edit, :update, :destroy]
  before_action :authorize_post, only: [:edit, :update, :destroy]

  # GET /posts
  def index
    @posts = Post.includes(:user)
                 .order(created_at: :desc)
                 .page(params[:page])
  end

  # GET /posts/:id
  def show
    @comments = @post.comments.includes(:user)
  end

  # GET /posts/new
  def new
    @post = Post.new
  end

  # POST /posts
  def create
    @post = current_user.posts.build(post_params)

    if @post.save
      redirect_to @post, notice: '帖子成功创建。'
    else
      render :new, status: :unprocessable_entity
    end
  end

  # GET /posts/:id/edit
  def edit
  end

  # PATCH/PUT /posts/:id
  def update
    if @post.update(post_params)
      redirect_to @post, notice: '帖子成功更新。'
    else
      render :edit, status: :unprocessable_entity
    end
  end

  # DELETE /posts/:id
  def destroy
    @post.destroy
    redirect_to posts_url, notice: '帖子成功删除。'
  end

  private

  def set_post
    @post = Post.find(params[:id])
  end

  def authorize_post
    unless @post.user == current_user
      redirect_to posts_path, alert: '未经授权'
    end
  end

  def post_params
    params.require(:post).permit(:title, :body, :status, tag_ids: [])
  end
end

2. 强参数

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  # 基本强参数
  def user_params
    params.require(:user).permit(:name, :email, :password)
  end

  # 嵌套属性
  def user_params_with_profile
    params.require(:user).permit(
      :name, :email,
      profile_attributes: [:bio, :avatar, :website]
    )
  end

  # 允许属性数组
  def post_params
    params.require(:post).permit(
      :title, :body,
      tag_ids: [],
      images: []
    )
  end

  # 条件参数
  def user_params
    permitted = [:name, :email]
    permitted << :admin if current_user.admin?
    params.require(:user).permit(*permitted)
  end

  # 深度嵌套属性
  def organization_params
    params.require(:organization).permit(
      :name,
      departments_attributes: [
        :id, :name, :_destroy,
        employees_attributes: [:id, :name, :role, :_destroy]
      ]
    )
  end

  # JSON参数
  def config_params
    params.require(:config).permit(
      settings: [:theme, :notifications, :language],
      preferences: {}
    )
  end
end

3. 过滤器和回调

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  # Before过滤器
  before_action :authenticate_user!
  before_action :configure_permitted_parameters, if: :devise_controller?
  before_action :set_time_zone, if: :user_signed_in?

  # After过滤器
  after_action :log_activity
  after_action :set_cache_headers

  # Around过滤器
  around_action :measure_action_time

  private

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:name])
  end

  def set_time_zone
    Time.zone = current_user.time_zone
  end

  def log_activity
    ActivityLogger.log(controller_name, action_name, current_user)
  end

  def set_cache_headers
    response.headers['Cache-Control'] = 'no-cache, no-store'
  end

  def measure_action_time
    start = Time.current
    yield
    duration = Time.current - start
    Rails.logger.info "操作耗时 #{duration}s"
  end
end

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  skip_before_action :authenticate_user!, only: [:index, :show]
  before_action :set_post, only: [:show, :edit, :update]
  before_action :verify_ownership, only: [:edit, :update]

  prepend_before_action :load_categories
  append_before_action :track_view, only: [:show]

  private

  def verify_ownership
    redirect_to root_path unless @post.user == current_user
  end

  def load_categories
    @categories = Category.all
  end

  def track_view
    @post.increment!(:views_count)
  end
end

4. RESTful约定

# config/routes.rb
Rails.application.routes.draw do
  resources :posts do
    # 集合路由(无ID)
    collection do
      get :drafts
      get :search
    end

    # 成员路由(有ID)
    member do
      post :publish
      patch :archive
    end

    # 嵌套资源
    resources :comments, only: [:create, :destroy]
  end

  # 浅嵌套
  resources :authors do
    resources :books, shallow: true
  end

  # only/except选项
  resources :users, only: [:index, :show]
  resources :sessions, except: [:edit, :update]

  # 自定义资源名称
  resources :posts, path: 'articles'
end

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  # GET /posts/drafts
  def drafts
    @posts = current_user.posts.draft
    render :index
  end

  # GET /posts/search
  def search
    @posts = Post.search(params[:q])
    render :index
  end

  # POST /posts/:id/publish
  def publish
    @post = Post.find(params[:id])
    if @post.publish!
      redirect_to @post, notice: '帖子已发布'
    else
      redirect_to @post, alert: '无法发布帖子'
    end
  end
end

5. 渲染响应

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def show
    @post = Post.find(params[:id])

    respond_to do |format|
      format.html # 默认渲染show.html.erb
      format.json { render json: @post }
      format.xml { render xml: @post }
      format.pdf { render pdf: @post }
    end
  end

  def create
    @post = Post.new(post_params)

    if @post.save
      # 重定向到URL
      redirect_to @post, notice: '已创建'

      # 回退重定向
      redirect_back fallback_location: root_path, notice: '已创建'
    else
      # 渲染模板并设置状态
      render :new, status: :unprocessable_entity
    end
  end

  def export
    # 渲染纯文本
    render plain: '导出完成'

    # 渲染JSON并设置状态
    render json: { status: 'ok' }, status: :ok

    # 渲染无内容
    head :no_content

    # 渲染文件
    send_file '/path/to/file.pdf',
      filename: 'document.pdf',
      type: 'application/pdf',
      disposition: 'attachment'

    # 流式传输文件
    send_data generate_csv, filename: 'report.csv',
      type: 'text/csv',
      disposition: 'inline'
  end

  def partial_update
    # 渲染部分
    render partial: 'post', locals: { post: @post }

    # 渲染集合
    render partial: 'post', collection: @posts

    # 使用布局渲染
    render 'special_layout', layout: 'admin'

    # 无布局渲染
    render layout: false
  end
end

6. 错误处理

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  rescue_from ActiveRecord::RecordNotFound, with: :not_found
  rescue_from ActiveRecord::RecordInvalid, with: :unprocessable_entity
  rescue_from ActionController::ParameterMissing, with: :bad_request
  rescue_from Pundit::NotAuthorizedError, with: :forbidden

  private

  def not_found(exception)
    respond_to do |format|
      format.html { render 'errors/404', status: :not_found }
      format.json { render json: { error: exception.message },
                           status: :not_found }
    end
  end

  def unprocessable_entity(exception)
    render json: { errors: exception.record.errors },
           status: :unprocessable_entity
  end

  def bad_request(exception)
    render json: { error: exception.message },
           status: :bad_request
  end

  def forbidden
    respond_to do |format|
      format.html { render 'errors/403', status: :forbidden }
      format.json { render json: { error: '禁止访问' },
                           status: :forbidden }
    end
  end
end

7. 会话和Cookie管理

# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  def create
    user = User.find_by(email: params[:email])

    if user&.authenticate(params[:password])
      # 设置会话
      session[:user_id] = user.id

      # 设置签名Cookie
      cookies.signed[:user_id] = user.id

      # 设置加密Cookie
      cookies.encrypted[:user_token] = user.token

      # 设置永久Cookie(20年)
      cookies.permanent[:remember_token] = user.remember_token

      # 设置Cookie并指定选项
      cookies[:preference] = {
        value: 'dark_mode',
        expires: 1.year.from_now,
        domain: '.example.com',
        secure: Rails.env.production?,
        httponly: true
      }

      redirect_to root_path
    else
      flash.now[:alert] = '凭据无效'
      render :new
    end
  end

  def destroy
    # 清除会话
    session.delete(:user_id)
    reset_session

    # 清除Cookie
    cookies.delete(:user_id)
    cookies.delete(:remember_token)

    redirect_to login_path
  end
end

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  private

  def current_user
    @current_user ||= User.find_by(id: session[:user_id])
  end

  def user_signed_in?
    current_user.present?
  end

  helper_method :current_user, :user_signed_in?
end

8. 闪存消息

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def create
    @post = Post.new(post_params)

    if @post.save
      # 标准闪存
      flash[:notice] = '帖子已创建'
      redirect_to @post

      # 带重定向的闪存
      redirect_to @post, notice: '帖子已创建'

      # 多种闪存类型
      flash[:success] = '操作成功'
      flash[:error] = '出现错误'
      flash[:warning] = '小心谨慎'
      flash[:info] = '仅供参考'

      # 用于渲染的闪存(非重定向)
      flash.now[:alert] = '验证失败'
      render :new
    end
  end

  def update
    if @post.update(post_params)
      # 自定义键的闪存
      flash[:custom_message] = '自定义通知'
      redirect_to @post
    else
      # 保留闪存到下一个请求
      flash.keep
      redirect_to edit_post_path(@post)
    end
  end
end

# app/views/layouts/application.html.erb
<%# 显示所有闪存消息 %>
<% flash.each do |type, message| %>
  <div class="flash <%= type %>">
    <%= message %>
  </div>
<% end %>

9. API控制器

# app/controllers/api/v1/base_controller.rb
module Api
  module V1
    class BaseController < ActionController::API
      include ActionController::HttpAuthentication::Token::ControllerMethods

      before_action :authenticate

      rescue_from ActiveRecord::RecordNotFound do |e|
        render json: { error: e.message }, status: :not_found
      end

      private

      def authenticate
        authenticate_or_request_with_http_token do |token, options|
          @current_user = User.find_by(api_token: token)
        end
      end

      def current_user
        @current_user
      end
    end
  end
end

# app/controllers/api/v1/posts_controller.rb
module Api
  module V1
    class PostsController < BaseController
      def index
        @posts = Post.page(params[:page]).per(20)

        render json: @posts,
               meta: pagination_meta(@posts),
               status: :ok
      end

      def show
        @post = Post.find(params[:id])
        render json: @post, status: :ok
      end

      def create
        @post = current_user.posts.build(post_params)

        if @post.save
          render json: @post, status: :created, location: api_v1_post_url(@post)
        else
          render json: { errors: @post.errors },
                 status: :unprocessable_entity
        end
      end

      def update
        @post = current_user.posts.find(params[:id])

        if @post.update(post_params)
          render json: @post, status: :ok
        else
          render json: { errors: @post.errors },
                 status: :unprocessable_entity
        end
      end

      def destroy
        @post = current_user.posts.find(params[:id])
        @post.destroy
        head :no_content
      end

      private

      def post_params
        params.require(:post).permit(:title, :body, :status)
      end

      def pagination_meta(collection)
        {
          current_page: collection.current_page,
          total_pages: collection.total_pages,
          total_count: collection.total_count
        }
      end
    end
  end
end

10. 流式响应

# app/controllers/reports_controller.rb
class ReportsController < ApplicationController
  include ActionController::Live

  def export
    response.headers['Content-Type'] = 'text/csv'
    response.headers['Content-Disposition'] =
      'attachment; filename="report.csv"'

    # 流式传输CSV数据
    User.find_each do |user|
      response.stream.write "#{user.id},#{user.name},#{user.email}
"
    end
  ensure
    response.stream.close
  end

  def events
    # 服务器发送事件
    response.headers['Content-Type'] = 'text/event-stream'
    response.headers['Cache-Control'] = 'no-cache'

    10.times do |i|
      response.stream.write "data: #{i}

"
      sleep 1
    end
  ensure
    response.stream.close
  end
end

最佳实践

  1. 遵循REST约定 - 尽可能使用标准CRUD操作
  2. 保持控制器简洁 - 将业务逻辑移到模型/服务中
  3. 使用强参数 - 始终净化输入参数
  4. 优雅处理错误 - 实现适当的错误处理
  5. 使用before_action - 用过滤器减少重复代码
  6. 返回正确的状态码 - 使用语义化的HTTP状态码
  7. 实现适当的认证 - 保护您的控制器
  8. 使用respond_to支持多种格式 - 支持HTML、JSON等
  9. 利用闪存消息 - 提供用户反馈
  10. 版本化您的API - 使用命名空间进行API版本控制

常见陷阱

  1. 臃肿的控制器 - 在控制器中放入过多逻辑
  2. 缺少CSRF保护 - 未使用真实性令牌
  3. 弱参数 - 允许过多或错误的参数
  4. 无错误处理 - 未捕获异常
  5. 缺少授权 - 未检查用户权限
  6. 不一致的响应 - 相同场景返回不同状态码
  7. 会话臃肿 - 在会话中存储过多数据
  8. 缺少before_action - 操作间重复代码
  9. 错误的重定向 - 需要渲染时重定向
  10. 无限流限制 - API没有节流

适用场景

  • 使用Rails构建Web应用程序
  • 创建RESTful API
  • 实现MVC模式
  • 处理HTTP请求和响应
  • 构建管理界面
  • 创建CRUD界面
  • 实现认证流程
  • 构建多租户应用程序
  • 创建Webhook和回调
  • 开发内容管理系统

资源