還記得之前我們實作 dependencies.rb 這個檔案的目的是什麼嗎?
# mavericks/lib/mavericks/dependencies.rb
class Object
  def self.const_missing(const)
    return nil if @called_const_missing
    @called_const_missing = true
    require Mavericks.to_underscore(const.to_s)
    klass = Object.const_get(const)
    @called_const_missing = false
    klass
  end
end
搭配在專案 just_do 的 config/application.rb
$LOAD_PATH << File.join(File.dirname(__FILE__), "..", "app", "controllers")
就可以做到 autoloading 的效果,不用每次新增一個 controller 的檔案就 require 一次,不過這樣做會有一些問題,原因是我們不應該處理所有的 const_missing,這樣當其他套件沒處理 const_missing 的狀況時,反而被我們給 "處理" 了,簡單說就是管太多,所以我們今天的目標除了要修正這個問題,另外也將檔案整理成 Active Support
在 Rails 裡面 Active Support 是一個擴充的工具函式庫,意思就是當你用 Rails 來寫網站時,常常會用到的一些函式庫,這些函式庫是 Ruby 本身沒有支援或是說不夠完整(畢竟 Ruby 本身不是為了寫網站而做得語言),例如像是 Rails 的 autoloading
要設計成 Active Support 的樣子,就要把原先掛在 Mavericks 底下的 dependencies.rb,搬移到 Active Support 底下,並且建立 Active Support 的資料夾,另外在 Mavericks 裡面,我們會處理一些文字的轉換,例如將 TasksController 轉成 tasks_controller,所以我們要擴充 Ruby 的 String
依照規劃,預期的資料夾結構會成為這樣
.
├── lib
│   ├── active_support
│   │   ├── core_ext
│   │   │   └── string.rb
│   ├── active_support.rb
之前我們的寫法是這樣
# mavericks/lib/mavericks/support.rb
module Mavericks
  def self.to_underscore(string)
    string.gsub(/::/, '/')
      .gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
      .gsub(/([a-z\d])([A-Z])/,'\1_\2')
      .tr("-", "_").downcase
  end
  def self.to_plural(string)
    pattern = /.*s$|x$|z$|sh$|ch$/
    pattern.match?(string) ? "#{string}es" : "#{string}s"
  end
end
但這樣有個問題,就是每次要使用都要這樣呼叫
Mavericks.to_underscore(const.to_s)
有點不太直覺,我們希望可以直接在  Ruby 的字串上直接呼叫 to_underscore 這個方法,像是這樣
'TasksController'.to_underscore
但是 Ruby 的 String 不是已經定義好了嗎?這裡我們可以用 Open Classes 這個技巧,來做到新增 method 到已經定義好的 Class,舉個例子來說,我們可以幫字串建立一個方法叫 tell_me_size
class String  
  def tell_me_size  
    self.size  
  end  
end  
puts '鐵人30'.tell_me_size  
這樣我們就可以新增或是複寫 method,知道原理以後,就來實作吧
我們在 string.rb 新增兩個 method
# mavericks/lib/active_support/core_ext/string.rb
class String
  def to_underscore
    self.to_s.gsub(/::/, '/')
      .gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
      .gsub(/([a-z\d])([A-Z])/,'\1_\2')
      .tr("-", "_").downcase
  end
  def to_plural
    str = self.to_s
    pattern = /.*s$|x$|z$|sh$|ch$/
    pattern.match?(str) ? "#{str}es" : "#{str}s"
  end
end
接著打開 irb 來看看效果
2.6.6 :001 > require('./lib/active_support/core_ext/string.rb')
 => true
2.6.6 :002 > 'TasksController'.to_underscore
 => "tasks_controller"
2.6.6 :003 > 'task'.to_plural
 => "tasks"
這樣就完成 String 的擴充了!
接著我們修改一下 dependencies.rb,開頭有說過我們原先的寫法有很大的問題就是「管太多」,所以我們要修正一下邏輯
module ActiveSupport
  module Dependencies
    extend self
    attr_accessor :autoload_paths
    self.autoload_paths = []
    def search_for_file(name)
      autoload_paths.each do |path|
        file = File.join(path, "#{name}.rb")
        if File.file? file
          return file
        end
      end
      nil
    end
  end
end
class Module
  def const_missing(name)
    if file = ActiveSupport::Dependencies.search_for_file(name.to_s.underscore)
      require file.sub(/\.rb$/, '')
      const_get name
    else
      raise NameError, "Uninitialized constant #{name}"
    end
  end
end
一樣我們用 const_missing 來攔截找不到常數的狀況,接著我們加上一個屬性叫 autoload_paths,來紀錄要搜尋常數的檔案位置,例如 app/controllers/ 或是 app/models 之類的資料夾,這樣我們就可以過濾掉那些不在我們檔案範圍內的狀況
接著我們就可以把昨天的 ActiveRecord 裡面的 mavericks/support 拿掉
module ActiveRecord
  # 拿掉原先的 support
  # autoload :Mavericks, "mavericks/support"
  autoload :Base, "active_record/base"
  autoload :Persistence, "active_record/persistence"
  autoload :ConnectionAdapter, "active_record/connection_adapter"
  autoload :Relation, "active_record/relation"
end
然後原先在 Mavericks 有用到的 Mavericks.to_underscore 和 Mavericks.to_plural 都要做修正
# mavericks/lib/active_record/base.rb
def self.table_name
  singular_table_name = name.to_s.to_underscore
  singular_table_name.to_plural
end
# mavericks/lib/mavericks/controller.rb
def controller_name
  klass = self.class
  klass = klass.to_s.gsub /Controller$/, ""
  klass.to_underscore
end
別忘了要在 active_support.rb 做 autoload,跟 active_record.rb 一樣
# mavericks/lib/active_support.rb
require 'active_support/core_ext/string'
module ActiveSupport
  autoload :Dependencies, 'active_support/dependencies'
end
因為 string.rb 並不是在 module ActiveSupport 底下,所以還是要另外 require 進來
接著在 mavericks.rb 上面也要 require 新的程式碼進來
# mavericks/lib/mavericks.rb
require "mavericks/version"
require "active_support"
require "active_record"
require "mavericks/routing"
require "mavericks/controller"
.
.
(略)
接著我們回到久違的 just_do,來用我們小改版的 Mavericks,先在 config/application.rb 做修改,讓專案在初始化啟動時,就將需要的設定做載入
# just_do/config/application.rb
require 'mavericks'
# 建立連線
ActiveRecord::Base.establish_connection
# autoload 預設路徑
ActiveSupport::Dependencies.autoload_paths = Dir["./app/*"]
module JustDo
  class Application < Mavericks::Application
  end
end
在專案初始化時建立連線以外,還要給 autoload 的預設路徑,其中 autoload_paths 就是我們剛剛提到不要讓 const_miss 管太多的部分
接下來的用法就跟 Rails 一樣,我們新增一個 Model 的檔案
# just_do/app/models/task.rb
class Task < ActiveRecord::Base
end
然後修改一下 Controller
# just_do/app/controllers/tasks_controller.rb
class TasksController < Mavericks::Controller
  def index
    @tasks = Task.all
  end
  def show
    @task = Task.find(params['id'])
  end
end
要記得現在取出的 Model 要呼叫 Attribute 是這樣呼叫 task.title,所以在 View 的檔案那邊也要做修改
# 原先的寫法
task['title']
# 改版後的寫法
task.title
然後啟動 server 看看現在專案可不可以執行起來,如果可以代表成功了!
如果不行可以參考我傳的 github 或是 debug 看看錯在那裏
https://github.com/apayu/mavericks