The Rails Way 读书笔记 | Chapter 2

Working with Controllers

Remove all Business Logic from your Controllers and put it in the model. Your
Controllers are only responsible for mapping between URLs (including other
HTTP Request data), coordinating with your Models and your Views, and channeling
that back to an HTTP response. Along the way, Controllers may do
Access Control, but little more. These instructions are precise, but following
them requires intuition and subtle reasoning.
—Nick Kallen, Pivotal Labs



The Dispatcher: Where It All Begins

Rails一般是被用来建立基于web的应用,所以在任何事情发生之前,会有一个web server来处理一个请求,比如Apache,Lighttpd,Nginx等等。然后服务器转发这些请求给Rails应用。 那么这个转发过程就是被dispatcher来处理的。

Request Handling
 
服务器通常会传递一些信息给dispatcher:
• The request URI ([url]http://localhost:3000/timesheets/show/3[/url], or whatever)
The CGI environment (bindings of CGI parameter names to values)

dispatcher的工作就是:
• 指定哪个controller来处理这个请求
指定哪个action会被执行
加载正确的controller文件,包含一个controller class的Ruby 类定义。
创建一个controller 类的实例。
告诉这个实例去执行正确的action。

Getting Intimate with the Dispatcher
 看个例子,我们有一个rails应用
$ rails dispatch_me
$ cd dispatch_me/
$ ruby ./script/generate controller demo index
在生成的views/demo/index.html.erb里加上:“hello”
然后:
$ ruby script/console
Loading development environment.
>>
然后
>> ENV[‘REQUEST_URI’] = “/demo/index”
=> “/demo/index”
>> ENV[‘REQUEST_METHOD’] = “get”
=> “get”
再执行
>> Dispatcher.dispatch

你会看到:
Content-Type: text/html; charset=utf-8
Set-Cookie: _company_session=BAh7BiIKZmxhc2hJQzonQWN0aW9uQ29udHJvbGxlcjo6Rmxhc2g6OkZsYXNoSGFzaHsABjoKQHVzZWR7AA%3D%3D--8bb9991e86a5883e151cd9d48a59886750685ccb; path=/; HttpOnly
Status: 200 OK
ETag: "2bbb5b6934b907513f16b9279d547207"
X-Runtime: 6ms
Content-Length: 16
Cache-Control: private, max-age=0, must-revalidate

<h1>Hello</h1>

=> #<IO:0x12fdcc>

我们执行了Ruby类Dispatcher的类方法dispatch,执行的结果是:index action执行了,index的模板被渲染了。

在public目录下,你会看到三个文件:
$ ls dispatch.*
dispatch.cgi dispatch.fcgi dispatch.rb

每次当请求过来的时候,就靠这些文件其中的一种来处理了,哪个文件去处理依赖的是你服务器的配置。最终他们还是做的同一件事情:它们会调用 Dispatcher.dispatch,就像我们在控制台里做的那样。

Render unto View…
controller action的终极任务就是渲染一个view模板。通常是从服务端返回给客户端一个html文档。如果你没有定义action,那么只有一个模板,那么也可以正常工作(不传递变量的情况下)。
你可以用上面的例子来测试,在控制台里,如果修改了代码的话,控制台需要重新加载。可以退出重新打开,或者使用命令:
reload!


When in Doubt, Render
render是action的默认行为

Explicit Rendering
Rendering Another Action’s Template
通常情况下,当你需要重新显示一个form表单的时候,会render另外一个action:

class EventController < ActionController::Base
def new
# This (empty) action renders the new.rhtml template, which
# contains the form for inputting information about the new
# event record and is not actually needed.
end
def create
# This method processes the form input. The input is available via
# the params hash, in the nested hash hanging off the :event key.
@event = Event.new(params[:event])
if @event.save
flash[:notice] = “Event created!”
redirect_to :controller => “main” # ignore this line for now
else
render :action => “new” # doesn’t execute the new method!
end
end
end

当@event.save返回false的时候,就会render new 的模板,new.rhtml。而且会自动显示错误信息到new页面上。但是event对象并不会被保存。这里,new.rhtml实际上它自己并不“知道”自己被谁render的, 它只需要完成它自己的工作就行,所以地位十分低下:)


Rendering a Different Template Altogether
你可以使用render :template 和 :file参数来render不同的模板。值得注意的是:
:template后面跟的是相对路径,相对于/app/views
:file后面跟的是绝对路径。

render :template => “abuse/report” # renders
app/views/abuse/report.rhtml
render :file => “/railsapps/myweb/app/views/templates/common.rhtml”

Rendering a Partial Template
render一个局部模板,使用:partial参数。

Rendering Inline Template Code
render :inline => “<%= auto_complete_result(@headings, ‘name’) %>”

Rendering Text
render :text => ‘Submission accepted’

Rendering其他数据结构
render :json => @record.to_json
render :xml => @record.to_xml

Rendering Nothing
为了避免safari的bug,实际上是给浏览器传一个空白字符。
render :nothing => true, :status => 401 # Unauthorized

Rendering Options
:content_type
:layout
:status


