昨天我们做了一个 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
完整的程序码明天会一起发布
<<: [2020铁人赛Day26]糊里糊涂Python就上手-Numpy的观念与运用(上)
numpy介绍: 一个可操作高维度阵列的套件,可快速的对整个资料做运算。 就不多说了,让我们直接实际...
元件介绍 Radio 是一个单选框元件。让我们在一组选项当中选择其中一个选项。当我们的情境是希望用户...
Replica选择切换机制 先剔除不健康的Replica Replica与Master失去连线时间,...
169. Majority Element 今天我们一起挑战leetcode第169题Majorit...
滚动组件-Sliver 若想要自定义滚动效果的介面功能,就需要使用 CustomScrollView...