[Ruby On Rails] Action Controller - 控制HTTP 流程

Controlling complexity is the essence of computer programming. — Brian Kernighan

HTTP通讯协定是一种Request-Response (请求-回应)的流程,客户端(通常是浏览器)向伺服器送出一个HTTP
request封包,然后伺服器就回应一个response封包。在上一章中,我们介绍了Rails如何使用路由来分派request到Controller的其中一个Action。而每个Action的任务就是根据客户端传来的资料与Model互动,然后回应结果给客户端。这一章中我们将仔细介绍负责回应请求的Controller。

ApplicationController

透过rails g controller指令产生出来的controller都会继承自ApplicationController。因此定义在这里的方法可以被所有Controller取用,你可以在这边定义一些共用的方法。预设的application_controller.rb长的如下:

class ApplicationController < ActionController::Base
  protect_from_forgery
end

其中的protect_from_forgery方法启动了CSRF安全性功能,所有非GET的HTTP
request都必须带有一个Token参数才能存取,Rails会自动在所有表单中帮你插入Token参数,预设的Layout中也有一行<%
= csrf_meta_tag %>
标签可以让JavaScript读取到这个Token。

但是当需要开放API给非浏览器客户端时,例如手机端或第三方应用的回呼(webhook),这时候我们会需要关闭这个功能,例如:

class ApisController < ApplicationController
  skip_before_action :verify_authenticity_token # 整个ApisController 关闭检查
end

CSRF 网路攻击http://en.wikipedia.org/wiki/Cross-site_request_forgery

注意,请将方法放在protected或private之下,如果是public方法,就会变成一个公开的Action可以给浏览器呼叫到。

产生Controller与Action

我们在Part1示范过,要产生一个Controller档案,请输入

rails g controller events

如此便会产生app/controllers/events_controller.rb,依照RESTful设计的惯例,所有的Controller命名都是复数,而档案名称依照惯例都是{name}_controller.rb。

一个Action就是Controller里的一个Public方法:

class EventsController < ApplicationController
  def show
    # ...
  end
end

除了 ??继承自ApplicationController,我们也可以继承更底层的ActionController::Metal,请参考Rails3:新的Metal机制

在Action方法中我们要处理request,基本上会做三件事情:
1.收集request的资讯,例如使用者传进来的参数2.操作Model来做资料的处理3.回传response结果,这个动作称作render

Request资讯收集

在Controller的Action之中,Rails提供了一些方法可以让你得知此request各种资讯,包括:

  • action_name目前的Action名称
  • cookies Cookie下述
  • headers HTTP标头
  • params包含用户所有传进来的参数Hash,这是最常使用的资讯
  • request各种关于此request的详细资讯,较常用的例如:
    • xml_http_request? 或xhr?,这个方法可以知道是不是Ajax 请求
    • host_with_port
    • remote_ip
    • headers
  • response代表要回传的内容,会由Rails设定好。通常你会用到的时机是你想加特别的Response
    Header。
  • session Session下述

正确的说,params这个Hash是ActiveSupport::HashWithIndifferentAccess物件,而不是普通的Hash而已。Ruby内建的Hash,用Symbol的hash[:foo]和用字串的hash["foo"]是不一样的,这在混用的时候常常搞错而取不到值,算是常见的臭虫来源。Rails在这里使用的ActiveSupport::HashWithIndifferentAccess物件,无论键是Symbol或字串,都指涉相同的值,减少麻烦。

Render结果

在根据request资讯做好资料处理之后,我们接下来就要回传结果给用户。事实上,就算你什么都不处理,Action方法里面空空如也,甚至不定义Action,Rails预设也还是会执行render方法。这个render方法会回传预设的Template,依照Rails惯例就是app/views/{controller_name}/{action_name}。如果找不到样板档案的话,会出现Template
is missing的错误。

当然,有时候我们会需要自定render,也许是指定不同的Template,也许是不需要Template。这时候有以下参数可以使用:

直接回传结果

  • render :text => "Hello"直接回传字串内容,不使用任何样板。
  • render :xml => @event.to_xml回传XML格式
  • render :json => @event.to_json回传JSON格式(再加上:callback就会是JSONP )
  • render :nothing => true空空如也

指定Template

  • :template指定Template,例如render
    :template => "index"
    或可以省略成render "index",如果是不同Controller的Template再加上Controller名称,例如render
    "events/index"
  • :action指定同一个Controller中另一个Action的Template (注意到只是使用它的Template,而不会执行该Action内的程式)

其他参数

  • :status设定HTTP
    status,预设是200,也就是正常。其他常用代码包括401权限不足、404找不到页面、500伺服器错误等。
  • :layout可以指定这个Action的Layout,设成false即关掉Layout

