iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 6
0
Modern Web

向 Rails 致敬!30天寫一個網頁框架,再拿來做一個 Todo List系列 第 6

[DAY 6] 復刻 Rails - 關於 Autoloading

如果讀者有跟著我一起做到今天,會發現前面幾天在使用 Mavericks 時,每次寫完 code,都需要手動 require 檔案,再重啟伺服器,讓 server 載入新的 code,接著重新整理畫面才會出現新的結果,開發時間一拉長,就會覺得很阿雜...

但如果你開發過一陣子的 Rails,一定會對 Rails 修改程式碼,只需要回到網頁按下重新整理,就會出現改變的結果,感到印象深刻,為什麼 Rails 可以做到?在實作這些東西之前,我們先來建立一些觀念

Ruby 的 require 都在做些什麼?

在實作 Rails 的載入機制之前,我們要先花點時間來了解 require,相信大家都不陌生在 Ruby 裡面要使用其他套件,可以這樣子寫

require 'mavericks'

但你曾經好奇過, Ruby 怎麼知道要去哪裡找嗎?其實都是基於 $LOAD_PATH 這個 global variable ,裡面存放了許多路徑,有興趣的人可以開啟 irb 來查看$LOAD_PATH

2.6.6 :001 > $LOAD_PATH
 => ["/.rvm/rubies/ruby-2.6.6/lib/ruby/gems/2.6.0/gems/did_you_mean-1.3.0/lib"..."]

這也是為什麼在 Ruby 裡面可以直接 require 那些套件的原因,但是光只有 $LOAD_PATH 是不夠的,Ruby 還有另一個 global variable $LOADED_FEATURES,來存放已經 require 的檔案路徑,為了要驗證 Ruby require 行為,我們先建立一個 hola/ 的資料夾,裡面放一個空的檔案叫hello.rb,接著在同個目錄下開啟 irb,執行以下程式碼觀察輸出結果

# 先印出 $LOADED_FEATURES
2.6.6 :001 > $LOADED_FEATURES.grep /hello.rb/
 => []

2.6.6 :002 > require './hello.rb'
 => true

# 再檢查一次 $LOADED_FEATURES
2.6.6 :003 > $LOADED_FEATURES.grep /hello.rb/
 => ["/Users/apa/yuapa/it_30_day/demo/hola/hello.rb"]

# 再 require 一次就會出現 false
2.6.6 :004 > require './hello.rb'
 => false

第一次檢查 $LOADED_FEATURES 結果會發現是空值,也因為是空值,路徑還沒被加到 $LOADED_FEATURES,所以 require 後 return 的結果為 true,接著再檢查一次 $LOADED_FEATURES 會發現這時候已經包含了 hello.rb,執行第二次 require 會發現結果為 false

關於 Ruby 尋找常數這件事

現在我們已經了解 Ruby 怎麼將檔案載入,但載入後,有這麼多的 module 和 class,那尋找的機制是什麼?這裡列出三個規則

  1. Each entry in Module.nesting
  2. Each entry in Module.nesting.first.ancestors
  3. Each entry in Object.ancestors if Module.nesting.first is nil or a module.

其中我們來看第一條規則

# hola/hello.rb

C = "At the top level"

module A
  C = "In A"
end

module A
  module B
    puts Module.nesting # => [A::B, A]
    puts C              # => "In A"
  end
end

module A::B
  puts Module.nesting # => [A::B]
  puts C              # => "At the top level"
end

透過上面的範例我們可以知道,Ruby 會參考 Module.nesting 來尋找常數,因為第一個例子 Module.nesting 有包含 module A,所以就 Ruby 就會尋找到 A::C,第二個因為沒有包含,所以就往上找到 ::C

那個 Ruby 似乎有內建 autoload 的功能?

除了剛剛介紹的那些基本觀念以外,Ruby 其實還有內建 autoload 的功能

module A
  # 只有建立 constant B
  autoload(:B, './b.rb')
  
  # 執行當下才會載入檔案
  B
end

只要給予路徑和常數名稱,就可以執行,但有趣的是,在還沒真正執行之前,Ruby 只有先建立常數,並沒有真正把檔案載入,直到執行的當下,才去把檔案載入進來

那 Rails 呢?

但是對於 Rails 來說,他不能直接使用 Ruby 的 autoload,因為這樣的方式需要先知道檔案的路徑和名稱,所以 Rails 另外建立了一套自己的機制,還記得在學習 Rails 常提到的 convention over configuration (慣例優於設定)嗎?在開發 Rails 專案時,我們會依照慣例將 Controller 放在 app/controllers,Model 放在 app/model,靠著這樣約定好的機制,搭配另一個大家可能比較少用到的 const_missing,來達到 autoload 的效果

autoload 的起點 const_missing

const_missing 跟另外一個 method_missing 很像,都是 Ruby 找不到東西時觸發的方法,只是一個針對 常數,一個針對 mehtod,利用前面建立的 hello.rb,來做一個範例觀察看看,

# hola/hello.rb

module Hola
  def self.const_missing(name)
    puts "In #{self} looking for #{name}..."
  end
end

接著打開 irb

# require hello.rb
2.6.6 :001 > require './hello.rb'
 => true

2.6.6 :002 > Hola::Amo
In Hola looking for Amo...
 => nil

當你在 Ruby 裡面使用了一個常數,Ruby 會先依照前面提到的規則去尋找,如果沒有找到,就會呼叫 Module#const_missing,以上面的例子來說就是 Hola.const_missing("Amo"),而 Rails 就是覆寫了這個 method,在沒有找到常數時,利用我們剛剛提到的 convention over configuration,去找相對應的檔案載入進來,來「猜」開發者想要的常數是什麼

跟 Ruby 的 預先知道檔案名稱 相反的是,Rails 是我不需要現在知道檔案名稱,但你需要按照約定的方法放檔案,等要用到的時候,我在去載入,而 Rails 預設是載入 app/ 底下的檔案,也可以透過 autoload_paths 來做修改

可能需要花點時間理解...

現在我們知道 Ruby 的 request 的基本原理,也大概了解 Ruby 尋找常數的規則,也練習使用 const_missing, Rails 在這些基礎原理上,做了其他更多的事情,這也是 Rails 時常被稱呼為黑魔法的原因之一,而明天我們將要來實作剛剛所提到的,怎麼樣用 convention over configuration 搭配 const_missing,來實現 autoload

參考:
Rails autoloading — how it works, and when it doesn't
Everything you ever wanted to know about constant lookup in Ruby


上一篇
[DAY 5] 復刻 Rails - 再替 Controller 做點加強
下一篇
[DAY 7] 復刻 Rails - 再加一點 Autoloading
系列文
向 Rails 致敬!30天寫一個網頁框架,再拿來做一個 Todo List30

尚未有邦友留言

立即登入留言