iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 21
0
Modern Web

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

[DAY 21] 復刻 Rails - 用 Rails 的方式整理程式碼 Active Support

還記得之前我們實作 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

關於 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

擴充 String

之前我們的寫法是這樣

# 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

接著我們修改一下 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_underscoreMavericks.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

接著我們回到久違的 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


上一篇
[DAY 20] 復刻 Rails - 用 Rails 的方式整理程式碼 Active Record
下一篇
[DAY 22]復刻 Rails - Application 啟動過程
系列文
向 Rails 致敬!30天寫一個網頁框架,再拿來做一個 Todo List30

尚未有邦友留言

立即登入留言