补充一提,在特定情况你想把render的结果存成一个字串,例如拿到局部样板Partials成为一个字串,这时候可以改使用render_to_string
:partial => "foobar"

Redirect

如果Action不要render任何结果,而是要使用者转向到别页,可以使用redirect_to

  • redirect_to events_url
  • redirect_to :back回到上一页。

注意,一个Action中只能有一个render或一个redirect_to。不然你会得到一个DoubleRenderError例外错误。

串流Sending data

如果需要回传二进位Binary资料,有两个方法可以使用:

send_data(data, options={})回传二进位字串,接受以下参数:

  • 其中data参数是二进位的字串:
  • :filename使用者储存下来的档案名称
  • :type预设是application/octet-stream
  • :disposition inline或attachment
  • :status预设是200

send_file(file_location, options={})回传一个档案,接受以下参数:

  • 其中file_location是档案路径和档名:
  • :filename使用者储存下来的档案名称
  • :type预设是application/octet-stream
  • :disposition inline或attachment
  • :status预设是200

不过实务上我们很少在上线环境上直接用Rails来推送静态档案,因为大档的传输时间会浪费宝贵的Rails运算资源。我们会改用X-Sendfile
Header将传档的任务委派给网页伺服器(例如Apache或Nginx )处理,来降低Rails伺服器的负担。或是搭配第三方云储存服务例如AWS
S3将传档的任务外包出去。

respond_to

我们在第六章RESTful应用程式中曾经示范过用法,respond_to可以用来回应不同的资料格式。Rails内建支援格式包括有:html,
:text, :js, :css, :ics, :csv, :xml, :rss, :atom, :yaml, :json
等。如果需要扩充,可以编辑config/initializers/mime_types.rb这个档案。

如果你想要设定一个else的情况,你可以用:any

respond_to do |format|
  format.html
  format.xml { render :xml => @event.to_xml }
  format.any { render :text => "WTF" }
end

另外,Rails也支援单行的简单写法:

respond_to :html, :json, :js

这样其实就是:

respond_to do |format|
  format.html
  format.json
  format.js
end

Sessions

HTTP是一种无状态的通讯协定,为了能够让浏览器能够在跨request之间记住资讯,Rails提供了Session功能,像是记住登入的状态、记住使用者购物车的内容等等,都是用Session实作出来的。

要操作Session,直接操作session这个Hash变数即可。例如:

session[:cart_id] = @cart.id

Session原理可以参考Session_ID,基本上也是利用浏览器的cookie来追踪requests请求。

Session storage

Rails预设采用Cookies
session storage来储存Session资料,它是将Session资料透过config/secrets.yml的secret_key_base编码后放到浏览器的Cookie之中,最大的好处是对伺服器的效能负担很低,缺点是大小最多4Kb,以及资料还是可以透过反编码后看出来,只是无法进行修改。因此安全性较低,不适合存放机密资料。

除了??Cookies session storage,Rails也支援其他方式,你可以修改config/initializers/session_store.rb:

  • :active_record_store使用资料库来储存
  • :mem_cache_store使用Memcached快取系统来储存,适合高流量的网站

一般来说使用预设的Cookies session storage即可,如果对安全性较高要求,可以使用资料库。如果希望兼顾效能,可以考虑使用Memcached。

采用:active_record_store的话,必须安装activerecord-session_store
gem,然后产生sessions资料表:

$ rails g active_record:session_migration
$ rake db:migrate

Cookies

除了??Session,我们也可以直接操作底层的Cookie,以下是一些使用范例:

# Sets a simple session cookie.
cookies[:user_name] = "david"

# Sets a cookie that expires in 1 hour.
cookies[:login] = { :value => "XJ-122", :expires => 1.hour.from_now }

# Example for deleting:
cookies.delete :user_name

cookies[:key] = {
   :value => ‘a yummy cookie‘,
   :expires => 1.year.from_now,
   :domain => ‘domain.com‘
}

cookies.delete(:key, :domain => ‘domain.com‘)

因为资料是存放在使用者浏览器,所以如果需要保护不能让使用者乱改,Rails也提供了Signed方法:

cookies.signed[:user_preference] = @current_user.preferences

另外,如果是尽可能永远留在使用者浏览器的资料,可以使用Permanent方法:

cookies.permanent[:remember_me] = [current_user.id, current_user.salt]

两者也可以加在一起用:

cookies.permanent.signed[:remember_me] = [current_user.id, current_user.salt]

Flash讯息

我们在Part1示范过用Flash来传递讯息。它的用处在于redirect时,能够从这一个request传递文字讯息到下一个request,例如从create
Action传递「成功建立」的讯息到show Action。

flash是一个Hash,其中的键你可以自定,常用:notice:warning:error等。例如我们在第一个Action中设定它:

def create
  @event = Event.create(params[:event])
  flash[:notice] = "成功建立"
  redirect_to event_url(@event)
end

那么在下一个Action中,我们就可以在Template中读取到这个讯息,通常我们会放在Layout中:

<p><%= flash[:notice] %></p>

或是直接用notice这个Helper:

<p><%= notice %></p>

使用过一次之后,Rails就会自动清除flash。

另外,有时候你等不及到下一个Action,就想让Template在同一个Action中读取到flash值,这时候你可以写成:

flash.now[:notice] = "foobar"

最后,Rails预设针对noticealert这两个类型可以直接塞进redirect_to当作参数,例如:

redirect_to event_url(@event), :notice => "成功建立"

你也可以自行扩充,例如新增一个warning:

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  add_flash_types :warning
  #...
end

# in your controller
redirect_to user_path(@user), warning: "Incomplete profile"

# in your view
<%= warning %>

Filters

可将Controller中重复的程式抽出来,有三种方法可以定义在进入Action之前、之中或之后执行特定方法,分别是before_actionafter_actionaround_action,其中before_action最为常用。这三个方法可以接受Code
block、一个Symbol方法名称或是一个物件( Rails会呼叫此物件的filter方法)。

before_action

before_action最常用于准备跨Action共用的资料,或是使用者权限验证等等:

class EventsControler < ApplicationController
  before_action :find_event, :only => :show

  def show
  end

  protected

  def find_event
    @event = Event.find(params[:id])
  end

end

每一个都可以搭配:only:except参数。

around_action

# app/controllers/benchmark_filter.rb
class BenchmarkFilter
    def self.filter(controller)
     timer = Time.now
     Rails.logger.debug "---#{controller.controller_name} #{controller.action_name}"
     yield # 这里让出来执行Action动作
     elapsed_time = Time.now - timer
     Rails.logger.debug "---#{controller.controller_name} #{controller.action_name} finished in %0.2f" % elapsed_time
    end
end

# app/controller/events_controller.rb
class EventsControler < ApplicationController
    around_action BenchmarkFilter
end

Filter的顺序

当有多个Filter时,Rails是由上往下依序执行的。如果需要加到第一个执行,可以使用prepend_before_action方法,同理也有prepend_after_actionprepend_around_action

如果需要取消从父类别继承过来的Filter,可以使用skip_before_action
:filter_method_name
方法,同理也有skip_after_actionskip_around_action

rescue_from

rescue_from可以在Controller中宣告救回特定的例外,改用你指定的方法处理,例如:

class ApplicationController < ActionController::Base

    rescue_from ActiveRecord::RecordInvalid, :with => :show_error

    protected

    def show_error
        # render something
    end

end

那些没有被拦截到的错误例外,使用者会看到Rails预设的500错误画面。一般来说比较常会用到rescue_from的时机,可能会是使用某些第三方函式库,该函式库可能会丢出一些例外是你想要做额外的错误处理。例如在pundit这个检查权限的套件,如果发生权限不够的情况,会丢出Pundit::NotAuthorizedError的例外,这时候就可以捕捉这个例外,改成回到首页:

rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized

protected

def user_not_authorized
 flash[:alert] = I18n.t(:user_not_authorized)
 redirect_to(request.referrer || root_path)
end

顺道一提,关于如何设计好例外处理,可以参考笔者的一份投影片:Exception
Handling: Designing Robust Software in Ruby

HTTP Basic Authenticate

Rails内建支援HTTP
Basic Authenticate,可以很简单实作出认证功能:

class PostsController < ApplicationController
    before_action :authenticate

    protected

    def authenticate
     authenticate_or_request_with_http_basic do |username, password|
       username == "foo" && password == "bar"
     end
    end
end

或是这样写:

class PostsController < ApplicationController
    http_basic_authenticate_with :name => "foo", :password => "bar"
end

侦测客户端装置提供不同内容

透过设定request.variant我们可以提供不同的Template内容,这可以拿来针对不同的客户端装置,提供不同的内容,例如利用request.user_agent来自动侦测电脑、手机和平板装置:

class ApplicationController < ActionController::Base

before_action :detect_browser

private

def detect_browser
  case request.user_agent
    when /iPad/i
      request.variant = :tablet
    when /iPhone/i
      request.variant = :phone
    when /Android/i && /mobile/i
      request.variant = :phone
    when /Android/i
      request.variant = :tablet
    when /Windows Phone/i
      request.variant = :phone
    else
      request.variant = :desktop
   end
end

接着在需要支援的action中,加上

def index
  # ...
  respond_to do |format|
    format.html
    format.html.phone
    format.html.tablet
  end
end

Template的命名则是index.html+phone.erb和index.html+tablet.erb。

