最近在 hoptoad 上看到一些詭異的 log,似乎是網站升到 Rails 3 後才出現的:
ActionView::MissingTemplate: Missing template posts/show with {:formats=>["application/youtube-client", "*/*"], :handlers=>[:rjs, :rxml, :rhtml, :builder, :erb], :locale=>[:"zh-TW", :"zh-TW"]} in view paths
沒錯,怎麼會有 format 是 “application/youtube-client”, “/“,這到底是什麼鬼玩意送出來的?(狀態顯示為才疏學淺)詳細看了一下 HTTP header 資訊發現
HTTP_ACCEPT "*/*, application/youtube-client" HTTP_USER_AGENT "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; HD2_T8585; Windows Phone 6.5)" HTTP_UA_OS "Windows CE (Pocket PC) - Version 5.2"
原來是 Windows Phone 這樣的 mobile 裝置送的。
但由於數量不少,於是再繼續看看是不是有其他奇怪的 log 有類似情況的:
ActionView::MissingTemplate: Missing template posts/show with {:handlers=>[:rjs, :builder, :rhtml, :rxml, :erb], :locale=>[:"zh-TW", :"zh-TW"], :formats=>["*/*;q=0.01"]} in view paths
HTTP_ACCEPT "*/*;q=0.01"
HTTP_USER_AGENT "Mozilla/4.0 (PSP (PlayStation Portable); 2.00)"
ActionView::MissingTemplate: Missing template photos/show with {:formats=>["application/vnd.wap.wmlc", "application/vnd.wap.wmlscriptc", "text/vnd.wap.wml", "image/vnd.wap.wbmp", "*/*"], :locale=>[:"zh-TW", :"zh-TW"], :handlers=>[:rjs, :rxml, :rhtml, :builder, :erb]} in view paths
HTTP_ACCEPT "application/vnd.wap.wmlc, application/vnd.wap.wmlscriptc, text/vnd.wap.wml, image/vnd.wap.wbmp, */*"
HTTP_USER_AGENT "Mozilla/5.0 (Linux; U; Android 2.1-update1; zh-tw; MB525 Build/JRDNEM_U3_2.51.0) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17"
HTTP_VIA "(infoX WAP Gateway), HTTP/1.1, Huawei Technologies"
HTTP_X_UP_BEAR_TYPE "WCDMA"
ActionView::MissingTemplate: Missing template pages/welcome with {:formats=>["text/*"], :handlers=>[:rjs, :rxml, :rhtml, :builder, :erb], :locale=>[:"zh-TW", :"zh-TW"]} in view paths
HTTP_ACCEPT "text/*"
HTTP_USER_AGENT "Microsoft Office Mobile /14.0"
ActionView::MissingTemplate: Missing template posts/show with {:formats=>["image/gif", "image/x-xbitmap", "image/jpeg", "image/pjpeg"], :locale=>[:"zh-TW", :"zh-TW"], :handlers=>[:rjs, :rxml, :rhtml, :builder, :erb]} in view paths
HTTP_ACCEPT "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg"
HTTP_USER_AGENT "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)"
ActionView::MissingTemplate: Missing template digiphoto_posts/show with {:formats=>[:html, :text, :js, :css, :ics, :csv, :xml, :rss, :atom, :yaml, :multipart_form, :url_encoded_form, :json], :locale=>[:"zh-TW", :"zh-TW"], :handlers=>[:rjs, :rxml, :rhtml, :builder, :erb]} in view paths
HTTP_ACCEPT "*/*"
ActionView::MissingTemplate: Missing template digiphoto_posts/show with {:formats=>[:html], :locale=>[:"zh-TW", :"zh-TW"], :handlers=>[:rjs, :rxml, :rhtml, :builder, :erb]} in view paths
HTTP_ACCEPT "application/x-ms-application, image/jpeg, application/xaml+xml, image/gif, image/pjpeg, application/x-ms-xbap, application/msword, application/vnd.ms-excel, application/x-shockwave-flash, */*"
HTTP_ACCEPT "application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5"
HTTP_ACCEPT "image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*"
HTTP_ACCEPT "image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/x-ms-application, application/x-ms-xbap, application/vnd.ms-xpsdocument, application/xaml+xml, application/vnd.ms-powerpoint, application/msword, application/x-shockwave-flash, application/vnd.ms-excel, */*"
看到這樣的現象,直覺反應是,事情絕對不是想像中的那麼簡單,必定有惡魔藏在細節裡。
仔細地整理一下,上述的錯誤,所要求的頁面 format 應該都要判斷為 :html 才是正確的,而其中共通的特性是 HTTP_ACCEPT 都不外乎是下列幾種:
"*/*", "*/*;q=0.01", "text/*", "application/youtube-client", "application/vnd.wap.wmlc"
並且似乎也跟 mobile 裝置有關,這就讓我想到有人抱怨 HTC 手機不能順利瀏覽 T 客邦網站這件事。於是乎抓了幾天的 log 來分析,居然有驚奇的發現
https://gist.github.com/9a396c56a73b57d5ad72
行動裝置的 HTTP_ACCEPT 長得都不太一樣,同樣是 Android 系統,卻沒幾個能正確送出 text/html,幾乎都是送 application/xml,太令人訝異了。
那 Rails 3 到底是怎麼利用 HTTP_ACCEPT 資訊,又怎麼判斷 format 的呢?
直接查原始碼可以發現 ActionDispatch::Http::MimeNegotiation 有個 formats 方法,當找不到 paramater[:format] 這個傳遞資訊時,會由 HTTP_ACCEPT 猜測一個適當的 format,最後才預設為 [Mime::HTML]。(Rails 3.0.7)
而這就是問題所在,猜測的邏輯沒有包含到這些狀況,上述那些怪異的 HTTP_ACCEPT,在 Rails 3.0.7 以前(含)並不能正確的處理,於是乎輕則看到這些錯誤 log,重則直接吐 502 給你。那可不妙,我可是要支援 mobile 裝置啊~
幸好,活躍的 Rails 圈有人提出了問題並獲得解答:
但解法目前只有 apply 在 rails master branch,我可是要解燃眉之急呀,於是自己搞了個 patch 來修補 Rails 3.0.7:
https://gist.github.com/33c10952fe3cadaec3c4
直接抓回來放在 config/initializers 後,重新啟動 application 就行了。
這個 patch 同時解決了兩件事:
如此一來就能好好面對行動裝置的挑戰了。
如果在 Ruby 1.9.2 開發 Rails 程式,最頭痛的問題應該就是編碼(Encoding)錯誤的問題。除了要考慮資料庫本身的編碼問題,所有的字串 IO 都會經過編碼處理,Ruby 1.9.2 提供了廣域設定與實體方法設定的方式,但該在什麼時機設定呢?
基本上 Yahuda Kats 已經寫了篇蠻詳盡的說明(Ruby 1.9 Encodings: A Primer and Solution for Rails),我這裡所要提的重點是,如果你的資料庫(如 MySQL)設定是用 UTF-8 做為儲存編碼,而 Web Server 所傳送的文字編碼是 ASCII-8BIT 的話(可以從 server log 得知),可能會遇到
“\xE4” from ASCII-8BIT to UTF-8
這樣的錯誤訊息,基本上可以用 Ruby 1.9.2 的廣域編碼設定來解決問題,只要在 $RAILS_ROOT/config/environment.rb 的檔案開頭加上這幾行設定即可:
if defined? Encoding
Encoding.default_internal = 'UTF-8'
Encoding.default_external = 'ASCII-8BIT'
end
這樣就可以正常地從 Web Form 裡面輸入中文,並以 UTF-8 字串儲存到資料庫中。如果 Web Server 並不是傳送 ASCII-8BIT 的話,那就要將 default_external 改成適當的編碼。
PS. 我是在 Ruby-1.9.2-head + Rails-3.0.0.beta4 + mysql-2.8.1 gem 的環境下測試,但 Ruby-1.9.2-preview3 之後應該都是同樣的解法。
PS2. Yahuda Kats 的這篇 Encodings, Unabridged 將目前 Ruby 1.9 處理 Encoding 的來由述說的很清楚,值得仔細研究。
PS3. 感謝 ihower 提醒 rails ticket #4336 有另外用 before_filter 對 params 做 force_encoding 的解法,這樣就方便看 log 做 debug。
當開發 Rails 3 應用程式時,了解新的 call stack 設計對除錯和開發新功能的幫助會很大:

首先 Action::Dispatch 成為整個 call stack 的主角,Jeremy McAnally 歸納出幾個特點:
如果熟悉 Rails 3 的初始化程序,應該可以了解 rack server 在接收到一個 HTTP request 後,要怎麼誘發 Rails::Application 的 .call 實體方法:
Rails::Server.new.start
Rack::Handler::Mongrel.run(wrapped_app)
Mongrel::HttpServer.new.run
Mongrel::HttpServer.new.process_client
Rack::Handler::Mongrel.new.process
Rack::Chunked.new.call(env)
Rack::ContentLength.new.call(env)
Rails::Rack::LogTailer.new.call(env)
YourApp::Application.call(env)
Rails::Application.call(env)
Rails::Application.app.call(env)
Rails::Application.middleware.build(routes).call(env) # @app.call(env)
Rails 3 為了符合 Rack 的設計理念(能回應 call 方法的物件),於是將 routes 和 middlewares 設計成串接式(cascade)型態的物件,並在執行 initializers 時實體化成 rails app stack 物件。其中有三個 initializer 跟 Action::Dispatch 最相關:
## actionpack/lib/action_dispatch/railtie.rb
initializer "action_dispatch.prepare_dispatcher" do |app|
require 'rails/dispatcher'
ActionDispatch::Callbacks.to_prepare { app.routes_reloader.reload_if_changed }
end
## railties/lib/rails/engine.rb
initializer :add_routing_paths do |app|
paths.config.routes.to_a.each do |route|
app.routes_reloader.paths.unshift(route) if File.exists?(route)
end
end
## railties/lib/rails/application/finisher.rb
initializer :build_middleware_stack do
app
end
其中 action_dispatch.prepare_dispatcher 會設定一個 ActionDispatch 的 callback 內容。而 add_routing_paths 會用 Rails::Application.routes_reloader 方法實體化一個 Rails::Application::RoutesReloader 物件,並將所有 Rails::Engine 的 config/routes.rb 加入到 @path 陣列。當執行 build_middleware_stack 時,會實體化 ActionDispatch::Routing::RouteSet 物件,連同各式剛實體化好的 middleware 物件一起放入 ActionDispatch::MiddlewareStack 陣列物件中,最後轉換成串接式型態的物件:
## railties/lib/rails/application.rb
class Rails::Application
def app
@app ||= middleware.build(routes)
end
def routes
@routes ||= ActionDispatch::Routing::RouteSet.new
end
end
## railties/lib/rails/engine.rb
class Rails::Engine
delegate :middleware, :paths, :root, :to => :config
end
## railties/lib/rails/application/configuration.rb
class Rails::Application::Configuration
def middleware
@middleware ||= default_middleware_stack
end
def default_middleware_stack
ActionDispatch::MiddlewareStack.new.tap do |middleware|
......
middleware.use('::ActionDispatch::Callbacks', lambda { !cache_classes })
......
end
end
end
## actionpack/lib/action_dispatch/middleware/stack.rb
class ActionDispatch::MiddlewareStack
def build(app = nil, &blk)
app ||= blk
raise "MiddlewareStack#build requires an app" unless app
active.reverse.inject(app) { |a, e| e.build(a) }
end
def use(*args, &block)
middleware = Middleware.new(*args, &block)
push(middleware)
end
end
仔細地看 Rails::Application.app 物件真正的樣貌,就是把一個擁有 @app 類別變數的 middleware 物件當成另一個擁有 @app 類別變數的 middleware 物件的初始化參數,一個個串接下去形成一個大型的 rack app stack 物件。這 rack app stack 物件也會被當成參數成為 rack server stack 的串接式物件,以便呼應 rack server 傳來的 call 方法:
#> s1 = ::ActiveRecord::QueryCache.new(@routes)
#> s9 = ::ActionDispatch::Callbacks.new(s8, !cache_classes)
#> ……
#> @app = ::ActionDispatch::Static.new(s13, Rails.public_path)
#> Rails::Application.app.call(env) = @app.call(env)
@app = #<ActionDispatch::Static:0x2ff9fa4
@app=#<Rack::Lock:0x2ffa1ac
@app=#<Rack::Runtime:0x2ffa2b0
@app=#<Rails::Rack::Logger:0x2ffaa1c
@app=#<ActionDispatch::ShowExceptions:0x2ffaabc
@app=#<ActionDispatch::Callbacks:0x2ffac60
@app=#<ActionDispatch::Cookies:0x2ffae40
@app=#<ActiveRecord::SessionStore:0x2ffaf1c
@app=#<ActionDispatch::Flash:0x2ffb0ac
@app=#<ActionDispatch::ParamsParser:0x2ffb50c
@app=#<Rack::MethodOverride:0x2ffb64c
@app=#<ActionDispatch::Head:0x2ffb944
@app=#<ActiveRecord::ConnectionAdapters::ConnectionManagement:0x300e2d8
@app=#<ActiveRecord::QueryCache:0x300e33c
@app=#<ActionDispatch::Routing::RouteSet:0x151c588»»»»»»»>
這裡要特別注意 ActionDispatch::Callbacks 這個 middleware 物件,當它初始化時會執行名為 :prepare 的 callback(先前的 initializer 已設定好內容)。這個 callback 執行時會用 Rails::Application.routes_reloader 方法來實體化一個 Rails::Application::RoutesReloader 物件,並動用 reload! 方法。這時會得到 ActionDispatch::Routing::RouteSet 實體物件,接著利用 clear! 方法取得一個新的 Rack::Mount::RouteSet 實體物件 @set,依序載入所有 Rails::Engine 的 config/routes.rb,最後執行 finalizer! 方法,將 @set 凍結住避免變動:
## railties/lib/rails/application.rb
class Rails::Application
def routes_reloader
@routes_reloader ||= RoutesReloader.new
end
end
## actionpack/lib/action_dispatch/middleware/callback.rb
class ActionDispatch::Callbacks
define_callbacks :prepare, :scope => :name
def self.to_prepare(*args, &block)
if args.first.is_a?(Symbol) && block_given?
define_method :"__#{args.first}", &block
set_callback(:prepare, :"__#{args.first}")
else
set_callback(:prepare, *args, &block)
end
end
def initialize(app, prepare_each_request = false)
@app, @prepare_each_request = app, prepare_each_request
run_callbacks(:prepare)
end
end
## railties/lib/rails/application/routes_reloader.rb
class Rails::Application::RoutesReloader
def reload!
routes = Rails::Application.routes
routes.disable_clear_and_finalize = true
routes.clear!
paths.each { |path| load(path) }
ActiveSupport.on_load(:action_controller) { routes.finalize! }
nil
ensure
routes.disable_clear_and_finalize = false
end
end
## actionpack/lib/action_dispatch/routing/route_set.rb
class ActionDispatch::Routing::RouteSet
def clear!
# Clear the controller cache so we may discover new ones
@controller_constraints = nil
@finalized = false
routes.clear
named_routes.clear
@set = ::Rack::Mount::RouteSet.new(:parameters_key => PARAMETERS_KEY)
end
def finalize!
return if @finalized
@finalized = true
@set.add_route(NotFound)
@set.freeze
end
end
當 routes_reloader 載入 config/routes.rb 時,會誘發 Rails::Application.routes 執行 draw 實體方法。這個 draw 方法會把 routes 自己(ActionDispatch::Routing::RouteSet 實體物件)當成參數實體化一個 ActionDispatch::Routing::Mapper 物件 mapper,並成為 mapper 的 @set 變數,而 mapper 會負責解譯 block 中定義的 Rails routes mapping DSL 內容。
例如當定義一個 match(‘/’, :to => “your_home#index”) 時,首先會把傳入 match 方法的參數轉換成 ActionDispatch::Routing::Mapper::Mapping 實體物件,再轉換成參數陣列 mapping。接著 mapper 的 @set 變數(routes)會以 add_route 方法把 mapping 參數實體化成 ActionDispatch::Routing::Route 物件 route。由於 routes 本身也有一個 @set 變數是個 Rack::Mount::RouteSet 實體物件,同樣會以 add_route 方法將 route 加入這個 @set 變數,以便串接 rack mount 的 router 辨識功能:
## actionpack/lib/action_dispatch/routing/route_set.rb
class ActionDispatch::Routing::RouteSet
def draw(&block)
clear! unless @disable_clear_and_finalize
mapper = Mapper.new(self)
if block.arity == 1
mapper.instance_exec(DeprecatedMapper.new(self), &block)
else
mapper.instance_exec(&block)
end
finalize! unless @disable_clear_and_finalize
nil
end
# @set = ::Rack::Mount::RouteSet.new
def add_route(app, conditions = {}, requirements = {}, defaults = {}, name = nil, anchor = true)
route = Route.new(app, conditions, requirements, defaults, name, anchor)
@set.add_route(*route)
named_routes[name] = route if name
routes << route
route
end
end
class ActionDispatch::Routing::Dispatcher
def initialize(options={})
@defaults = options[:defaults]
@glob_param = options.delete(:glob)
end
end
## actionpack/lib/action_dispatch/routing/mapper.rb
class ActionDispatch::Routing::Mapper
def initialize(set)
@set = set
end
# @set = ::ActionDispatch::Routing::RouteSet.new
def match(*args)
mapping = Mapping.new(@set, @scope, args).to_route
@set.add_route(*mapping)
self
end
end
class ActionDispatch::Routing::Mapper::Mapping
def to_route
[ app, conditions, requirements, defaults, @options[:as], @options[:anchor] ]
end
def app
Constraints.new(
to.respond_to?(:call) ? to : Routing::RouteSet::Dispatcher.new(:defaults => defaults),
blocks
)
end
end
產生 mapping 陣列時會實體化一個 Routing::RouteSet::Dispatcher 物件 app,負責拆解 params,執行特定 controller 的 action 內容,屬於仲介 route 到 controller 的主要 app stack 物件。這時 initializer 才算完成整個 rails app stack 的準備工作。
當一個 HTTP request 從 rack server stack 傳遞到 rails app stack 後,會先經過 middlewares 一層層地處理,直到 ActionDispatch::Routing::RouteSet 實體物件傳遞給 Rack::Mount::RouteSet 實體物件,才會辨識 http request 該由哪個 ActionDispatch::Routing::Route 實體物件來執行,並經由 ActionDispatch::Routing::RouteSet::Dispatcher 實體物件誘發特定 controller 的 action 內容:
Rails::Application.middleware.build(routes).call(env) # @app.call(env)
ActionDispatch::Static.new.call(env) # @app.call(env)
…… # cascade @app.call(env) of middlewares
ActionDispatch::Routing::RouteSet.new.call(env) # @app.call(env)
Rack::Mount::RouteSet.new.call(env) # @set.call(env)
ActionDispatch::Routing::Route.new.call(env) # route.app.call(env)
ActionDispatch::Routing::RouteSet::Dispatcher.new.call(env) # @app.call
YourHomeController.action(params[:action]).call(env)
YourHomeController.middleware_stack.build(default_stack_proc).call(env)
#> default_stack_proc = Proc.new { new.dispatch(name, klass.new(env)) }
YourHomeController.new.dispatch(params[:action], request)
#> request = ActionDispatch::Request.new(env)
YourHomeController.new.send(params[:action])
YourHomeController.new.to_a
從程式碼中可以發覺 ActionDispatch::Routing::RouteSet::Dispatcher 實體物件利用 controller.action(params[:action]) 方法來取得任一個 controller 的 action dispatch stack 串接式物件(ActionDispatch::MiddlewareStack 物件),這個 stack 預設是一個 Proc 物件,能夠執行 controller 的某個 action:
## railties/lib/rails/application.rb
class Rails::Application
def call(env)
env["action_dispatch.parameter_filter"] = config.filter_parameters
app.call(env)
end
end
## actionpack/lib/action_dispatch/routing/route_set.rb
class ActionDispatch::Routing::RouteSet
def call(env)
finalize!
@set.call(env)
end
end
## rack-mount/lib/rack/mount/recognition/route_set.rb
class Rack::Mount::RouteSet
def call(env)
raise 'route set not finalized' unless @recognition_graph
env[PATH_INFO] = Utils.normalize_path(env[PATH_INFO])
request = nil
req = @request_class.new(env)
recognize(req) do |route, matches, params|
# TODO: We only want to unescape params from uri related methods
params.each { |k, v| params[k] = Utils.unescape_uri(v) if v.is_a?(String) }
if route.prefix?
env[Prefix::KEY] = matches[:path_info].to_s
end
env[@parameters_key] = params
result = route.app.call(env)
return result unless result[1][X_CASCADE] == PASS
end
request || [404, {'Content-Type' => 'text/html', 'X-Cascade' => 'pass'}, ['Not Found']]
end
end
## actionpack/lib/action_dispatch/routing/route_set.rb
class ActionDispatch::Routing::RouteSet::Dispatcher
def call(env)
params = env[PARAMETERS_KEY]
prepare_params!(params)
# Just raise undefined constant errors if a controller was specified as default.
unless controller = controller(params, @defaults.key?(:controller))
return [404, {'X-Cascade' => 'pass'}, []]
end
controller.action(params[:action]).call(env)
end
end
## actionpack/action_controller/metal.rb
class ActionController::Metal
class_attribute :middleware_stack
self.middleware_stack = ActionDispatch::MiddlewareStack.new
def self.inherited(base)
self.middleware_stack = base.middleware_stack.dup
super
end
def self.action(name, klass = ActionDispatch::Request)
middleware_stack.build do |env|
new.dispatch(name, klass.new(env))
end
end
def dispatch(name, request)
@_request = request
@_env = request.env
@_env['action_controller.instance'] = self
process(name)
to_a
end
def to_a
response ? response.to_a : [status, headers, response_body]
end
end
## actionpack/abstract_controller/base.rb
class AbstractController::Base
def process(action, *args)
@_action_name = action_name = action.to_s
unless action_name = method_for_action(action_name)
raise ActionNotFound, "The action '#{action}' could not be found"
end
@_response_body = nil
process_action(action_name, *args)
end
def process_action(method_name, *args)
send_action(method_name, *args)
end
alias send_action send
end
由於 controller 的 action 不會再傳遞 HTTP request 到其他物件,而是直接回傳標準的 rack response 陣列 [status, headers, response_body],成了所謂的 rack endpoint。最後再由 rack server 把 HTTP response 送回到使用者個瀏覽器中解讀。
以上就是 Action::Dispatch 整個 call stack 大略的樣貌。如果想要在這中間做些處理,則是安插個 middleware 進去,如 José Valim 就寫了個 devise 提供 rack 風格的 authentication 功能。
徹底了解 Rails 3 的 boot/initialization 流程對於控制 Rails 應用程式是很重要的一個課題,當我們下了 rails server 命令,究竟發生了什麼事,Bundler 和 Rack 又扮演了什麼角色,讓我們來瞧個仔細。
說明:環境變數 GEM_PATH 是 rubygems 的安裝路徑,ROOT_PATH 是應用程式根目錄,而本篇文章使用 RAILTIES_PATH 代表 railties 這個 gem 的根目錄,依此類推。引述段落則是依序指出重點檔案或指令。
在 Rails 3 應用程式根目錄下,執行 rails server 指令,第一步便是透過 boot.rb 誘發 bundler 將所有相關 rubygems 的 lib 都加到 $LOAD_PATH:
GEM_PATH/bin/rails server
RAILTIES_PATH/bin/rails
ROOT_PATH/script/rails
#> also define ENV_PATH, BOOT_PATH, APP_PATH
ROOT_PATH/config/boot.rb
#> add required gems’s lib into $LOAD_PATH
bundler 預設用 Definition.from_gemfile 解讀 ROOT_PATH/Gemfile,再以 Bundler.setup 方法呼叫 Bundle::Runtime 設定 rubygems;或是先執行 bundle lock 產生 ROOT_PATH/.bundle/environment 檔案(由 Definition.from_lock 解讀 ROOT_PATH/Gemfile.lock,再由 Bundle::Runtime 生成),當被 require 時會執行新的 Bundler.setup 方法設定 rubygems。
而 ROOT_PATH/script/rails 最後所 require 的 commands.rb 便會依序載入 rails.rb,設定基礎 Rails 環境,加載 rails/application.rb 及各式 Railtie,掛載透過 bundler 設定的 rubygems,啟動 Rails::Server 通知 Rack::Server 跑 Rails::Application:
RAILTIES_PATH/lib/rails/commands.rb
RAILTIES_PATH/lib/rails/commands/server.rb
rails_server = Rails::Server.new #=> also set ENV[“RAILS_ENV”]
ROOT_PATH/config/application.rb
RAILTIES_PATH/lib/rails/all.rb
RAILTIES_PATH/lib/rails.rb
ACTIVESUPPORT_PATH/lib/active_support.rb
……
RAILTIES_PATH/lib/rails/application.rb
……
Bundler.require :default, Rails.env
……
rails_server.start
這時 Rack::Server.start 會呼叫 .app 實體方法,先讓 Rack::Builder 以 #parse_file 類別方法解讀 config.ru,產生 Rakc::Builder 實體物件。此 Rack::Builder 物件會載入 environment.rb 來初始化 YourApp::Application,用 .run 實體方法把 YourApp::Application 這個龐大的 Rack application(可回應 call 方法傳入 env 參數並回傳 [status, header, body] 的 Ruby 實體物件/Proc 物件)也加入 Rack::Builder@ins 陣列,並回傳成 Rack::Server@app 陣列。最後 Rack::Server 透過 .wrapped_app 實體方法把 Rails::Server 的 middlewares 包裹成 @app 陣列,並實體化為 rack application,一起丟給 Rack::Handler 處理:
rails_server.start
……
wrapped_app = rails_server.build_app(@app)
#> @app = Rack::Builder.parse_file(‘config.ru’) = YourApp::Application
##> ROOT_PATH/config.ru
##> ROOT_PATH/config/environment.rb
##> YourApp::Application.initialize!
##> run YourApp::Application
#> rails_server.middleware[‘development’]
rack_app = wrapped_app = #<Rails::Rack::LogTailer:0x2f92228 @app=YourApp::Application, @cursor=612423, @file=#<File:log/development.log>, @last_checked=1270047942.86726>
……
rails_server.server.run(wrapped_app)
#> Rack::Server.new.server.run(wrapped_app)
#> Rack::Handler.default(options).run(wrapped_app)
rails_server = #<Rails::Server:0x2dafa14 @app=YourApp::Application, @_server=Rack::Handler::Mongrel, @options={:server=>nil, :AccessLog=>[], :pid=>”tmp/pids/server.pid”, :Host=>”0.0.0.0”, :debugger=>false, :daemonize=>false, :environment=>”development”, :config=>”config.ru”, :Port=>3000}>
……
Rack::Handler::Mongrel.run(rack_app)
#> http_server = Mongrel::HttpServer.new
#> handler = Rack::Handler::Mongrel.new(rack_app)
#> handler = Rack::Chunked.new(Rack::ContentLength.new(rack_app))
#> http_server.register(‘/’, handler)
……
Rack::Handler::Mongrel.process
如果跑 mongrel server 則 Rack::Handler.default 會以 Rack::Handler::Mongrel.run 方法跑一個 Mongrel::HttpServer 實體物件,並把每個 rack application(獨立的 Rack::Handler::Mongrel 實體物件)都註冊起來。
當 mongrel server 收到 http request 後,會送給 Rack::Handler::Mongrel 的 .process 實體方法,呼叫 rack application 的 .call 實體方法送出 http response。因為 Rails 3 每個 controller action、metal 都被設計成 rack application,所以創造出無限可能。
另外要提的是 Phusion Passenger 2.2.9 之後也採用類似的方法跑 Rails 3 application(主要是 require 的順序不一樣):
PhusionPassenger::Railz::ApplicationSpawner#preload_application
ROOT_PATH/config/environment.rb
ROOT_PATH/config/application.rb
ROOT_PATH/config/boot.rb
….
YourApp::Application.initialize!
run YourApp::Application
以上是 rails boot process 的概要,接著來看 rails initialization process 的部份。當 Rack Server 在解讀 config.ru 時,會產生一個這樣的 rack application 物件:
rack_app = Rack::Builder.new {
require ::File.expand_path('../config/environment', __FILE__)
run YourApp::Application
}.to_app
而 Rails::Application 初始化等於是展開 config.ru 這個 block 內容,最終會產生一個描述完整的 Rack::Builder 實體物件:
rack_app = Rack::Builder.new {
use ActionDispatch::Static
use Rack::Lock
use Rack::Runtime
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::RemoteIp
use Rack::Sendfile
use ActionDispatch::Callbacks
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ParamsParser
use Rack::MethodOverride
use ActionDispatch::Head
use ActiveRecord::ConnectionAdapters::ConnectionManagement
use ActiveRecord::QueryCache
run YourApp::Application.routes
}.to_app
一切都從類別宣告開始。從 ROOT_PATH/config/environment.rb 把 ROOT_PATH/config/application.rb 給 require 後的宣告開始:
YourApp::Application < Rails::Application < Rails::Engine < Rails::Railtie
這 YourApp::Application 的繼承宣告主要做了幾件事,定義 called_from 變數,引入 Rails::Application::Configurable 模組,定義 #config 類別方法用來建立 Rails::Application::Configuration 實體物件,排 subclasses 順序,把自己定義成 Singleton,最後把自己實體化:
## snippet of Rails::Application
def inherited(base)
raise "You cannot have more than one Rails::Application" if Rails.application
super
Rails.application = base.instance
end
## snippet of Rails::Engine
def inherited(base)
unless base.abstract_railtie?
base.called_from = begin
# Remove the line number from backtraces making sure we don't leave anything behind
call_stack = caller.map { |p| p.split(':')[0..-2].join(':') }
File.dirname(call_stack.detect { |p| p !~ %r[railties[\w\-\.]*/lib/rails|rack[\w\-\.]*/lib/rack] })
end
end
super
end
## snippet of Rails::Railtie
def inherited(base)
unless base.abstract_railtie?
base.send(:include, self::Configurable)
subclasses << base
end
end
每種 Railtie(Application, Engine, Plugin 都是一種 Railtie 型態)的 #config 類別方法都是在管理 Rails::Railtie::Configuration(子)物件。如呼叫 Rails::Application.config 時,會以 called_from 路徑中第一個有 config.ru 的路徑做為 root 參數建立 Rails::Application::Configuration 實體物件。
## snippet of Rails::Application::Configurable
def config
@config ||= Application::Configuration.new(self.class.find_root_with_flag("config.ru", Dir.pwd))
end
## snippet of Rails::Application::Configuration
def initialize(*)
super
@allow_concurrency = false
@filter_parameters = []
@dependency_loading = true
@serve_static_assets = true
@time_zone = "UTC"
@consider_all_requests_local = true
@session_store = :cookie_store
@session_options = {}
end
## snippet of Rails::Engine::Configuration
def initialize(root=nil)
super
@root = root
end
## snippet of Rails::Railtie::Configuration
def initialize
@@options ||= {}
end
基本上各 Railtie 利用 #initializer 類別方法定義了許多初始化時要執行的 initializer block,當 YourApp::Application.initialize! 執行時,會透過 #method_missing 類別方法得到 YourApp::Application 實體物件,透過 railties.all 實體方法取得所有 Railtie 實體物件,接著取得各 initializers 實體物件,再依序執行這些 initializer block,完成整個應用程式初始化動作:
## snippet of Rails::Application
def self.method_missing(*args, &block)
instance.send(*args, &block)
end
def initialize!
run_initializers(self)
self
end
def initializers
initializers = Bootstrap.initializers_for(self)
railties.all { |r| initializers += r.initializers }
initializers += super
initializers += Finisher.initializers_for(self)
initializers
end
## Rails::Initializable
def run_initializers(*args)
return if instance_variable_defined?(:@ran)
initializers.each do |initializer|
initializer.run(*args)
end
@ran = true
end
def self.initializer(name, opts = {}, &blk)
raise ArgumentError, "A block must be passed when defining an initializer" unless blk
opts[:after] ||= initializers.last.name unless initializers.empty? || initializers.find { |i| i.name == opts[:before] }
initializers << Initializer.new(name, nil, opts, &blk)
end
而 initializers 實體物件群組執行順序是 Rails::Application::Bootstrap -> Rails::Railtie.subclasses -> Rails::Engine.subclasses -> Rails::Plugin.all -> Rails::Engine -> Rails::Application::Finisher。雖然 initializer 執行順序預設是依照宣告順序,也可用 :before, :after 參數排序,但 Railtie、Engine、Plugin 各群組內依名稱字母排序的特性,實際撰寫 Engine 或 Plugin 時還是要多留心一下:
Rails::Application::Bootstrap
ActiveSupport::Railtie
I18n::Railtie
ActionDispatch::Railtie
ActionView::Railtie
ActionController::Railtie
ActiveRecord::Railtie
ActionMailer::Railtie
ActiveResource::Railtie
Rails::TestUnitRailtie
AllKindsOfEnginesOrderedByAlphabet (Rails::Engine)
AllKindsOfPluginsOrderedByAlphabet (Rails::Plugin)
YourApp::Application (Rails::Engine)
Rails::Application::Finisher
其中 Rails::Engine 有個名為 :add_routing_paths 的 initializer 會處理各 engine 的 routes.rb 載入;另 Rails::Application::Finisher 中有個名為 :build_middleware_stack 的 initializer 會呼叫 Rails::Application 的 .app 實體方法將所有 default_middleware、Railtie 的 middlewares 或自定的 middlewares 都丟到一個 ActionDispatch::MiddlewareStack 陣列中,並建立這些 middlewares 實體物件,這都是值得留意的基本 initializer。當所有 initializers 都執行完畢,一個 Rails 3 的 rack application(@app)便建立完成,達到初始化的最終目的:
## Rails::Application def app @app ||= middleware.build(routes) end def call(env) env["action_dispatch.parameter_filter"] = config.filter_parameters app.call(env) end
接著就是搬椅子等待 Rack server 呼叫 Rails::Application 的 .call 實體方法囉。
更新:內文已更新為 rails-3.0.0.beta2 的版本為主。
當我們在撰寫 Rails 3 的 Plugin/Engine 時,會被要求養成好的習慣,如你要覆寫 ActiveRecord 等底層模組/類別時,請先 require 它和你的覆寫模組(注意順序),再把覆寫模組給 include 進去:
## in lib/my_gem.rb
require "active_record"
require "my_gem/active_record_sti_fix"
ActiveRecord::Base.class_eval { include MyGem::ActiveRecordStiFix }
對於常寫 Rails plugin 的人,會想直接更動 Rails 3 原本的 Life Cycle 來滿足需求,這時只要引入一個 Rails::Railtie 子類別便可變更啟動程序,這是因為 ActionMailer, ActionController, ActionDispatch, ActionView, ActiveModel, ActiveRecord, ActiveResource 和 ActiveSupport 等都擁有一個繼承 Rails::Railtie 的類別,Rails 3 啟動程序主要工作就是正確誘發這些 railtie(請注意 require 的順序):
## in lib/my_gem.rb
module MyGem
# Make sure your Gem loads the railtie.rb file if Rails is loaded first.
require 'my_gem/railtie' if defined?(Rails::Railtie)
end
## in lib/my_gem/railtie.rb
require 'my_gem'
require 'rails'
module MyGem
class Railtie < ::Rails::Railtie
# This creates a config.my_gem in the user's Application
config.my_gem = MyGem
# configuration shared by all railties and the application
config.generators.test_framework :rspec
config.middlewares.use MyGem::MyMiddleware
config.to_prepare do
MyGem.config_setup!
end
# rails 3 notification system
require "my_gem/railties/log_subscriber"
log_subscriber MyGem::Railties::LogSubscriber.new
rake_tasks do
load "my_gem/setup.rake"
end
generators do
require "../../generators/another_generator.rb"
end
initializer :setup_my_gem, :before => :load_environment_config do |app|
app.config.locale = 'zh-TW'
app.config.time_zone = 'taipei'
end
end
end
關於以上程式內容,設定 config.my_gem 作為這個 rubygem 特有的設定值辨識名,config.* 可以設定所有 railtie 都讀得到的設定值,可定義 Rails::LogSubscriber 來處理 log 記錄,可用 rake_tasks 方法載入 rake tasks,用 generators 方法指定 generators 的位置,以及用 initializer 方法定義一些常量、變數等等(可以不需要在 config/initializers 下建設定檔),最後這些方法都會在執行 YourApp::Application < Rails::Application 時被誘發。
而平常寫的 Rails 3 Engine(小型嵌入式 rails 應用程式,會自動載入 app, config, lib 等目錄)分為兩種。一種是 Engines in gems,需要額外載入 Rails::Engine 類別(在 Rails.root/config/environment.rb 一開始就 require 進來或在 Gemfile 定義 gem ‘my_engine’),才會把這個 rubygem 啟動成為 rails engine,這是因為 Rails::Engine 本身也是 Rails::Railtie 的子類別(多了預設的 initializers 和特定的 methods),因此擁有類似的載入和設定方式:
## lib/my_engine.rb
module MyEngine
# Make sure your Gem loads the railtie.rb file if Rails is loaded first.
require 'my_engine/engine' if defined?(Rails::Railtie)
end
## lib/my_engine/engine.rb
require 'my_engine'
require 'rails'
module MyEngine
class Engine < ::Rails::Engine
config.my_engine = MyEngine
# Add a load path for this specific Engine
config.load_paths << File.expand_path("../../a_path", __FILE__)
config.eager_load_paths << File.expand_path("../../b_path", __FILE__)
config.load_once_paths << File.expand_path("../../c_path", __FILE__)
# Add/specify the load path for this specific Engine
paths.app.controllers << "lib/controllers"
paths.app.metals = "lib/metal"
end
end
另一種是 Engines in plugins,不需額外載入 Rails::Plugin 類別(這是 Rails::Engine 的子類別),必須依循預設規則,只要把 engine 程式放在 vender/plugins 下,Rails 3 就自動幫你處裡好,如同以往也會自動載入 PLUGIN_ROOT/init.rb,唯一要注意的是 Rails::Plugin 是最終類別,且同一個 engine 中不能同時載入另一個 Rails::Engine。
最後來看 Rails::Application,它本身就是個巨大的 Rails::Engine,以 Singleton 方式存在,主要工作是協調各 Railtie 的啟動程序,如 initialization, configuration, routes, middleware 和 metal(定義在 Rails::Application 目錄下 ),讓彼此和睦相處。
註:以上主要編修自 josevalim 的 gist 文件,並以 rails-3.0.0.beta2 這個版本為主,詳細的 method 用法請參考 Rails 原始碼。
研究 Rails 3 的時候看到 The Rails Module (in Rails 3) 這篇文章覺得很有趣,於是利用 looksee 把所有的 Rails module public method 都翻出來:
[Rails] application cache initialize! logger public_path= application= configuration initialized= logger= root backtrace_cleaner env initialized? public_path version
雖然大部分在 Rails 2.3.5 都已經支援,也不難查到,但還是得看過 source code,了解的夠深入,才能運用的恰到好處:
Rails.root.join("config", "database.yml")
Rails.env.development?
Rails.application.routes.recognize_path("rails/info/properties")
純粹做個記錄。
Rails 3 提供開發人員可改用 jquery-ujs,但並沒有將方法整合的很完整、方便,只好自己利用 generator 來管理 jquery 的 plugin,整理一下就變成 jquery_corpus 了。使用例子如下:
rails generate jquery:ui 1.8rc3 rails generate jquery:colorbox
由於機制上是透過 generator 來處理 jquery plugin 的 fetch、unzip、file copy、file create、file delete 等工作,因此使用者可以看到每個步驟的 log,並決定版本衝突時是否更動,增加了使用上的彈性。
因為一個 generator 對應一個 jquery plugin,必要時開發者只需要寫個 generator 和 spec 便可將新的 jquery plugin 納入管理,而目前 generator 只寫了上面兩個,有待日後的努力慢慢補足。
開發 Rails 應用程式時,常會使用 generate scaffold 的指令來建立基本的 MVC 架構,安裝 rubygem 時也會有 generate spec 這類的指令要執行,這些都是利用 generator 來達到設定上的自動化,Rails 3 改寫了這部份的實作方法,有必要重新熟悉一下。
基本上 Rails 3 的 generator 是繼承自 thor 的 Thor::Group 物件,並整合了 Rails 2.3 原本 generator 和 template 的方法成為 Rails::Generators::Actions。實作方面,我們只需要在 lib/generators 下建立一個 _generator.rb 檔,宣告一個繼承 Rails::Generators::Base(或 Rails::Generators::NamedBase、Thor::Group)的 Class,並定義要執行的 tasks (public instance method) 就完成了。
使用 bundler 管理 Rails 應用程式所需的 rubygem 非常好用,而用 jeweler 產生 rubygem 也十分簡單,以下便介紹如何撰寫一個 generator 來抓取 jquery-ujs 及其使用方法,並說明打包 rubygem 的流程。
首先安裝 jeweler 並建立一個新的 rubygem,命名為 generator_demo,採用 rspec 來做為 test framework,預計打包到 rubygems.org:
gem install jeweler
jeweler —rspec —gemcutter generator_demo
cd generator_demo
git log
jeweler 會用它的 generator 幫你建立 generator_demo 這個目錄及相關檔案,並自動使用 git 做版本控管。接著我們需要一個名為 jquery_ujs_generator.rb 的檔案。Rails 3 本身提供建立 generator template 的 generate 指令,使用 rails generate generator jquery_ujs 會自動在 Rails 3 應用程式的 lib 下建立 generator 所需的檔案。但這裡我們必須手動建立:
mkdir -p lib/generators/jquery_ujs
mkdir -p lib/generators/jquery_ujs/templates
touch lib/generators/jquery_ujs/jquery_ujs_generator.rb
touch lib/generators/jquery_ujs/USAGE
編輯 jquery_ujs_generator.rb 填入以下內容:
require 'rails/generators/base'
class JqueryUjsGenerator < Rails::Generators::Base
def self.source_root
@source_root ||= File.expand_path('../templates', __FILE__)
end
def fetch_rails_jquery_ujs_file
url = 'http://github.com/rails/jquery-ujs/raw/master/src/rails.js'
get url, 'public/javascripts/rails.js'
end
end
這裡我們定義一個 JqeuryUjsGenerator 類別,每個 instance method 將會依序直接被 invoke,而 method 的命名建議使用直述式的(類似 rpsec 的 it 敘述的命名)。其中 fetch_rails_jquery_ujs_file 使用了 thor 的 get 方法取得遠端的檔案,並存放於 public/javascript 底下。而 source_root 是定義本地端範本檔案所在的位置,因為我們直接從網路抓取檔案,所以用不到。這樣最簡單的 generator 便完成,其他常用的 method 請查閱 thor 的 rdoc 說明。
一般建議要養成撰寫測試的習慣,先將 spec/generator_demo_spec.rb 的 fail 敘述刪掉,再建立 generator 對應的 spec 檔案:
mkdir -p spec/generators
touch spec/generators/jquery_ujs_generator_spec.rb
編輯 jquery_ujs_generator_spec.rb,填入以下內容:
require 'spec_helper'
require 'generators/jquery_ujs/jquery_ujs_generator'
describe JqueryUjsGenerator do
before do
@generator = JqueryUjsGenerator.new([], {}, {})
end
it "fetch rails jquery-ujs file" do
url = 'http://github.com/rails/jquery-ujs/raw/master/src/rails.js'
@generator.should_receive(:get).with(url, 'public/javascripts/rails.js').and_return(true)
@generator.fetch_rails_jquery_ujs_file
end
end
如果使用 rspec 1.x 的話,直接在 spec/spec_helper.rb 加入:
require 'rails/generators'
便可檢測 spec 是否通過:
spec spec
但這裡打算直接使用 rspec 2,請先安裝好:
gem install rspec -v=2.0.0.beta.3
需要修改 spec/spec_helper.rb 成為:
$LOAD_PATH.unshift(File.dirname(__FILE__))
$LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__))
require 'rubygems'
require 'rspec'
require 'rspec/autorun'
require 'rails/generators'
require 'generator_demo'
Rspec.configure do |config|
config.mock_framework = :rspec
end
改用新的指令進行檢測:
rspec spec
最後準備打包。由於 jeweler 提供不少相關 rake task 可配合使用,包含自動產生 generator_demo.gemspec 的指令,這個 .gemspec 檔案是用來定義將程式打包成 rubygem 所需的 Gem::Specification,透過撰寫 jeweler task 的方式來簡化流程,因此只要將相關資訊填入 Rakefile 即可:
begin
require 'jeweler'
Jeweler::Tasks.new do |gem|
gem.name = "generator_demo"
gem.summary = %Q{A rails generator demo}
gem.description = %Q{tell you creating generator is quit simple}
gem.email = "tsechingho@gmail.com"
gem.homepage = "http://github.com/tsechingho/generator_demo"
gem.authors = ["Tse-Ching Ho"]
gem.add_development_dependency "rspec", ">= 2.0.0.beta.3"
end
Jeweler::GemcutterTasks.new
rescue LoadError
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
end
begin
require 'rspec/core/rake_task'
Rspec::Core::RakeTask.new(:spec)
Rspec::Core::RakeTask.new(:rcov) do |spec|
spec.rcov = true
end
task :spec => :check_dependencies
rescue LoadError
task :spec do
abort "Rspec 2 is not available. In order to run specs, you must: [sudo] gem install Rspec -v=2.0.0.beta.3"
end
end
第一段是建立 rubygem 的 jeweler task 設定,第二段則是替換 jeweler 原本的 spec task 改使用 rspec 2 的 spec task 寫法。接著便可在 local 端將這個 rubygem 裝起來:
rake -T
rake spec
rake version:write
rake gemspec
(rake build)
rake install
git add .
git commit -m ‘release 0.0.0; add jquery-ujs generator.’
先跑完 spec 後,建立 jeweler 需要的 VERSION 檔案來描述 rubygem 的版號,再產生 gemspec,安裝後可用 gem search generator_demo 找到 0.0.0 的版本,記得將更動納入版本控管。
在 Rails 3 應用程式的 Gemfile 中加入:
gem 'generator_demo'
便能直接使用這個 generator 了:
rails generate jquery_ujs
後續可將程式碼放到 github.com 上。先設定好 github 的 ssh token 並在 github 上建立好同名的 repository(這裡是 generator_demo),依照 github 的指示設定 git config(jeweler 已經預設好同名的 origin remote),之後便可 push 到 github 上去了:
git push origin master
再將產生的 rubygem 放到 rubygems.org 上供人使用。先註冊好 rubygems.org 的帳號,通過測試、增加 rubygem 的版號、納入版本控管並包裝後,便可 push 出去:
rake spec
rake version:bump:minor
rake gemspec
rake install
git add .
git commit -m ‘release 0.1.0’
git push origin master
gem push pkg/generator_demo-0.1.0.gem
以上便是一整套撰寫與發表 rubygem 的流程,以及 Rails 3/Thor generator 的簡易例子,希望說明夠簡潔易懂。
Rails 3 beta 釋放出來一個月了,需要開始為 Rails 2.3.5 的應用程式做轉換準備,首先第一步便是先把 gems 的管理交給 bundler 來處理。
步驟很簡單,首先把 bundler (>=0.9.5) 裝起來:
gem install bundler
再來修改 Rails 2 應用程式內的 config/boot.rb 檔案,在 Rails.boot! 這行之前加入下面的 patch 來將 bundler 包裹到 Rails 2 的啟動程序:
# add this to the bottom of config/boot.rb, before the line `Rails.boot!`
class Rails::Boot
def run
load_initializer
extend_environment
Rails::Initializer.run(:set_load_path)
end
def extend_environment
Rails::Initializer.class_eval do
old_load = instance_method(:load_environment)
define_method(:load_environment) do
Bundler.require :default, Rails.env
old_load.bind(self).call
end
end
end
end
接著,建立新的設定檔 config/preinitializer.rb 來套用 bundler 設定:
# this code goes in config/preinitializer.rb, which you should create if it doesn't exist
begin
# Require the preresolved locked set of gems.
require File.expand_path('../../.bundle/environment', __FILE__)
rescue LoadError
# Fallback on doing the resolve at runtime.
require "rubygems"
require "bundler"
if Gem::Version.new(Bundler::VERSION) <= Gem::Version.new("0.9.5")
raise RuntimeError, "Bundler incompatible.\n" +
"Your bundler version is incompatible with Rails 2.3 and an unlocked bundle.\n" +
"Run `gem install bundler` to upgrade or `bundle lock` to lock."
else
Bundler.setup
end
end
然後建立 bundler 環境和 Gemfile 檔案:
bundle init
這個指令會自動幫你在 Rails 2 應用程式根目錄下加入 .bundle 目錄和 Gemfile 設定檔:
# A sample Gemfile source :gemcutter # # gem "rails"
請記得將 .bundle 這個隱藏目錄加入 .gitignore 裡面:
.bundle
最後,將原本在 config/environment.rb 上的 config.gem 設定都搬到 Gemfile 內,例如:
source :gemcutter gem 'rails', '~> 2.3.5', :require => nil gem 'sqlite3-ruby', :require => 'sqlite3' gem 'mongrel' group :development do gem 'rails-footnotes', '>= 3.6.3' end group :test do gem 'rspec-rails' end group :cucumber do gem 'database_cleaner', '>= 0.5.0', :require => nil gem 'cucumber-rails', '>= 0.3.0', :require => nil gem 'webrat', '>= 0.7.0', :require => nil gem 'rspec-rails', '>= 1.3.2', :require => nil end
再來檢查設定有沒有成功:
bundle check
bundle install
bundle show
script/server
一切無誤,大功告成。
番外篇:
有時候還是會遇上些小問題,我在使用 preferences 這個 gem 時,就會遇到 uninitialized constant Preferences::InstanceMethods::Preference 的錯誤,解決方法是乖乖的把 config.gem ’preferences’ 加回 config/environment.rb 去。
以現今的技術來說,要開發一套網路應用程式,幾乎都需要建立使用者認證(authentication)與授權(authorization)機制,加上身為華人的我們,中文界面(i18n)更是必要的功能,像 Rails 這種以敏捷開發為特色的網頁程式語言框架(web framework),更是擁有眾多好用且多樣的開放源碼(Open Source)套件可供選擇。
隨著開發者使用的套件越來越多樣,要啟動一個新專案開發一套新的應用程式,變得不斷在重複弄相關必需套件的設定與修改,然後才能專心於新專案功能的開發。Rails 2.3 引入了 template 的功能,讓開發者能將熟稔的套件組合拼湊成一個模板,自動化地套用在新應用程式中,加上內建的 engines 機制,讓程式 MVC 部份可以切割出來放在 Plugin 中,強化了程式模組化的能力,根據這兩樣功能,打造一個程式開發包變成輕鬆且有意義。
以目前主流的 Rails 開放源碼套件來說,使用者認證以 Resful_authentication , Clearance , Authlogic 這三個套件最受歡迎,並且常會配合使用 Open ID Authentication 作為單一認證的方式。而使用者授權套件則更為多樣,從簡單的 Role Requirement 到功能齊全的 Rails Authorization Plugin , Declarative Authorization 都有。
於是根據個人的喜好,將 Authlogic, Open ID Authentication, Declarative Authorization 打包成 Authlogic Bundle,並支援 Rails-i18n 及 Git 版本控制系統,以開放源碼的形式給喜愛 Rails 的開發者使用,使用方式十分簡單,只要一行指令:
$ rails your-app-name -m http://github.com/tsechingho/authlogic_bundle/raw/master/templates/remote.rb
便可以輕鬆地開始撰寫專案程式了。修改方式也很容易,利用 engines 的特色,只要將 Plugin 中的程式檔案拷貝到相對應路徑便可以改寫程式行為與界面。如果有任何建議,隨時歡迎指教。