iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 4
0
Modern Web

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

[DAY 4] 復刻 Rails - 從 Controller 開始

說話要建立一個 Controller,就要先從 Controller 在做些什麼開始了解,我們先來看看 Rails 的官方文件怎麼解釋

What Does a Controller Do?
Action Controller is the C in MVC. After the router has determined which controller to use for a request, the controller is responsible for making sense of the request, and producing the appropriate output. Luckily, Action Controller does most of the groundwork for you and uses smart conventions to make this as straightforward as possible.

Rails 在 Router 解析網址以後,將 request 分配到指定的 Controller 和 Action,如果有需要,Controller 會跟 Model 要資料,最後將資料丟到 View 做回應,有了這個基本的概念以後,我們就可以開始今天的實作

想想怎麼處理每一條送進來的 request?

雖然現在 Mavericks 已經有基礎的架構了,但是總不能一直回應 200 吧?而且回應內容也是寫死的,在修改之前我們先來看看 lib/mavericks.rb

# mavericks/lib/mavericks.rb

  class Application
    def call(env)
      [200, {'Content-Type' => 'text/html'},
       ["I use mavericks, just do !!"]]
    end
  end

這裡會看到 call 這個 method return 了一個陣列,裡面依序分別放了

  • 200
  • {'Content-Type' => 'text/html'}
  • "I use mavericks, just do !!"

陣列第一個是 HTTP status code,這裡回應的是 200,也可以依照當下的情況回應其他的數字,例如伺服器出現錯誤就回應 500,找不到頁面就回應404

第二個則是 HTTP headers,裡面可以包含許多東西,像是 cookies 或是陣列裡面所寫的 Content-Type,而 text/html 則是告訴瀏覽器現在這個回應是一個 HTML 文件

陣列的最後一個就是我們的網頁內容,用字串來表示

神秘的 env

還記得我們傳的引數 env 嗎?前面雖然都沒什麼提到他,但他裡面其實包含了非常多的資訊,讓我們修改一下 Mavericks,嘗試在網頁上將 env 印出來

# mavericks/lib/mavericks.rb

    def call(env)
      [200, {'Content-Type' => 'text/html'},
       [env.to_s]]
    end

記得要 rebuild,在重新安裝 mavericks

接著回到 just_do 的專案,執行 rackup -p 3001 啟動 server,打開網頁一看,你會發現許多的訊息,這些訊息代表這次 request 所夾帶的資訊,包括大家比較熟悉的 HTTP_USER_AGENTREQUEST_METHOD

其中你會發現一個叫做 PATH_INFO ,這個東西你可以想像把 URL 扣除掉 hostname 同時也扣除掉 parameters,所剩下來的中間那段網址,例如我們現在所打開的網頁是 http://127.0.0.1:3001/,那 PATH_INFO 就等於 /

建立 mavericks 的 routing 機制

現在我們已經知道如何透過 env 取得 request 的網址,同時我們也了解到 Rails 網址基本概念就像這樣 ontroller/action,那我們就開始實作 Mavericks 的 routing,將每條 request 導向相對應的 Controller 和 Action

首先修改 lib/mavericks.rb

# mavericks/lib/mavericks.rb

require "mavericks/version"
require "mavericks/routing"

module Mavericks
  class Error < StandardError; end

  class Application
    def call(env)
      klass, act =  get_controller_and_action(env)
      controller = klass.new(env)
      text = controller.send(act)
      [200, {'Content-Type' => 'text/html'},
       [text]]
    end
  end

  class Controller
    attr_reader :env

    def initialize(env)
      @env = env
    end
  end
end

要特別注意的是,因為等下我們會新增一個檔案叫 routing,所以我們需要 require 進來,在這段程式碼裡, 有些人而或許會看到一個比較陌生的 method 叫 send

text = controller.send(act)

send 是一個很有趣同時威力又很強大的 method,他可以讓你在程式碼執行前的最後一刻,來決定你要呼叫那個 method,也就是在 Ruby metaprogramming 中,所謂的 Calling Methods Dynamically

舉例來說,一般我們在 call method 會這樣寫,用逗點方式來呼叫

obj.some_method()

但是 Ruby 有另一種 call 法

obj.send(:some_method)

而剛剛說威力強大是因為,在一般在物件導向語言中,我們是無法呼叫到 private method,但在 Ruby 的世界中,你還是可以用 send 呼叫,另外我們還定義了一個 class 叫 Controller,裡面用 instance variableenv 存放起來,這個在之後的文章會在用到

接著我們來建立 routing,這個檔案會將網址解析成我們要的 Controller 和 Action

# mavericks/lib/mavericks/routing.rb

module Mavericks
  class Application
    def get_controller_and_action(env)
      before, cont, action, after = env["PATH_INFO"].split('/', 4)

      cont = cont.capitalize
      cont += "Controller"

      [Object.const_get(cont), action]
    end
  end
end

這裡我們用最簡單的方式,就是用 split 來處理字串,當然這個寫法的問題很多,在後續的文章我們還會討論更多 routing 的機制

將 Controller 取出來以後,把 Controller 字首換成大寫,並且在字尾加上 Controller,所以如果是 Tasks 就會變成 TasksController,最後還記得在 Ruby 世界裡面,Class 也是一個常數嗎?利用這個特性,我們用 const_get 來取得 Class

MVC 的 C 有了!

我們已經改良了 Mavericks,現在回到 just_do 立刻來寫專案裡面第一個 controller,這裡我規劃這個網站是一個任務管理工具,可以新增、刪除、修改任務,所以我們就建立一個 TasksController

# just_do/app/controllers/tasks_controller.rb

class TasksController < Mavericks::Controller
  def index
    "This is a Task's index"
  end
end

迫不及待想存檔測試結果了嗎?休淡幾勒!我們還有地方還沒改完

# just_do/config/application.rb

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

module JustDo
  class Application < Mavericks::Application
  end
end

讀者做到這裡可能已經發現到,我們使用 Mavericks 只要每次修改,都要重新開啟伺服器,我記得 Rails 在開發時只要改完程式碼,按 F5 重整就好了呀,為什麼我們要那麼辛苦?那是因為 Rails 有強大的 autoload 機制,但是我們卻還沒有,之後我們會談到如何實作,所以在這之前,我們先辛苦的手動增加 Controller 檔案到 application.rb

特別注意的是這段程式碼

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

這段程式碼的意思是,將 app/controllers 這個路徑加到 $LOAD_PATH 裡面, 接下來就不需要像下面這樣辛苦的替每個 require 前面加上 app/controllers/

# 只需要這樣
require '檔名'

# 不需要像這樣
require 'app/controllers/檔名'

最後重新開啟伺服器,瀏覽 http://127.0.0.1:3001/tasks/index

This is a Task's index

成功了!

回頭看看 Rails

現在 just_do 的專案結構已經有 app/controllers/,同時底下有一個 Controller,裡面包含 index 的 Action ,瀏覽 http://127.0.0.1:3001/tasks/index 就會出現 action 裡面的所回應的字串,但如果瀏覽 http://127.0.0.1:3001/tasks/show 呢?或是首頁 http://127.0.0.1:3001/?

嗯嗯...看起來我們明天還要繼續努力改善 Mavericks 這個框架


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

尚未有邦友留言

立即登入留言