改善Rails3中render json的性能

在上一篇文章Rails3中的JSON知道render :json => @xxx其实是调用的to_json方法,而to_json其实是ActiveSupport::JSON.encode方法。

在数据量比较小的时候,性能还可以,但是当数据量很大的时候,性能非常差,这里我们可以用一些第三方的快的json库来解析:

我使用的是oj,配合MultiJson(Rails3中已经有了,但是只在decode的时候有用到)

# Gemfile
gem 'oj'

# application.rb
MultiJson.engine = :oj

render :json => MultiJson.encode(api_response)

其他一些json库

supported-json-engines

Rails3中的JSON

Rails 3.2.12

to_json

#to_json是由gem ‘json’引入的,但是ActiveSupport重写了,和as_json联系起来

[Object, Array, FalseClass, Float,
 Hash, Integer, NilClass, String, TrueClass].each do |klass|
  klass.class_eval do
    def to_json(options = nil)
      ActiveSupport::JSON.encode(self, options)
    end
  end
end

ActiveSupport::JSON#encode

def encode(value, use_options = true)
  check_for_circular_references(value) do
    jsonified = use_options ? value.as_json(options_for(value)) : value.as_json
    jsonified.encode_json(self)
  end
end

#as_json

这个直接看api就知道了,http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html

render :json => @xxx

来看看render :json是如何实现的:

actionpack/lib/action_controller/metal/renderers

add :json do |json, options|
  json = json.to_json(options) unless json.kind_of?(String)
  json = "#{options[:callback]}(#{json})" unless options[:callback].blank?
  self.content_type ||= Mime::JSON
  json
end

可以看到就是调用了to_json,如果是String就直接返回,如果有callback就返回jsonp的格式

另外我们可以添加新的`renderer`:

ActionController::Renderers.add :csv do |obj, options|
  filename = options[:filename] || 'data'
  str = obj.respond_to?(:to_csv) ? obj.to_csv : obj.to_s
  send_data str, :type => Mime::CSV,
   :disposition => "attachment; filename=#{filename}.csv"
end

# 使用

render :csv => @csvable, :filename => @csvable.name

总结:尽量不要在rails3中重写to_json,如果确实需要自己构造json,重写as_json方法。

rails中的一些技巧

这篇文章可以说是10-things-you-didnt-know-rails-could-do10-most-underused-activerecord-relation-methods/的读书笔记,只列出我不熟悉的,所以如果想看全部内容可以参考上面的链接

1)pluck

原先我代码里确实充斥着published_book_titles = Book.published.map(&:title)

Good:

published_book_titles = Book.published.pluck(:title)

2)find_by (rails 4 only)

很多时候,我找单条记录一般是这样:

Book.where(:title => 'Three Day Road', :author => 'Joseph Boyden').first

更常用的是使用dynamic finder:

Book.find_by_title_and_author 'Three Day Road', 'Joseph Boyden'

如果使用find_by就可以这样:

Book.find_by(:title => 'Three Day Road', :author => 'Joseph Boyden')
# 或者你可以写成这样
Book.where(:title => 'Three Day Road').find_by :author => 'Joseph Boyden'

参考这边的讨论,可以看出find_by的目的是取代使用method_missing的dynamic finder,find_by的实质就是where().first,所以如果你不想等rails4,自己写一个也行。

3)none (rails 4 only)

其实不是返回空数组,而是返回一个NullRelation的实例,他的应用场景在于期望返回0条记录但是又可以使用chainable(查询链),官方的example:

@posts = current_user.visible_posts.where(:name => params[:name])
# => the visible_post method response has to be a chainable Relation

def visible_posts
  case role
  if 'Country Manager'
    Post.where(:country => country)
  if 'Reviewer'
    Post.published
  if 'Bad User'
    Post.none # => returning [] instead breaks the previous code
  end
end

并且类似于Post.none.published,不会向数据库发起请求。相关讨论pull request

4)scoping 和 unscoped

scoping这个用的不多

Comment.where(:post_id => 1).scoping do
  Comment.first # SELECT * FROM comments WHERE post_id = 1
end

unscoped用来取消default_scope的作用

class Post < ActiveRecord::Base
  def self.default_scope
    where :published => true
  end
