昨天我們做了一個 MiniSinatra
來實作 routing
的部分,今天我們將會運用一樣的方式在我們的 Mavericks 加上這個功能,用 DSL
來寫 routing
,另外接下來這兩天的文章內容會有點複雜,如果有寫不清楚的地方或是錯誤的地方,再麻煩大家在底下留言告訴我
我們先來回想一下 Rails routing 的寫法
Rails.application.routes.draw do
# For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
root 'tasks#index'
resources :tasks
end
在之前我們已經實作 Rails.application
,接下來我們要實作 routes
這個 instance
,會用 draw
來實作加入規則的部分,就如同昨天的 add_route
另外我們發現一件事情是,如果我們在 Rails 專案裡面執行 rake middleware,會發現 routing 也是 Middleware 的其中一員(詳情參考官網)
.
.
(略)
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ContentSecurityPolicy::Middleware
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use Rack::TempfileReaper
run MyApp::Application.routes
知道這些東西後,我們就知道實作方法,首先第一步,我們要先為 just_do 加上 config/routes
Mavericks.application.routes.draw do
root to: 'tasks#index'
resources :tasks
end
我們替 just_do 加上一個 root
設為我們的首頁,並且加上 task
的 resources
,一個 Rails 很常見的 routing 規則,接著就回到 Maverciks 來實作這部分
看一下原先 application.rb 的 call
method 寫法,我們在 Rack 一執行應用程式時,就把規則寫死在 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
在之前將網址用很簡單的方式來做判斷,直接預設網址就是 contoller/action
的格式,並且用簡單的字串分割來取出 Controller 和 Action,現在我們要捨棄這樣的做法,改讓開發者可以自己自訂 routing
所以先把 application.rb 做重構
# mavericks/lib/mavericks/application.rb
module Mavericks
class Error < StandardError; end
class Application
def default_middleware_stack
Rack::Builder.new
end
def app
@app ||= begin
stack = default_middleware_stack
stack.run routes
stack.to_app
end
end
def routes
@routes ||= ActionDispatch::Routing::RouteSet.new
end
def call(env)
app.call(env)
end
def self.inherited(klass)
super
@instance = klass.new
end
def self.instance
@instance
end
def initialize!
config_environment_path = caller.first
@root = Pathname.new(File.expand_path("../..", config_environment_path))
raw = @root.join('config/database.yml').read
database_config = YAML.safe_load(raw)
database_adapter = database_config['default']['adapter']
database_name = database_config[Mavericks.env]['database']
ActiveRecord::Base.establish_connection(database_adapter: database_adapter, database_name: database_name)
ActiveSupport::Dependencies.autoload_paths = Dir["#{@root}/app/*"]
load @root.join('config/routes.rb')
end
def root
@root
end
end
end
連同底下原本的 index
和 default_render
那些全部刪掉,讓 Application class 只做自己該做的事情,刪完後突然覺得乾淨許多
其中我們建立了幾個新的 method,第一個是 routes
,routes
這個 method 呼叫時會回傳一個 instance 讓開發者加入 routing 規則,也就是讓 just_do 在 config/routes.rb 所呼叫的物件
def routes
@routes ||= ActionDispatch::Routing::RouteSet.new
end
另外我們用 Rack::Builder
來實作了簡單的 Middleware Stack
def default_middleware_stack
Rack::Builder.new
end
def app
@app ||= begin
stack = default_middleware_stack
stack.run routes
stack.to_app
end
end
stack.run routes
會將我們所寫的規則加入到 Middleware,讓每一次的 reuqest 都會做 routing 的處理
另外我們也必須在 initialize!
裡面把 config/routes.rb
一起 load 進來,跟資料庫設定是一樣的道理
def initialize!
# .
# .
# (略)
load @root.join('config/routes.rb')
end
修改完後,我們就要來處理 ActionDispatch
的部分
接下來我們就來實作 route_set 的部分,先新增 action_dispatch/routing/route_set.rb
並且加入以下程式碼
module ActionDispatch
module Routing
class Route
attr_accessor :method, :path, :controller, :action, :name
def initialize(method, path, controller, action, name)
@method = method
@path = path
@controller = controller
@action = action
@name = name
end
def match?(request)
request.request_method == method && request.path_info == path
end
def controller_class
"#{controller.classify}Controller".constantize
end
def dispatch(request)
controller = controller_class.new
controller.request = request
controller.response = Rack::Response.new
controller.process(action)
controller.default_render(action) unless controller.content
controller.render_layout
controller.response.finish
end
end
class RouteSet
def initialize
@routes = []
end
def add_route(*args)
route = Route.new(*args)
@routes << route
route
end
def find_route(request)
@routes.detect { |route| route.match?(request) }
end
def draw(&block)
mapper = Mapper.new(self)
mapper.instance_eval(&block)
end
def call(env)
request = Rack::Request.new(env)
if route = find_route(request)
route.dispatch(request)
else
[404, {'Content-Type' => 'text/plain'}, ['Not found page']]
end
end
end
end
end
這裡我實作了兩個 class,一個是 RouteSet
,另一個是 Route
,RouteSet
主要做的事情就是接到 env
的參數後,去分析要做什麼樣的事情,剛剛提到因為 RouteSet
也是 Middleware 的一部分,所以一樣要建立 call
method 來處理 request
def call(env)
request = Rack::Request.new(env)
if route = find_route(request)
route.dispatch(request)
else
[404, {'Content-Type' => 'text/plain'}, ['Not found page']]
end
end
我們用之前提到過的 Rack::Request
來包裝處理傳進來的 env
,將 request 傳遞給 find_route
做尋找判斷有沒有對應的 routing 規則,有的話就執行 Controller 的 Action,也就是 route.dispatch(request)
,如果沒有在規則裡面就判斷為 404
找不到頁面
Route 我們可以稱做為一個個的 Routing「規則」,可以看到在裡面放了這個規則的
HTTP Methods
request 的路徑,也就是 env['PATH_INFO']
執行那個 Controller
執行那個 Action
替規則命名,可以建立 url helper,例如 root_path
所以如果是這樣寫
root to: 'tasks#index'
那對應的值就是
GET
/
tasks
index
root
match?
昨天有提到過就是用來比對 request 與規則是不是符合
def match?(request)
request.request_method == method && request.path_info == path
end
而整個規則真正執行 Controller 和 Action 部分就寫在這裡
def controller_class
"#{controller.classify}Controller".constantize
end
def dispatch(request)
controller = controller_class.new
controller.request = request
controller.response = Rack::Response.new
controller.process(action)
controller.default_render(action) unless controller.content
controller.render_layout
controller.response.finish
end
我們需要有一個 method 來將傳進來的字串 controller
,轉換成常數也就是 Controller 的 class,另外也需要在 ActionSupport 裡面的 String 加上新的擴充來處理這段轉換
# mavericks/lib/active_support/string.rb
class String
# .
# .
# (略)
def classify
self.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
end
def constantize
names = self.split('::')
names.shift if names.empty? || names.first.empty?
constant = Object
names.each do |name|
constant = constant.const_get(name, false) || constant.const_missing(name)
end
constant
end
end
這裡我們直接借用 Rails 的處理方式,做法是將 tasks
轉成像是 Tasks
,最後再利用 const_get
和 const_missing
來尋找 class,細節部分就不太多的說明
而最後執行 dispatch
的部分,我們會 new
一個 Rack::Response
並且做 render 最後回傳 response
今天我們聚集在執行 Routing 的部分,明天我們會繼續來實作定義規則的部分,也就是將 DSL 轉換成定義規則
def draw(&block)
mapper = Mapper.new(self)
mapper.instance_eval(&block)
end
完整的程式碼明天會一起發布