2012年8月30日 星期四

[Ruby] Ruby的頭號Gem:Rake

文章來源:http://blog.csdn.net/smilewater/article/details/1683808

Rake簡介

RakeMakeAnt
Rake的意思是Ruby Make,一個用ruby開發的代碼構建工具。Rake的英文意思是耙子,一種很樸實的勞動工具。真的是很貼切,Rake正是一個功能強大、勤勤懇懇的勞動工具。
Rake會經常跟C/C++領域的makeJava世界的Ant進行對照,事實上,它們有很多相似的地方。我們先來看一下makeant的歷史。
make的出現是為瞭解決批量編譯的問題。對於一個小型的項目來說,用一個腳本文件或者批處理命令來進行批量編譯就已經足夠好。但是對於大型的項目來說,僅僅為了少數幾個文件的改變就全部重新進行一次編譯無疑是耗時且不必要的。而且,在大型的項目中,往往會有很複雜的依賴關係。
Make的出現就是為瞭解決這兩個問題,make有兩個優點:
  1. Make瞭解自上次Make運行以來哪些文件發生了變化,它會僅僅編譯那些發生變化的文件。
  2. Make會跟蹤文件之間的依賴性,如果文件A依賴於文件B,那麼如果兩者都沒有編譯時,Make會首先編譯文件B
Ant算是一個Java世界的make,它要比make年輕許多(想想make是出現在1972年吧),它除了支持批量編譯之外,還支持單元測試、JavaDoc等任務。因此,AntJava世界中比Make更加流行。
但是,為什麼Ruby需要Rake
Ruby代碼不需要編譯,為什麼需要Rake?其實,與其說Rake是一個代碼構建工具,不如說Rake是一個任務管理工具,通過Rake我們可以得到兩個好處:
  1. 以任務的方式創建和運行腳本
當然,你可以用腳本來創建每一個你希望自動運行的任務。但是,對於大型的應用來說,你幾乎總是需要為數據庫遷移(比如Railsdb:migrate任務)、清空緩存、或者代碼維護等等編寫腳本。對於每一項任務,你可能都需要寫若干腳本,這會讓你的管理變得複雜。那麼,把它們用任務的方式整理到一起,會讓管理變得輕鬆很多。
  1. 追蹤和管理任務之間的依賴
Rake還提供了輕鬆管理任務之間依賴的方式。比如,"migrate"任務和"schemadump"任務都依賴於 "connect_to_database"任務,那麼在"migrate"任務調用之前,"connect_to_database"任務都會被執行。
在哪裡可以獲得Rake
Rake的主頁是在http://rake.rubyforge.org/,在這裡你可以獲得Rake的簡單介紹,API以及一些有用文檔的鏈接。可以在http://rubyforge.org/frs/?group_id=50獲得最新版的Rake,在作者寫作時,最新版本是0.7.3

Rake腳本編寫

一個簡單腳本

Rake的腳本相當簡單,下面用一個例子進行說明。假設你是一個勤勞的家庭型程序員,在週末你打算為你的家人做一些貢獻。所以你為自己制定了三個任務:買菜、做飯和洗衣服。打開你的文本編輯器,創建一個名叫rakefile的文件(Rake會在當前路徑下尋找名叫RakefilerakefileRakeFile.rbrakefile.rbrake文件),並輸入如下內容:
desc "任務1 -- 買菜"
task :purchaseVegetables do
puts "到沃爾瑪去買菜。"
end

desc "任務2 -- 做飯"
task :cook do
puts "做一頓香噴噴的飯菜。"
end

desc "任務3 -- 洗衣服"
task :laundry do
puts "把所有衣服扔進洗衣機。"
end
打開命令行工具,進入這個文件所在目錄,然後運行下面的命令,大致應該類似如下結果:
D:/work/ruby_works/ruby_book>rake purchaseVegetables
(in D:/work/ruby_works/ruby_book)
到沃爾瑪去買菜。