end

Post.all          # Fires "SELECT * FROM posts WHERE published = true"
Post.unscoped.all # Fires "SELECT * FROM posts"

Post.unscoped {
  Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10"
}

需要注意的是scope chain不受影响,下面两个等价:

Post.unscoped.published
Post.published

5)first_or_create 和 first_or_initialize

# Find the first user named Penélope or create a new one.
User.where(:first_name => 'Penélope').first_or_create

User.where(:first_name => 'Scarlett').first_or_create(:last_name => 'Johansson')

6)merge

有篇文章提到了merge这个方法,我特意去看了源码,还发现了几个有意思的地方。

关于default_scope
如果定义多个default_scope,他们会合并在一起,这里就用了merge:

class Article < ActiveRecord::Base
  default_scope { where(:published => true) }
  default_scope { where(:rating => 'G') }
end

还可以自定义default_scope方法:

class Article < ActiveRecord::Base
  def self.default_scope
    # Should return a scope, you can call 'super' here etc.
  end
end

merge在ActiveRecord::SpawnMethods模块中:

# Merges in the conditions from other,
# if other is an ActiveRecord::Relation.
Post.where(:published => true).joins(:comments).merge( Comment.where(:spam => false) )

# 如果传入的是数组则返回数组
recent_posts = Post.order('created_at DESC').first(5)
Post.where(:published => true).merge(recent_posts)

merge还是很有用的,类似的方法还有except和only

except:

# discards the order condition
Post.order('id asc').except(:order)

# discards the where condition but keeps the order
Post.where('id > 10').order('id asc').except(:where)

only:

# discards the order condition
Post.order('id asc').only(:where)

# uses the specified order
Post.order('id asc').only(:where, :order)

7)rake notes

原先我都是用一些TODO list软件,这里可以直接通过rake命令来提醒自己

class UserController < ApplicationController
  # TODO: BalaBala...
  # FIXME: ...
  # OPTIMIZE: ...
  # CUSTOM: ...
end

# 使用 use it!
rake notes
rake notes:todo
rake notes:fixme
rake notes:custom ANNOTATION=CUSTOM

8)rails r

有时候我们想进rails终端做简单的测试,很多时候都是rails c
如果不复杂可以直接用:

rails r 'p [Article, Comment, User].map(&:count)'
# => [0,0,0]

9)直接使用helper测试helper方法

原先我都是通过include xxx模块在console中调用helper方法的,原来还有这么简单的方式:

helper.time_ago_in_words 3.days.ago
# '3 days'

10)rails generate resource command

rails g resource user name:index email:uniq token:string{6} bio:text

rails g resource article user:references subject body:text

rails g resource comment user:belongs_to article:belongs_to body:text

11)inquiry,except

inquiry:

env = "production".inquiry
env.production?  # => true
env.development? # => false

except:

params = {controller: 'home', action: 'index', from: 'Google'}
params.except :controller, :action
# => {:from => 'Google'}

How does rails server static file in public directory

Rails3是如何处理在public目录下的静态文件的?

首先要说的是,Rails3.1在生产环境下默认是不开启静态文件服务的,因为Apache或者Nginx这些web服务器可以帮我们做这些,所以如果在生产环境下出现404,您可能需要在config/environments/production.rb中开启:

# Disable Rails's static asset server (Apache or nginx will already do this)
config.serve_static_assets = true

我们知道在Rails3之后,Rails和Rack的关系很大,提到Rack就不得不说中间件了(middleware),先来看看Rails中用到了哪些middleware:

rake middleware

use ActionDispatch::Static
use Rack::Lock
use ActiveSupport::Cache::Strategy::LocalCache
use Rack::Runtime
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::DebugExceptions
use ActionDispatch::RemoteIp
use Rack::Sendfile
use ActionDispatch::Callbacks
use ActiveRecord::ConnectionAdapters::ConnectionManagement
use ActiveRecord::QueryCache
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ParamsParser
use Rack::MethodOverride
use ActionDispatch::Head
use ActionDispatch::BestStandardsSupport
run Blog::Application.routes

OK,我们可以看到ActionDispatch::Static就是这个,是用来处理静态文件的。
看过ActionDispatch::Static的源码不难发现,用的主要是FileHandler:

class Static
  def initialize(app, path, cache_control=nil)
    @app = app
    @file_handler = FileHandler.new(path, cache_control)
  end
end

FileHandler符合标准的Rack中间件的定义,相应call方法:

class FileHandler
 def initialize(root, cache_control)
   @root          = root.chomp('/')
   @compiled_root = /^#{Regexp.escape(root)}/
   @file_server   = ::Rack::File.new(@root, cache_control)
 end

 def call(env)
   @file_server.call(env)
 end
end

原来就是`rack/utils`中的Rack::File了。Rack::File通过Request的路径到相应的目录下寻找文件。

通过ActionDispatch::Static的测试用例也可以看出它的作用:

def test_serves_static_index_at_root
  assert_html "/index.html", get("/index.html")
  assert_html "/index.html", get("/index")
  assert_html "/index.html", get("/")
  assert_html "/index.html", get("")
end

def test_serves_static_file_in_directory
  assert_html "/foo/bar.html", get("/foo/bar.html")
  assert_html "/foo/bar.html", get("/foo/bar/")
  assert_html "/foo/bar.html", get("/foo/bar")
end

def test_serves_static_index_file_in_directory
  assert_html "/foo/index.html", get("/foo/index.html")
  assert_html "/foo/index.html", get("/foo/")
  assert_html "/foo/index.html", get("/foo")
end

OK,到这里就应该清楚了,下面就只要指定路径就行了,也就是”Public”,在Rails::Application文件中:

if config.serve_static_assets
  middleware.use ::ActionDispatch::Static, paths["public"].first, config.static_cache_control
end

How to test mount rack app in rails3

在测试路由的时候,可以通过:

class RouteTest < ActionController::TestCase
  assert_routing '/posts/1', { :controller => "posts", :action => "show", :id => "1" }
end

我们也知道:

route = Dummy::Application.routes
route.recognize_path "/posts/1"
#=> { :controller => "posts", :action => "show", :id => "1" }

route.generate :controller => "posts", :action => "show", :id => "1"
#=> "/posts/1"

但是对于下面这种情况:

My::Application.routes.draw do
  mount Rack::App => "/rack"
end

通过route是找不到的

route = My::Application.routes
route.recognize_path "/rack"
#=> "No route matches /rack"

如何测试呢?现提供下面这个方法:

routes = Dummy::Application.routes.routes
server = routes.select { |r| r.app.instance_of?(Rack::App) }.pop
# test mount successful
assert !server.nil?
path   = server.path.spec.to_s
# test mount path correct
assert_equal('/rack', path)

注意:上面的测试只是覆盖了Rack App是否挂载在正确的路径上,而不会测试返回的内容是否正确!

Sprockets and Rails3.1

rails3.1的Asset Pipeline已经用的很Cool了. `Asset Pipeline`主要是通过Sprockets提供支持.

Sprockets

Sprockets很简单,需要一个Sprockets Environment实例来处理所有的assets. 在Rails中就是 `YourApp::Application.assets`,在终端中输出看下:

YourApp::Application.assets

#=>#<Sprockets::Environment:0x1c3c840 root="/xxx/xxxx/your_app", paths=["...", ...], digest="aa7d0db7619379e13b08335dee027df2">

可以看到这里有个paths实例变量,里面都是默认的rails的assets路径,并且这个paths可以在application.rb中配置的:

config.assets.paths << "#{Rails.root}/foo/bar"

Sprockets::BundledAsset

e = YourApp::Application.assets
asset = e['application.js']
# => #<Sprockets::BundledAsset ...>
asset.to_s
# => The content of asset
asset.length
asset.mtime
asset.pathname

javascript_include_tag “application”

其实就是在Sprockets Environment实例的paths中,寻找’application.js’文件,找到就返回其内容.

Sprockets Directives
  • require
  • require path inserts the contents of the asset source file specified by path. If the file is required multiple times, it will appear in the bundle only once.

  • include
  • include path works like require, but inserts the contents of the specified source file even if it has already been included or required.

  • require_directory
  • require_directory path requires all source files of the same format in the directory specified by path. Files are required in alphabetical order.

  • require_tree
  • require_tree path works like require_directory, but operates recursively to require all files in all subdirectories of the directory specified by path.

  • require_self
  • require_self tells Sprockets to insert the body of the current source file before any subsequent require or include directives.

  • depend_on
  • depend_on path declares a dependency on the given path without including it in the bundle. This is useful when you need to expire an asset’s cache in response to a change in another file.