Redirecting  重定向
重定向实际上是终止当前请求, 然后初始化另一个新的请求。
def create
@event = Event.new(params[:event])
if @event.save
flash[:notice] = “Event created!”
Redirecting 39
redirect_to :controller => “main”
else
render :action => “new”
end
end
如果保存成功就会重定向到main controller的index action了。为什么不用render呢?
请记住,代码在redirect或render调用之后仍然会运行,应用在数据发送到浏览器之前一直会等待。如果你有复杂的逻辑,你经常想在redirect或render之后return:
def show
@user = User.find(params[:id])
if @user.activated?
render :action => ‘activated’ and return
end
case @user.info
...
end
end
这样会引发DoubleRenderError异常。假设main/index是这样:
def index
@events = Event.find(:all)
end

如果你从event/create action 那render到index.rhtml的话,这个main/index action将不会被执行。所以@events变量不会被初始化。
这就是用redirect的原因, 它会创建一个新的request,触发一个新的action,并且开始决定render什么。


Controller/View Communication
一个view模板通常是被controller从数据库里提出来的数据渲染的。换句话说,就是这个controller从model层得到你需要的数据,返回给view层。

Rails这种controller-to-view的数据处理是通过实例属性来实现的。通常,一个controller action会初始化一个或多个实例属性。这些实例属性都能被view使用。

这里有点疑惑,为什么这些实例属性就能在controller和view之间共享数据呢? 主要原因是, 实例属性的存在导致对象(不管是controller对象,还是字符串对象等等)都能持有这些它们不能共享给其他对象的数据。当你的controller action被执行的时候,整个controller对象的上下文会发生一些事情。这里包含了一个事实:这里的每个实例属性都属于controller实例。当view模板被渲染的时候,上下文是那些不同controller对象中的一个和一个ActionView::Base 的实例,这个实例有它自己的实例属性,并且不会访问那些controller对象。 这里真相就出来了,Rails对每一个controller对象变量都会做相同的事情:为那个view对象用和controller实例属性相同的名字以及相同的数据创建一个实例属性。
有点失望? 


Filters
举例子:
before_filter :require_authentication
你也可以这样:
before_filter  :security_scan, :audit, :compress
你也可以把它们写成三行。

你应该确保你的过滤方法是protected或是private的,以免被当作公共的action来被使用。

Filter Inheritance
父类的过滤器可以被子类继承。
class ApplicationController < ActionController::Base
after_filter :compress

class User  < ApplicationController
...
end

子类也可以增加自己的filter或者跳过从父类继承的filter。
class User  < ApplicationController
skip_after_filter :compress
before_filter :audit

private
def audit
     #record some log
end
...
end
(skip_before_filter,skip_after_filter,skip_filter)

Filter Types
一个过滤器可以带三种形式:方法引用(符号型), 外部类,或者是内部方法(proc)

第一种是最常见的。上面的例子就是。

来看看其他形式的。
Filter Classes
使用一个外部类更容易复用一个通用的过滤方法。例如:
class OutputCompressionFilter
def self.filter(controller)
controller.response.body = compress(controller.response.body)
end
end
class NewspaperController < ActionController::Base
after_filter OutputCompressionFilter
end
这个类的self.filter方法中会被传入要过滤的那个controller实例。

Inline Filter Method
内部过滤方法常常用来做一些小事情。看个例子:
class WeblogController < ActionController::Base
before_filter {|controller| false if controller.params[“stop”]}
end

这意味着,这个块可以访问request或response对象里的方法,比如params,session,template等。这里并不一定是需要一个块,也可以是一个Porc对象,或是Method对象。

Filter Chain Ordering
有时候你可能要指定filter的顺序。那么你可以用 prepend_before_filter 和prepend_after_filter. 看例子:
class ShoppingController < ActionController::Base
before_filter :verify_open_shop
class CheckoutController < ShoppingController
prepend_before_filter :ensure_items_in_cart, :ensure_items_in_stock

CheckoutController的filter链的顺序是,:ensure_items_in_cart, :ensure_items_in_stock,:verify_open_shop
所以,如果ensure开头的filter方法如果返回false,那么你就不会看到这个shop是不是open,因为方法链被中断了。

Around Filters
Around Filters会包裹一个action整个生命周期。用法,传入一个符号类型,在filter方法内部使用yield(或者block.call)。 看例子:
around_filter :catch_exceptions
private
def catch_exceptions
yield
rescue => exception
logger.debug “Caught exception! #{exception}”
raise
end

使用block的形式:
around_filter do |controller, action|
logger.debug “before #{controller.action_name}”
action.call
logger.debug “after #{controller.action_name}”
end

使用外部类的形式:
around_filter BenchmarkingFilter
class BenchmarkingFilter
def self.filter(controller, &block)
Benchmark.measure(&block)
end
end

如果你在around_filter方法里使用before after方法的话,要想执行after方法, 必须是before方法返回true的情况。看例子:
around_filter Authorizer
class Authorizer
# This will run before the action. Returning false aborts the action
def before(controller)
if user.authorized?
return true
else
redirect_to login_url
return false
end
end
def after(controller)
# runs after the action only if the before returned true
end
end

