本文使用當下最新的 Ruby 3.0.1 和 Rails 6.1.4 作為示範
什麼是 Rake
Rake 就像是 Ruby 的 Make,或是 Golang 的 Mage,其實也就是可以讓我們寫一些基本的 task 和 build 程序的地方。
在 Ruby on Rails 裡面,已經自帶了幾個常用的 rake tasks(有時候我們也會簡稱 rake):
rake about
rake db:create
rake db:migrate
rake db:seed
當然還有更多,可以用 rake -T
來查看。
Rails 的 Rakefile
Rakefile 其實就是 Ruby 的 Makefile,是用來定義 rake 的地方。
不過如果你在 Rails 的框架中打開 Rakefile,會發現只有簡單兩行:
require_relative "config/application"
Rails.application.load_tasks
如果你把 Rails 的原始碼打開,發現其中第二行會去讀取 lib/tasks
裏面的所有檔案:
# https://github.com/rails/rails/blob/v6.1.4/railties/lib/rails/engine.rb
def load_tasks(app = self)
require "rake"
run_tasks_blocks(app)
self
end
...
def run_tasks_blocks(*) #:nodoc:
super
paths["lib/tasks"].existent.sort.each { |ext| load(ext) }
end
也就是說我們不只可以在 Rakefile
寫 task,也可以在 lib/tasks
寫,更方便大型專案的管理。
附帶一提,因為 Rails.application
繼承了上面的 Engine
class,他還會另外去讀取 Rails 自定義的常用 rake 們,也就是剛剛提到的 db:create
之類的 rakes:
# https://github.com/rails/rails/blob/v6.1.4/railties/lib/rails/application.rb#L526
def run_tasks_blocks(app) #:nodoc:
railties.each { |r| r.run_tasks_blocks(app) }
super
load "rails/tasks.rb"
task :environment do
ActiveSupport.on_load(:before_initialize) { config.eager_load = config.rake_eager_load }
require_environment!
end
end
而且在 Rails 中最基本的 environment
rake 也是定義在這裡,他會去讀取 config/environment
這個檔案。
# https://github.com/rails/rails/blob/v6.1.4/railties/lib/rails/application.rb#L365
def require_environment! #:nodoc:
environment = paths["config/environment"].existent.first
require environment if environment
end
Rake 怎麼寫
實際打開一個 Rails 新專案,會發現 lib/tasks
這個資料夾已經幫我們保留下來了。在裡面新增一個 rake task:
# lib/tasks/hello.rake
namespace :hello do
desc 'print hello world'
task :world do
puts 'Hello, world!'
end
end
這裡出現了三個 function,或著是說,Rake 的 DSL:
namespace
: 如其名,可以幫助 rake task 命名與分類管理desc
:rake -T
時顯示的描述,rake 必須加上desc
才會顯示在rake -T
的列表中task
: 定義 task 名稱與內容
附上 Rake 的文件,裡面有更完整的用法:https://ruby.github.io/rake/Rake/DSL.html
執行剛寫好的 rake task:
$ bundle exec rake hello:world
Hello, world!
Task Dependencies
task
這個 DSL 可以吃除了名稱之外的參數:
task task_name
task task_name: dependencies
task task_name, arguments → dependencies
剛剛我們用的是第一個定義。
接下來介紹 dependencies,其實就是在執行這個 rake task 時,哪些 rake 需要被確保執行。
我們舉最常用的 database 操作相關的 rake 為例子(只截取部分片段):
# https://github.com/rails/rails/blob/v6.1.4/activerecord/lib/active_record/railties/databases.rake
db_namespace = namespace :db do
desc "Creates the database, loads the schema, and initializes with the seed data (use db:reset to also drop the database first)"
task setup: ["db:create", :environment, "db:schema:load", :seed]
end
這是 db:setup
這個 rake task 的定義,傳給 task 的是個 hash,一個 key value pair,括號在這裡省略了,setup
是 task_name
,而後面的 array 則是 dependencies
。
這段 code 的意思是說,當我執行 db:setup
時,會需要先跑 dependencies 裡面的 task。
這裡的「先」指的是和 task 後面接的 block 相比較,會先跑 dependencies 才跑 block 內的東西。不巧 db:setup
剛好沒有 block,所以就是把 db:create
、environment
、db:schema:load
、seed
這幾個 rake 依序執行。
我們可以修改剛剛的 hello:world
task 來測試 block 和 dependencies 的順序:
# lib/tasks/hello.rake
namespace :hello do
desc 'print hello world'
task world: %w[hello:man] do
puts 'Hello, world!'
end
task :man do
puts 'Hello, man.'
end
end
再跑一次 hello:world
:
$ bundle exec rake hello:world
Hello, man.
Hello, world!
會發現寫在 dependencies 的 hello:man
的確會先被執行,接著才執行 block 內的東西。
那既然叫做 dependencies,如果被呼叫很多遍,會發生什麼事呢?
可以在同個 namespace 內再定義一個 rake 來實驗看看:
# lib/tasks/hello.rake
namespace :hello do
desc 'print hello world'
task world: %w[hello:man hello:earth] do
puts 'Hello, world!'
end
task earth: %w[hello:man] do
puts 'Hello, Earth~'
end
task :man do
puts 'Hello, man.'
end
end
現在 world
depends on man
和 earth
,而 earth
也 depends on man
。根據我們對 dependency (相依)這個詞的了解,hello:man
應該只會被執行一遍。
而事實正是如此:
$ bundle exec rake hello:world
Hello, man.
Hello, Earth~
Hello, world!
可以使用 --trace
來追蹤 rake task 彼此之間被 invoke 和執行的過程:
$ bundle exec rake hello:world --trace
** Invoke hello:world (first_time)
** Invoke hello:man (first_time)
** Execute hello:man
Hello, man.
** Invoke hello:earth (first_time)
** Invoke hello:man
** Execute hello:earth
Hello, Earth~
** Execute hello:world
Hello, world!