API

rails3 application add subdomain

在rails3项目中配置subdomain。假设我们有个网站www.zires.info。我想把www.zires.info/photos变成子域名的形式www.photos.zires.info。

第一步:配置路由

constraints(:subdomain=>"photos") do
  root :to => "photos#index"
end

至此,访问photos.zires.info就会到photos的index页面。但是www.photo.zires.info就不对了,所以还需要对www进行过滤。

第二步:过滤”www”

创建一个subdomain.rb文件在项目的initializers中

class Subdomain
  def self.matches?(request)
    request.subdomain.present? and request.subdomain == 'photos'
  end
end
constraints(Subdomain) do
  root :to => "photos#index"
end

这样路由就配置好了。

第三步:修改UrlHelper

我们还需要在url_for中增加一个subdomain参数,这样就可以指定url是在哪个域名下面了。

module UrlHelper

def url_for(options =nil)
  if options.kind_of?(Hash) and options.has_key?(:subdomain)
   options[:host] = with_subdomain(options.delete(:subdomain))
  end
  super
end

def with_subdomain(subdomain="")
  subdomain += "." unless subdomain.empty?
  [subdomain, request.domain].join
end

end

第四步:session共享

但是发现session不能在domain和subdomain之间共享,需要指定下

  Zires::Application.config.session_store :cookie_store, key: '_zires_session', domain: '.zires.info'

重启下就完成了。

参考链接:
http://asciicasts.com/episodes/221-subdomains-in-rails-3

rack rails3

简单提下rack

1)要求

一个rack应用程序要满足三个条件,a)响应call方法,b)call方法要有一个env环境参数,c)方法返回一个数组,数组包括三个部分,状态码,header和body

2)简单的rack

require 'rubygems'
require 'rack'

Rack::Handler::WEBrick.run lambda{ |env| [200,{},["hello rack!"]]}, :Port => 3000

3)使用中间件

Rack的中间件会一层层的封装rack应用程序,Rack中间件也是一个rack应用程序,新建一个最简单的中间件:

class SomeMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    # 对@app做处理最后再返回一个符合rack标准的数组
    [status, headers, body]
  end
end

# 使用
Rack::Handler::WEBrick.run SomeMiddleware.new(app), :Port => 3000

4)使用Rack::Builder

Rack::Builder是rack的DSL,以更优雅的方式来打造一个rack应用程序。

app = Rack::Builder.new {
  use Rack::ContentLength
  use SomeMiddleware
  run SomeRackApp.new
}
# 或者直接用lambad
app = Rack::Builder.app do
  use Rack::ContentLength
  use SomeMiddleware
  lambda { |env| [200, {'Content-Type' => 'text/plain'}, 'OK'] }
end

# 使用
Rack::Handler::WEBrick.run app, :Port => 3000

5)使用rackup和config.ru

rackup让我们可以使用一个配置文件来运行Rack应用程序。

rackup -s thin -p 3000 [config.ru]

config.ru的内容就和Rack::Builder差不多了。

use Rack::ContentLength
use SomeMiddleware
lambda { |env| [200, {'Content-Type' => 'text/plain'}, 'OK'] }

Rackup的实现其实就是Rack::Server.start,start方法最重要的一句:

app, options = Rack::Builder.parse_file(self.options[:config], opt_parser)

还是Rack::Builder吧,一目了然。

Rails3 on Rack

如果看过rails的启动过程,我们知道,rails server 其实是调用了Rails::Server.new#start方法,也就是Rack::Server的start方法。也就是用Rack::Builder读取了config.ru文件,把整个Rails Application跑了起来。

MyOwnApp::Application.initialize!
run MyOwnApp::Application

这里有个继承关系要知道:

MyOwnApp::Application < Rails::Application < Rails::Engine < Rails::Railtie

整个rails3程序可以说是一个大的rack应用程序,里面每个中间件又都是一个rack应用程序,到每一个action又是一个rack应用程序,所以rails3和rack是密不可分的。

