iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 7
0
Modern Web

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

[DAY 7] 復刻 Rails - 再加一點 Autoloading

昨天我們提到了 Ruby 的 const_missing,也知道在 Ruby 的世界裡面,用 class 關鍵字定義的時候會自然存在 常數,我們也提到 Rails 因為沒辦法預先知道檔名和位置,需要靠著 convention over configuration 的方式,來判斷檔案的位置,並且「猜」出開發者所需要的常數(也就是 Class)是什麼,今天就來將這些技巧結合在一起

有蹊蹺的命名規則

再開始之前,首先來談談關於 Rails 的檔案命名規則,在程式碼的世界裡面有許多命名的規則,在 Rails 裡面,一般我們會用 Camel Case(駝峰式大小寫)的方式,來表示 Class,例如 HomeController 或是 TasksController,在檔名部分,我們則會用蛇底式命名,例如 home_controller.rbtasks_controller.rb

有了這樣的慣例以後,接下來就是要處理轉換的問題,怎麼樣讓我們的程式碼可以看到 class TasksController,就知道要去尋找 tasks_controller.rb 這個檔案並且載入,要實作這樣的尋找過程,我們會用到比對文字最常使用的正規表示式 Regular expression

# 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
end

這裡我們借用了早期 Rails 處理命名轉換的一段程式碼,對於有些人來說 像是我,正規表示式一直都學不好,這裡我推薦一個蠻不錯的學習網站 regex101

現在我們就一段一段來看

先做 :: 處理,換成 / 做為路徑

# 將 "Service::HomeController" 轉換成 "Service/HomeController"

string.gsub(/::/, '/')

再來以大寫做為分割並加上底線

# 將 "Service/HomeController" 轉換成 "Service/Home_Controller"

.gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').gsub(/([a-z\d])([A-Z])/,'\1_\2')

最後把全部字串換成小寫,並將 - 換成 _

# 將 "Service/Home_Controller" 轉換成 "service/home_controller"

.tr("-", "_").downcase

const_missing 登場

我們有了轉換的方法,接下來就是要做攔截,還記得 const_missing 怎麼用嗎?如果呼叫一個不存在的常數 時,就會觸發 const_missing,換句話說,當觸發時我們要嘗試剛剛所寫的 規則,去試圖找出不存在的 常數
這裡建立另一個新的檔案 dependencies.rb 來處理 const_missing

# mavericks/lib/mavericks/dependencies.rb

class Object
  def self.const_missing(const)
    require Mavericks.to_underscore(const.to_s)
    Object.const_get(const)
  end
end

別忘了要在 lib/mavericks.rb 底下把所寫的程式碼都載入,就如同前幾天的所提到,lib/mavericks.rb 是 Mavericks 的入口,要在這裡將其他寫好的檔案都載入

# mavericks/lib/mavericks.rb

require "mavericks/version"
require "mavericks/routing"
require "mavericks/support"
require "mavericks/dependencies"

module Mavericks
.
.
(略)

回到 just_do

經過一番修改,現在 Mavericks 已經有 autoloading 了,立刻來測試看看吧!

先把原本為了新增 Controller 檔案而加上的 require 都刪掉

# just_do/config/application.rb

require 'mavericks'
$LOAD_PATH << File.join(File.dirname(__FILE__), "..", "app", "controllers")

# 將 controller 的 require 都刪掉
# require 'books_controller'
# require 'home_controller'

module JustDo
  class Application < Mavericks::Application
  end
end

然後執行 bundle exec rackup -p 3001 ,重整一下畫面 http://127.0.0.1:3001/,well..網頁看起來都正常,那我們再隨便新增一個 controller 看看

# just_do/app/controllers/orders_controller.rb

class OrdersController < Mavericks::Controller
  def index
    'This is a Order index page'
  end
end

一樣重啟伺服器,然後連到 http://127.0.0.1:3001/orders/index,就會看到下面畫面