D:/work/ruby_works/ruby_book>rake cook
(in D:/work/ruby_works/ruby_book)
做一頓香噴噴的飯菜。

D:/work/ruby_works/ruby_book>rake laundry
(in D:/work/ruby_works/ruby_book)
把所有衣服扔進洗衣機。


分析
很簡單,也很易讀,對吧。這個文件一共定義了3個任務,descRake定義的方法,表示對下面定義任務的描述。這個描述會在使用Rake --tasks(或者Rake -T,為懶人準備的快捷方式)命令時輸出在屏幕上。
D:/work/ruby_works/ruby_book>rake --tasks
(in D:/work/ruby_works/ruby_book)
rake cook #
任務2 -- 做飯
rake laundry #
任務3 -- 洗衣服
rake purchaseVegetables #
任務1 -- 買菜


下面的語句定義了purchaseVegetables這個任務,taskRake最重要的方法。它的方法定義是:task(args, &block)。任務體是一個block,本例中只是簡單輸出你所要做的工作。需要注意的是代碼
puts "到沃爾瑪去買菜。"
完全是一個普通的Ruby語句,putsRuby中進行輸出的一般性方法,可以看出,Rake任務可以完全使用Ruby的能力,這使得它非常強大。

加入依賴關係

很顯然,在我們定義的任務中,做飯是依賴於買菜的(我相信大多數程序員在週末的冰箱裡除了可樂沒有別的)。那麼,我們需要在我們的任務定義中加入這個依賴關係,修改後的文件如下:
desc "任務1 -- 買菜"
task :purchaseVegetables do
puts "
到沃爾瑪去買菜。"
end

desc "
任務2 -- 做飯"
task :cook => :purchaseVegetables do
puts "
做一頓香噴噴的飯菜。"
end

desc "
任務3 -- 洗衣服"
task :laundry do
puts "
把所有衣服扔進洗衣機。"
end


再次運行做飯任務,你會得到如下結果:
D:/work/ruby_works/ruby_book>rake cook
(in D:/work/ruby_works/ruby_book)
到沃爾瑪去買菜。
做一頓香噴噴的飯菜。


是的,你當然需要先買菜,誰讓你是一個冰箱空空如野的程序員呢。

命名空間

跟任何編程語言類似,當你的rake文件很多時,當你有很多任務的時候,你需要關注它們的命名衝突問題,命名空間(namespace)就是一個自然的解決方案。你可以為上面的三個任務定義一個叫做home的命名空間。
namespace :home do
desc "
任務1 -- 買菜"
task :purchaseVegetables do
puts "
到沃爾瑪去買菜。"
end
……
end


再次運行rake --tasks,你會得到如下的結果:
D:/work/ruby_works/ruby_book >rake --tasks
(in D:/work/ruby_works/ruby_book)
rake home:cook #
任務2 -- 做飯
rake home:laundry #
任務3 -- 洗衣服
rake home:purchaseVegetables #
任務1 -- 買菜


你現在需要使用rake home:cook才能啟動做飯這個任務了。當然,你可以在你的rakefile中使用多個命名空間,對任務進行分類。

在一個任務中調用另外一個任務

當任務眾多的時候,你很可能需要在一個任務中調用另外一個任務,假設我們把今天所有要做的工作定義為一個任務:today。在這個任務中,有兩個任務需要被調用,一個是做飯,一個是洗衣服。當然,由於做飯依賴於買菜,我們還是需要買菜的(這一步是逃不過去的,呵呵)。在文件的頂部定義一個today的任務:
desc "今天的任務"
task :today do
Rake::Task["home:cook"].invoke
Rake::Task["home:laundry"].invoke
end

namespace :home do
……
end