可以用rack middleware看看一共使用了多少middleware

use ActionDispatch::Static
use Rack::Lock
use ActiveSupport::Cache::Strategy::LocalCache
use Rack::Runtime
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::RemoteIp
use Rack::Sendfile
use ActionDispatch::Callbacks
use ActiveRecord::ConnectionAdapters::ConnectionManagement
use ActiveRecord::QueryCache
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ParamsParser
use Rack::MethodOverride
use ActionDispatch::Head
use ActionDispatch::BestStandardsSupport
use Warden::Manager
use Sass::Plugin::Rack
run MyOwnApp::Application.routes

这里有篇railscasts讲如何将Sinatra和Rails结合起来,理论上任何符合Rack规则的框架都可以和Rails结合。rack-in-rails-3

rails3 初始化和启动 Initialization Process

一直想搞清楚rails的启动和整个生命进程,好在有官方的guide用来参考,The Rails Initialization Process
1)先来看看rails的组织结构

%w(
  actionmailer
  actionpack
  activemodel
  activerecord
  activeresource
  activesupport
  railties
)

2)rails是如何启动的?
按下‘rails s’ 命令发生了什么?
rails3中rails已经变成了一个全局的命令,s是它的参数,s是server的缩写。
rails命令在gem包的bin/rails脚本中

#!/usr/bin/env ruby

begin
  require "rails/cli"
rescue LoadError
  railties_path = File.expand_path('../../railties/lib', __FILE__)
  $:.unshift(railties_path)
  require "rails/cli"
end

如果找不到rails/cli就把railties_path加到当前环境变量中。railties作用是将每个rails模块串联起来,它负责rails的启动顺序,管理rails的命令行接口,并且提供Rails的generators。

” rails/cli ” 是负责什么的?

require 'rbconfig'
require 'rails/script_rails_loader'

# If we are inside a Rails application this method performs an exec and thus
# the rest of this script is not run.
Rails::ScriptRailsLoader.exec_script_rails!

require 'rails/ruby_version_check'
Signal.trap("INT") { puts; exit(1) }

if ARGV.first == 'plugin'
  ARGV.shift
  require 'rails/commands/plugin_new'
else
  require 'rails/commands/application'
end

rbconfig是对ruby标准库的配置和补充,在ruby编译的时候起效,和rails关系不大,不谈。
走进script_rails_loader.rb文件,里面定义了两个常量

RUBY = File.join(*RbConfig::CONFIG.values_at("bindir", "ruby_install_name")) + RbConfig::CONFIG["EXEEXT"]
SCRIPT_RAILS = File.join('script', 'rails')

RUBY是ruby的bin文件位置,不管是Mac OS还是Windows还是其他,都会指向可执行文件。
SCRIPT_RAILS就是指向rails生成项目script目录下的rails脚本。大体内容如下:

#!/usr/bin/env ruby
# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.

APP_PATH = File.expand_path('../../config/application',  __FILE__)
require File.expand_path('../../config/boot',  __FILE__)
require 'rails/commands'

回到cli文件中,下一句可以看到执行了一个方法

Rails::ScriptRailsLoader.exec_script_rails!

exec_script_rails!方法的内容:

def self.exec_script_rails!
      cwd = Dir.pwd
      return unless in_rails_application? || in_rails_application_subdirectory?
      exec RUBY, SCRIPT_RAILS, *ARGV if in_rails_application?
      Dir.chdir("..") do
        # Recurse in a chdir block: if the search fails we want to be sure
        # the application is generated in the original working directory.
        exec_script_rails! unless cwd == Dir.pwd
end

exec RUBY, SCRIPT_RAILS, *ARGV 这边大致上可以看出端倪了,这行代码的实质是:

ruby script/rails [arguments]
# 这里的arguments 可以是server, generate, console等

OK,重点来看看script/rails

首先有一个APP_PATH的常量,可以看到指向的是config目录下的application.rb文件。接着require了boot文件和commands文件。这里可以看到boot文件是第一个被载入的。

boot文件的任务其实很简单,就是准备好Gemfile中的gems。

# rubygems第一个被加载
require 'rubygems'

