[DAY 27] 复刻 Rails - Routing 威力加强版 - 1

昨天我们做了一个 MiniSinatra 来实作 routing的部分,今天我们将会运用一样的方式在我们的 Mavericks 加上这个功能,用 DSL 来写 routing,另外接下来这两天的文章内容会有点复杂,如果有写不清楚的地方或是错误的地方,再麻烦大家在底下留言告诉我

了解 config/routes.rb

我们先来回想一下 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 设为我们的首页,并且加上 taskresources,一个 Rails 很常见的 routing 规则,接着就回到 Maverciks 来实作这部分

修改一下 application.rb

看一下原先 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

连同底下原本的 indexdefault_render 那些全部删掉,让 Application class 只做自己该做的事情,删完後突然觉得乾净许多

其中我们建立了几个新的 method,第一个是 routesroutes 这个 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 的部分

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

RouteSet

这里我实作了两个 class,一个是 RouteSet,另一个是 RouteRouteSet 主要做的事情就是接到 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

Route 我们可以称做为一个个的 Routing「规则」,可以看到在里面放了这个规则的

  • method: HTTP Methods
  • path: request 的路径,也就是 env['PATH_INFO']
  • controller: 执行那个 Controller
  • action: 执行那个 Action
  • name: 替规则命名,可以建立 url helper,例如 root_path

所以如果是这样写

root to: 'tasks#index'

那对应的值就是

  • method: GET
  • path: /
  • controller: tasks
  • action: index
  • name: 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_getconst_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的观念与运用(上)

>>:  用自己方式存在的工程师 - TonyQ [中]

Day2 - numpy(1)基本介绍及使用

numpy介绍: 一个可操作高维度阵列的套件,可快速的对整个资料做运算。 就不多说了,让我们直接实际...

【Day03】数据输入元件 - Radio

元件介绍 Radio 是一个单选框元件。让我们在一组选项当中选择其中一个选项。当我们的情境是希望用户...

Day25 Redis架构实战-Sentinel选取Replica机制

Replica选择切换机制 先剔除不健康的Replica Replica与Master失去连线时间,...

Leetcode 挑战 Day 14 [169. Majority Element]

169. Majority Element 今天我们一起挑战leetcode第169题Majorit...

Flutter体验 Day 16-滚动组件-Sliver

滚动组件-Sliver 若想要自定义滚动效果的介面功能,就需要使用 CustomScrollView...