本文章来自于https://ihower.tw/rails4/

时间: 2024-09-29 13:47:14

[Ruby On Rails] Action Controller - 控制HTTP 流程的相关文章

Asp.Net MVC 权限控制(三):Controller和Action级别控制

续接上篇:Asp.Net MVC 权限控制(二):Controller级别控制 再次在重构!这次对Controller和Action进行验证. 思路:系统有很多功能集,功能集对应很多Controller和Action,角色分配很多功能集. 首先构建一个基础数据: 1.功能集初始化: /// <summary> /// 系统模块 /// </summary> public class SystemModule { public SystemModule() { this.ID = G

Ruby on Rails开发Web应用的基本概念

Web应用架构 C/S架构 Web应用从最初就採用C/S架构.Server负责监听client请求,提供资源,Client向server发起请求并渲染页面.两者通过TCP/IP协议栈之上的HTTP协议通信. 多层架构 在Web 2.0时代,随着交互性的要求,这个架构变得更为复杂.Server须要提供更复杂的服务,Client也要完毕很多其它的交互任务,涌现出非常多新的提供更快更好服务的技术.对应的,C/S架构须要以一种更复杂的方式来组织,即多层架构. 多层架构中的每一层负责提供一个特定的功能,与

理解ruby on rails中的ActiveRecord::Relation

ActiveRecord::Relation是rails3中添加的.rails2中的finders, named_scope, with_scope 等用法,在rails3统一为一种Relation用法. 以下是返回ActiveRecord::Relation的方法: bind create_with distinct eager_load extending from group having includes joins limit lock none offset order preloa

2--Windows下: RubyMine + Ruby On Rails + mysql 搭建开发环境

最近在接手一个手机项目.在搭建环境的过程中,遇到了一些问题,在下文中已做记录,并奉上个人的解决方案. 开发环境 win2003 ;  JetBrains RubyMine6.3.3 1.  下载最新版ruby,(rubyinstaller-2.0.0-p598.exe ,最新版) 官网:http://rubyinstaller.org/downloads/ 2.  安装ruby 双击安装,安装过程出现如下界面.如图 这里我们选择安装路径为 D:\Ruby200. 下面有3个选项分别是:(1) 是

Ruby on Rails: 使用devise+cancan+rolify建立完整的权限管理系

devise.cancan和rolify这三个组件结合,可以建立完整而强大的用户权限模型. devise介绍,负责用户注册.登录.退出.找回密码等操作.细节参考devise on github cancan介绍, 负责角色建立.对角色授权.在页面中根据授权是否显示元素,以及模型中超出授权时抛出异常.细节参考rolify on github rolify介绍,负责将用户与角色关联.细节参考rolify on github 下面就简单介绍下这三者结合使用的方法,比较浅,深层次的大家自己去看文档挖掘,

Ruby On Rails中REST API使用示例——基于云平台+云服务打造自己的在线翻译工具

做为一个程序员可能在学习技术,了解行业新动态,解决问题时经常需要阅读英文的内容:而像我这样的英文小白就只能借助翻译工具才能理解个大概:不禁经常感慨,英文对学习计算机相关知识太重要了!最近发现IBM的云平台Blumemix,并且提供语言翻译的服务,感觉不错,就拿来研究学习一下:这里就分享一下我的研究学习过程,如何使用Ruby On Rails调用REST API打造自己的在线翻译工具,并演示如何把它发布到云平台上,让每个人都可以通过网络访问使用它. 应用效果展示 您可以通过点击效果图片的链接访问它

10 steps to get Ruby on Rails running on Windows with IIS FastCGI

Since the original tech preview release of FastCGI last year, we've been seeing a lot of requests for getting Ruby on Rails running with our FastCGI.  Theoretically, since the FastCGI component uses a standard protocol to support FastCGI-enabled appl

[ruby on rails] 跟我学之路由映射

前面<[ruby on rails] 跟我学之Hello World>提到,路由对应的文件是 config/routes.rb 实际上我们只是添加了一句代码: resources :posts 但是这个代码默认的路由却有多个,可以通过 rake routes进行查看,如下: [email protected]:/home/ywt/ror_tests/blog# rake routes Prefix Verb URI Pattern Controller#Action posts GET /po

ruby on rails nginx 如何上传大文件?

用ruby on rails开发的web,用了carrierwave和dropzone实现了上传文件.但后来发现,一旦文件大于200M时,就不行了,特别慢,虽说carrierwave有个move_to_cache.move_to_store的选项,但好像起不了作用.于是又去研究其它的上传方式,之后发现nginx的upload module比较靠普,但这样做有一个问题就是nginx必须是编译安装的,要把upload module一块编译进行才能用.在这里记录一下实现的具体流程. 一.编译安装ngi