# Set up gems listed in the Gemfile.
gemfile = File.expand_path('../../Gemfile', __FILE__)
begin
    ENV['BUNDLE_GEMFILE'] = gemfile
    require 'bundler'
    Bundler.setup
rescue Bundler::GemNotFound => e
    STDERR.puts e.message
    STDERR.puts "Try running `bundle install`."
    exit!
end if File.exist?(gemfile)

命令行的实质就全在rails/commands.rb中了,源码地址commands.rb

ARGV < < '--help' if ARGV.empty?

aliases = {
  "g" => "generate",
  "c" => "console",
  "s" => "server",
  "db" => "dbconsole"
}

command = ARGV.shift
command = aliases[command] || command

可以看到这里的ARGV就是上面提到的exec RUBY, SCRIPT_RAILS, *ARGV后面的参数,也就是我们传进去的”server,generate,console”等等。这里为空就是”–help”,并且还有4个aliases方便简写。

主要还是看server是如何启动的。

when 'server'
  # Change to the application's path if there is no config.ru file in current dir.
  # This allows us to run script/rails server from other directories, but still get
  # the main config.ru and properly set the tmp directory.
  Dir.chdir(File.expand_path('../../', APP_PATH)) unless File.exists?(File.expand_path("config.ru"))

  require 'rails/commands/server'
  Rails::Server.new.tap { |server|
    # We need to require application after the server sets environment,
    # otherwise the --environment option given to the server won't propagate.
    require APP_PATH
    Dir.chdir(Rails.application.root)
    server.start
  }

上面代码有三点,第一是实例化了Rails::server,第二是require APP_PATH(也就是application.rb),第三调用了server#start方法

实例化了一个Rails::Server,这是个什么东东?参考源码commands/server.rb

require 'fileutils'
require 'optparse'
require 'action_dispatch'

module Rails
  class Server < ::Rack::Server
    ......
  end
end

原来Rails::Server继承了Rack::Server(终于和Rack挂钩了),Rack::Server用来给所有基于rack的应用提供一般的server接口。而且,这里第一次引入了action_dispatch。Rails::Server的initialize方法如下:

def initialize(*)
      super
      set_environment
end

superRack::Server源码那么Rack::Server的initialize方法如下:

def initialize(options = nil)
      @options = options
      @app = options[:app] if options && options[:app]
end

由于options为nil所以Rack::Server中的initialize无作为,回到Rails::Server中,super的下一句是set_environment,代码如下:

def set_environment
      ENV["RAILS_ENV"] ||= options[:environment]
end

一目了然,设定环境咯,production,development,还是test。这里options是父类Rack::Server中的:

def options
      @options ||= parse_options(ARGV)
end

def parse_options(args)
    options = default_options

    # Don't evaluate CGI ISINDEX parameters.
    # http://hoohoo.ncsa.uiuc.edu/cgi/cl.html
    args.clear if ENV.include?("REQUEST_METHOD")

    options.merge! opt_parser.parse! args
    options[:config] = ::File.expand_path(options[:config])
    ENV["RACK_ENV"] = options[:environment]
    options
end

default_options在Rails::Server中被覆写了:

def default_options
      super.merge({
        :Port => 3000,
        :environment => (ENV['RAILS_ENV'] || "development").dup,
        :daemonize => false,
        :debugger => false,
        :pid => File.expand_path("tmp/pids/server.pid"),
        :config => File.expand_path("config.ru") # config.ru被指定,这也是commands.rb中server要chdir项目根目录的原因。
      })
end

第二是require APP_PATH(也就是application.rb),其实就是将项目的application给配置好,里面有这么一句:

require 'rails/all'

其实就是将rails所有的模块都引入进来,all.rb如下:

require "rails"
 
  %w(
    active_record
    action_controller
    action_mailer
    active_resource
    rails/test_unit
  ).each do |framework|
    begin
      require "#{framework}/railtie"
    rescue LoadError
    end
  end

第三是调用了server#start方法,在Rails::Server中:

def start
      puts "=> Booting #{ActiveSupport::Inflector.demodulize(server)}"
      puts "=> Rails #{Rails.version} application starting in #{Rails.env} on http://#{options[:Host]}:#{options[:Port]}"
      puts "=> Call with -d to detach" unless options[:daemonize]
      trap(:INT) { exit }
      puts "=> Ctrl-C to shutdown server" unless options[:daemonize]

      #Create required tmp directories if not found
      %w(cache pids sessions sockets).each do |dir_to_make|
        FileUtils.mkdir_p(Rails.root.join('tmp', dir_to_make))
      end

      super
    ensure
      # The '-h' option calls exit before @options is set.
      # If we call 'options' with it unset, we get double help banners.
      puts 'Exiting' unless @options && options[:daemonize]
    end

多熟悉啊,先一步步的屏显。然后是trap(:INT) { exit },是响应Ctrl-C中断的。再创建了tmp文件夹和四个子文件夹,最后回到Rack::Server的start方法中。

Rack::Server的start方法有这么一句:

wrapped_app

实际调用的是:

def wrapped_app
    @wrapped_app ||= build_app app
end

这里有两个方法的调用,一个是#build_app,另一个是#app,先来看#app方法:

def app
      @app ||= begin
        if !::File.exist? options[:config]
          abort "configuration #{options[:config]} not found"
        end

        app, options = Rack::Builder.parse_file(self.options[:config], opt_parser)
        self.options.merge! options
        app
      end
end

了然了,原来通过Rack::Builder和config.ru构建了一个Rack application啊.Rack::Builder源码

再来看#build_app方法:

def build_app(app)
  middleware[options[:environment]].reverse_each do |middleware|
    middleware = middleware.call(self) if middleware.respond_to?(:call)
    next unless middleware
    klass = middleware.shift
    app = klass.new(app, *middleware)
  end
  app
end

通过middleware来新建一个app,像剥洋葱一样,一个一个middleware的往外套。

ok,由上面我们知道,通过config.ru,我们创建了一个Rack Application,下面来看看config.ru

# This file is used by Rack-based servers to start the application.

require ::File.expand_path('../config/environment',  __FILE__)
run example::Application

可以看到引入了environment.rb文件。如下:

# Load the rails application
require File.expand_path('../application', __FILE__)

# Initialize the rails application
example::Application.initialize!

初始化了example::Application,example::Application是继承的Rails::Application,那么Rails::Application是什么呢?

#Rails::Application is responsible for executing all railties, engines and plugin
# initializers. Besides, it also executed some bootstrap initializers (check
# Rails::Application::Bootstrap) and finishing initializers, after all the others
# are executed (check Rails::Application::Finisher).

通过代码,Rails::Application继承了Engine。

看看initialize!方法:

def initialize!
  raise "Application has been already initialized." if @initialized
  run_initializers(self)
  @initialized = true
  self
end

至此,Rails Application的初始化和Rails Server基本上都已经启动完成。

3)rails是如何响应一个请求的?
占座,等下一篇文章完成后链接到这里。

rails3 respond_with

rails3有个新的controller跳转方法叫respond_with,rails总是不忘自己的哲学-“不要重复自己”,在rails2.x的版本中,下面的写法比较常见:

class PostsController < ApplicationController::Base

  def index
    @posts = Post.all
    respond_to do |format|
      format.html
      format.xml { render :xml => @posts.to_xml }
      format.json { render :json => @posts.to_json }
    end
  end

  def create
    @post = Post.create(params[:post])
    respond_to do |format|
      format.html { redirect_to posts_url }
      format.xml { render :xml => @post.to_xml }
      format.json { render :json => @post.to_json }
    end
  end
end

又臭又长,有了respond_with,就变成如下了:

class PostsController < ApplicationController::Base

  respond_to :html, :xml, :json

  def index
    respond_with(@posts = Post.all)
  end

  def create
    @post = Post.create(params[:post])
    respond_with(@post)
  end
end

多清爽啊,respond_with会根据请求的类型,自动的返回html,xml,或者json。当然了,respond_with也可以override来调整到需要的。
1)status,head

  respond_with(@posts = Post.all, :status => 200, :head => :ok)

2)override

respond_with(@posts) do |format|
    format.html { redirect_to posts_url }
end

3)any,匹配任意的格式

respond_with(@posts) do |format|
    format.any(:xml, :json) { redirect_to posts_url }
end

4):location,类似上面的2

respond_with(@posts, :location => posts_url)