可以看出,調用其它任務的方式很簡單,只需要調用Rake::Task["task_name"].invoke 方法就可以了。在命令行中啟動rake today,可以得到:
D:/work/ruby_works/ruby_book >rake today
(in D:/work/ruby_works/ruby_book)
到沃爾瑪去買菜。
做一頓香噴噴的飯菜。
把所有衣服扔進洗衣機。


默認任務

可以為Rake增加一個默認任務,這樣可以簡單地用Rake命令來觸發這個默認任務,在上面的rakefile中,我們可以用如下方式把"today"任務作為默認任務。
task :default => [:today]
然後調用直接在命令行中調用rake,可以得到跟調用rake today同樣的輸出結果。
這就是我們簡單的一個Rake任務定義,下面是完整的修改後的rakefile
task :default => [:today]

desc "
今天的任務"
task :today do
Rake::Task["home:cook"].invoke
Rake::Task["home:laundry"].invoke
end

namespace :home do

desc "
任務1 -- 買菜"
task :purchaseVegetables do
puts "
到沃爾瑪去買菜。"
end

desc "
任務2 -- 做飯"
task :cook => :purchaseVegetables do
puts "
做一頓香噴噴的飯菜。"
end

desc "
任務3 -- 洗衣服"
task :laundry do
puts "
把所有衣服扔進洗衣機。"
end
end


Rails中的Rake任務

Rails預定義了大量的Rake任務,在Rails應用的開發過程中,你想必已經在大量使用它們了。在Rails中,所有的Rake任務都放在rails目錄的lib/tasks目錄下(在作者的環境下是c:/ruby/lib/ruby/gems/1.8/gems/rails-1.1.4/lib/tasks/),所有的rake任務都以.rake作為後綴名,這些以.rake結尾的文件會被自動加載到你的環境中。你可以到一個已有的Rails工程根目錄下鍵入rake --tasks,可以看到很多的rake任務已經為你整裝待發了。
Rails中,最常使用的Rake任務之一是進行數據庫的遷移(migration)。數據庫遷移程序允許你使用Ruby腳本來定義數據庫模式,而db:migrate就是進行這個工作的rake任務。下面我們來分析這個rake任務。
db:migrate任務
db:migrate任務存放在lib/tasks/databases.rake文件中。這個文件中定義了所有與數據庫操作相關的任務,我們僅僅抽出db:migrate的定義:
namespace :db do
desc "Migrate the database through scripts in db/migrate. Target specific version with VERSION=x"
task :migrate => :environment do
ActiveRecord::Migrator.migrate("db/migrate/", ENV["VERSION"] ? ENV["VERSION"].to_i : nil)
Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby
end
……
end


分析
首先是命名空間的聲明,migrate任務的命名空間是db。這也就是我們用db:migrate來引用它的原因。
下面是一個描述,說明該任務的功能是把定義在db/migrate目錄下(相對於你的Rails應用程序的根目錄)的遷移腳本遷移到數據庫中,如果不指定VERSION的話,默認是最新版本,否則可以恢復到一個指定的版本。
接著是任務的定義,該任務依賴於enviroment任務,這個任務在misc.rake中定義,用來加載Rails的環境,它的定義相當簡單:
task :environment do
require(File.join(RAILS_ROOT, 'config', 'environment'))
end


用來加載config/environment.rb文件,該文件會加載Rails工作所需要加載的環境。由於加載了這個環境,所以ActiveRecord對象現在可以使用,下面就是調用ActiveRecord::Migrator.migrate方法對每個db/migrate/下的腳本文件進行遷移。
最後會調用db:schema:dump任務,該任務的主要作用是產生db/schema.rb文件。該文件用來記錄不同版本的數據庫模式。這個任務的定義就在db:migrate任務下面不遠的地方,有興趣的讀者可以自行進行分析。

【下列文章您可能也有興趣】

2 則留言:

HzChris 提到...

你好,我覺得這篇很棒,請問可以轉貼到我自己的部落格嗎?(會附上原出處

千江有水千江月 提到...

歡迎