Filter Chain Skipping
你在一个父类里声明了一个filter链,但是子类里可能不需要,那么需要跳这个filter链:
class ApplicationController < ActionController::Base
before_filter :authenticate
around_filter :catch_exceptions
end
class SignupController < ApplicationController
skip_before_filter :authenticate
end
class ProjectsController < ApplicationController
skip_filter :catch_exceptions
end

Filter Conditions
使用filter条件可以指定filter的action。写法有两种:
:only => :index) or arrays of actions
(:except => [:foo, :bar]).
看个例子
class Journal < ActionController::Base
before_filter :authorize, :only => [:edit, :delete]
around_filter :except => :index do |controller, action_block|
results = Profiler.run(&action_block)
controller.response.sub! “</body>”, “#{results}</body>”
end
private
def authorize
# Redirect to login unless authenticated.
end
end


Filter Chain Halting
before_filter和around_filter可能会在controller的action执行之前会中断请求,这是非常有用的。例如,可以拒绝一个非法访问的请求。正如前面所说,当一个filter返回false的时候,会中断filter链,你也可以使用render或redirect_to来中断filter链。

如果filter链中断的时候,after filter将不会被执行。
如果一个around filter在yielding之前return,可以有效的中断链。任何after filter都不会被运行。
如果before filter返回false,第二段的around filter仍然会执行,但是action方法不会被执行。任何after filter都不会被执行。


Streaming
据说这是一个很少人知道的事实 ?
Rails提供了二进制流的支持。好像敏捷那本书上就说过。应该是大部分人都知道的知识点。Rails提供了两个方法:
ActionController::Streaming module: send_data 和 send_file

先来看看有用的这个:
send_data(data, options = {})
这个方法允许你以命名文件的方式发送文本或二进制数据给用户。
Options for send_data
• :filename 指定文件名
• :type 指定 HTTP文本类型.默认是 ‘application/octetstream’.
• :disposition 指定文件是inline显示还是下载。
Valid values are inline and attachment (default).
:status 指定响应状态码,默认是‘200  OK’.

Usage Examples
来看个使用的例子:
创建一个动态生成的tarball文件下载:
send_data generate_tgz(‘dir’), :filename => ‘dir.tgz’
来看一个验证码的实现例子, 是给浏览器动态发送图片:

require ‘RMagick’
class CaptchaController < ApplicationController
def image
# create an RMagic canvas and render difficult to read text on it
...
image = canvas.flatten_images
image.format = “JPG”
# send it to the browser
send_data(image.to_blob, :disposition => ‘inline’,
:type => ‘image/jpg’)
end
end


send_file(path, options = {})
这个方法每次给客户端传送4096bytes的流文件。API文档说:此方式不需要一次读入整个文件,这就可以用来送大型文件。不过不幸的是,这不是真的。当你在运行于Mongrel上的Rails应用(大多数人用mongrel)里使用send_file的时候,整个文件完全被读入内存里了。因此,使用send_file去发送一个大文件仍然是个头疼的问题。下一节会说到这个问题。使用的时候要当心,要对来自一个Web页的path参数进行清理。send_file(@params[‘path’]) 允许一个怀有恶意的用户下载你的服务器上的任何文件。

Options:
:filename – 建议浏览器使用的文件名。默认值是File.basename(path)。
:type -指定一个HTTP content 类型。缺省是 ‘application/octet-stream’。
:disposition - 指定文件是内联显示还是下载。有效的值是‘inline’ 和 ‘attachment’ (缺省值)。
:stream – 是否发送文件给用户代理,以在发送前可读取整个文件。默认值为true。
:buffer_size – 指定用于文件流的缓冲区大小(按 bytes)。默认值为4096。
:url_based_filename - 如果你想让浏览器去猜你的文件名应该设为true。可能会在i18n的时候使用(使用:filename会覆盖这个选项)

大多数的options都是被ActionController::Streaming模块的私有方法send_file_headers!来处理的。

许多浏览器上缺省的Content-Type 和 Content-Disposition headers 被设置成下载任意的二进制文件。IE 的 4, 5, 5.5, 和 6 版本有特殊变化 (尤其是下载 SSL)。
简单的下载:
send_file '/path/to.zip'
在浏览器内显示JPEG :
send_file '/path/to.jpeg', :type => 'image/jpeg', :disposition => 'inline'

send_file ‘/path/to/404.html,
:type => ‘text/html; charset=utf-8’,
:status => 404

send_file @video_file.path,
:filename => video_file.title + ‘.flv’,
:type => ‘video/x-flv’,
:disposition => ‘inline’
如果你想提供给用户更多的信息,可读取其它的Content-* HTTP headers (例如 Content-Description)。

Letting the Web Server Send Files
解决send_file那个消耗内存的问题,可以使用Apache,Lighttpd,Nginx提供的杠杆功能。
Apache和Lighttpd这样设置:
response.headers[‘X-Sendfile’] = path
Nignx这样设置:
response.headers[‘X-Accel-Redirect’] = path
然后再告诉Rails controller action方法不要去处理任何事情,因为web服务器会帮你处理:
render :nothing => true

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值