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
最佳实践
- 遵循REST约定 - 尽可能使用标准CRUD操作
- 保持控制器简洁 - 将业务逻辑移到模型/服务中
- 使用强参数 - 始终净化输入参数
- 优雅处理错误 - 实现适当的错误处理
- 使用before_action - 用过滤器减少重复代码
- 返回正确的状态码 - 使用语义化的HTTP状态码
- 实现适当的认证 - 保护您的控制器
- 使用respond_to支持多种格式 - 支持HTML、JSON等
- 利用闪存消息 - 提供用户反馈
- 版本化您的API - 使用命名空间进行API版本控制
常见陷阱
- 臃肿的控制器 - 在控制器中放入过多逻辑
- 缺少CSRF保护 - 未使用真实性令牌
- 弱参数 - 允许过多或错误的参数
- 无错误处理 - 未捕获异常
- 缺少授权 - 未检查用户权限
- 不一致的响应 - 相同场景返回不同状态码
- 会话臃肿 - 在会话中存储过多数据
- 缺少before_action - 操作间重复代码
- 错误的重定向 - 需要渲染时重定向
- 无限流限制 - API没有节流
适用场景
- 使用Rails构建Web应用程序
- 创建RESTful API
- 实现MVC模式
- 处理HTTP请求和响应
- 构建管理界面
- 创建CRUD界面
- 实现认证流程
- 构建多租户应用程序
- 创建Webhook和回调
- 开发内容管理系统