This is a Order index page

酷!我們不用在那麼辛苦一個一個載入 Controller 了

事情沒有那麼簡單

雖然我們已經做出 autoloading,但其實 Rails 處理的狀況還要更多,像是 nested namespaces 的問題,例如 Service::MyController,還有各種複雜的狀況,如果有興趣深入研究的讀者可以參考這篇

另外依照現在 Mavericks 的架構,我們用 const_missing 來捕捉找不到 Class 的狀況,但如果載入的檔案還是沒有那個 Class 呢?我們可以嘗試清空 tasks_controller.rb 這個檔案,然後重新載入一次 http://127.0.0.1:3001/tasks/index

你會發現 server 整個 crash 而關掉

那是因為 Ruby 因為載入了檔案,卻發現還是找不到常數,所以又再 call 一次 const_missing,進入到無限循環的狀態,最後讓 server 當掉,為了避免這個情況發生,我這裡用很簡單的方式做處理

# 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

用一個變數來紀錄有沒有呼叫過 const_missing,最後再重新測試一下,一樣的狀況,這次就會導向 404 的畫面

加快一點開發速度吧 - bundl exec

除了利用 Autoloading 來減少我們手動加入檔案,還有沒有其他的方式可以加快開發速度?其實有的,還記得我們這幾天一直重覆這樣的動作嗎?

$ gem build mavericks.gemspec
$ gem install mavericks-0.1.0.gem

還不包括刪掉舊的 mavericks-0.1.0.gem,然後關閉伺服器,再重新打開,重整瀏覽器畫面...其實我們可以利用 Bundler 內建的功能機制,來改善我們的開發流程,先去 Gemfile 修改一下檔案

# just_do/Gemfile

source 'https://rubygems.org'
# 加上 path
gem 'mavericks', :path => "../mavericks"

接下來利 用bundle exec 來執行 Gemfile 裡面的 gem,透過這樣的方式,我們就不用每次 build 完在 install 到本機上,但別忘了我們要把之前 Mavericks 打包的 mavericks-0.1.0.gem 給刪掉,也要把安裝在本機的 mavericks-0.1.0 也一併 uninstall

$ gem uninstall mavericks

接著之後每次只要修改 Mavericks,只要像下面這樣開啟 Rack 伺服器,就可以直接套用最新版的 Mavericks

$ bundle exec rackup -p 3001

是不是很方便呢?

更像 Rails 一點

雖然我們已經有了 autoloading,也學到 path 的技巧,但現在每次修改 controller 內容,還是要重新啟動伺服器,這樣開發速度還是很慢,為了更像 Rails 一點,我們可以使用 rerun 這個 gem,來幫助我們改善這個問題

# just_do/Gemfile

source 'https://rubygems.org'
gem 'mavericks', :path => "../mavericks"

group :development do
  gem 'rerun'
end

接著在 just_do 目錄底下重新 bundle install,然後利用 rerun 啟動 server

$ bundle install
$ bundle exec rerun -- rackup -p 3001

開啟後會發現 rerun 正在監聽你的檔案,接著隨意更改一個 Controller,然後直接重新整理,就會發現修改結果立刻顯示,再也不用重新開啟 server 了!

只能回傳文字好不方便

不但是我們已經過了一個禮拜了,我說那個 View 呢?應該不會有人想一直回傳字串吧,尤其現在是大前端的時代,不能寫些 HTML 和 CSS 的網站能用嗎?

嗯...好的,明天我們就要進入 MVC 架構裡面的 V 部分,加上 View 以後,網站看起來也會更加豐富


上一篇
[DAY 6] 復刻 Rails - 關於 Autoloading
下一篇
[DAY 8] 復刻 Rails - 這裡的 View 還不錯
系列文
向 Rails 致敬!30天寫一個網頁框架,再拿來做一個 Todo List30

尚未有邦友留言

立即登入留言