Ruby [Rails 原理] mini-rails,600 行代码看懂 Rails

xaqi · 2017年04月12日 · 最后由 StephenZzz 回复于 2019年05月09日 · 4537 次阅读

github:https://github.com/xaqi/mini-rails

mini-rails 是由 Rails 源码精简而成,浓缩的都是精华。

mini-rails 实现了从 socket 到 controller 的层层封装,并注释了 Rails 源码中相应模块的位置,可作为学习 Rails 源码的目录或大纲。

看懂这 600 行代码,就不用再去研究 Rails 源码了😆

#mini-rails.rb

require 'socket'
require 'thread'

module WEBrick

  module Utils
    #https://github.com/candlerb/webrick/blob/master/lib/webrick/utils.rb
    def create_listeners
      address='0.0.0.0'
      port=8081
      res = Socket::getaddrinfo(address, port, Socket::AF_UNSPEC, Socket::SOCK_STREAM, 1, Socket::AI_PASSIVE)
      sockets = []
      res.each{|ai|
        puts ("TCPServer.new(#{ai[3]}, #{port})")
        sock = TCPServer.new(ai[3], port)
        sockets << sock
      }
      sockets
    end
    module_function :create_listeners
  end

  #https://github.com/candlerb/webrick/blob/master/lib/webrick/server.rb
  class SimpleServer
    def SimpleServer.start
      yield
    end
  end

  #https://github.com/candlerb/webrick/blob/master/lib/webrick/server.rb
  class GenericServer
    def start
      @listeners = Utils::create_listeners
      SimpleServer.start{
        while true
          svrs = IO.select(@listeners, nil, nil, 2.0)
          if svrs
            svrs[0].each{|svr|
              sock = svr.accept
              sock.sync = true
              start_thread sock if sock
            }
          end
        end
      }
    end
    def run(sock)
      raise 'run() must be provided by user.'
    end
    def start_thread(sock)
      Thread.start{
        begin
          run sock
        ensure
          sock.close
        end
      }
    end
  end

  #https://github.com/candlerb/webrick/blob/master/lib/webrick/httprequest.rb
  class HTTPRequest
    LF="\n"
    def parse(socket=nil)
      @request = socket.gets LF,4096
      puts @request
      @path_info = @request
    end
    def path
      '/'
    end
    def meta_vars
      meta = Hash.new
      meta["PATH_INFO"]         = @path_info
      meta
    end
  end

  #https://github.com/candlerb/webrick/blob/master/lib/webrick/httpresponse.rb
  class HTTPResponse
    attr_reader :header, :body
    def initialize
      @header=Hash.new
      @body=[]
    end
    def send_response(sock)
      sock << "HTTP/1.1 200/OK\r\nContent-type:text/html\r\n\r\n"
      @body.each { |part| sock << part }
    end
  end

  #https://github.com/candlerb/webrick/blob/master/lib/webrick/httpserver.rb
  class HTTPServer < GenericServer
    def initialize
      @mount_tab = Hash.new
    end
    def run(sock)
      req=HTTPRequest.new
      res=HTTPResponse.new
      req.parse sock
      self.service(req, res)
      res.send_response(sock)
      sock.close
    end
    def service(req, res)
      servlet, options = search_servlet req.path
      si = servlet.get_instance self, *options
      si.service req, res
    end
    def search_servlet(path)
      @mount_tab[path]
    end
    def mount(dir,servlet,*options)
      @mount_tab[dir] = [servlet,options]
    end
  end

end

#WEBrick::HTTPServer.new.start

module WEBrick
  module HTTPServlet

    #https://github.com/candlerb/webrick/blob/master/lib/webrick/httpservlet/abstract.rb
    class AbstractServlet
      def initialize(server, *options)
        @server = server
        @options = options
      end
      def self.get_instance(server,*options)
        self.new server,*options
      end
      def service(req, res)
        raise 'service(req, res) must be provided by user.'
      end
    end

  end
end

module Rack
  module Handler

    def self.default
      Rack::Handler::WEBrick
    end

    #https://github.com/rack/rack/blob/master/lib/rack/handler/webrick.rb
    class WEBrick < ::WEBrick::HTTPServlet::AbstractServlet
      def self.run(app)
        @server = ::WEBrick::HTTPServer.new
        @server.mount '/',Rack::Handler::WEBrick, app
        @server.start
      end
      def initialize(server, app)
        super server
        @app = app
      end
      def service(req, res)
        env = req.meta_vars
        status, headers, body = @app.call(env)
        body.each { |part| res.body << part }
      end
    end

  end

  #https://github.com/rack/rack/blob/master/lib/rack/server.rb
  class Server
    def self.start
      new.start
    end
    def start &blk
      server.run wrapped_app, &blk
    end
    def server
      @_server = Rack::Handler.default
    end
    def build_app_and_options_from_config
      app = Rack::Builder.parse_file 'config.ru'
    end
    def wrapped_app
      @wrapped_app ||= build_app app
    end
    def app
      @app ||= build_app_and_options_from_config
    end
    def build_app(app)
      middleware.reverse_each do |middleware|
        middleware = middleware.call(self)
        klass, *args = middleware
        app = klass.new(app, *args)
      end
      app
    end
    def middleware
      self.class.middleware
    end
  end

  #https://github.com/rack/rack/blob/master/lib/rack/builder.rb
  class Builder
    def self.parse_file(config)
      cfgfile='Rails.application' #read config.ru
      app=new_from_string cfgfile
      app
    end
    def self.new_from_string(builder_script)
      app = eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app"
    end
    def initialize(default_app = nil,&block)
      @run =  default_app
      @run = instance_eval(&block) if block_given?
    end
    def to_app
      app = @run
      app
    end
  end

  class Request
    module Env
      attr_reader :env
      def initialize(env)
        @env=env
        super()
      end
    end
  end
end

#Rack::Server.start
module Rails

  #https://github.com/rails/rails/blob/master/railties/lib/rails/commands/server/server_command.rb
  class Server < ::Rack::Server
    def initialize
      super
    end
    def start
      super
    end
    def middleware
      Hash.new([])
    end
  end

  module Command

    #https://github.com/rails/rails/blob/56b3849316b9c4cf4423ef8de30cbdc1b7e0f7af/railties/lib/rails/command/actions.rb
    module Actions
    end
    class Base
      include Actions
    end

    #https://github.com/rails/rails/blob/master/railties/lib/rails/commands/server/server_command.rb
    class ServerCommand < Base
      def perform
        #require APP_PATH
        Rails::Server.new.tap do |server|
          server.start
        end
      end
    end

    #https://github.com/rails/rails/blob/master/railties/lib/rails/commands/application/application_command.rb
    class ApplicationCommand < Base
      def perform(*args)
        Rails::Command::ServerCommand.new.perform
      end
    end

    #https://github.com/rails/rails/blob/master/railties/lib/rails/command.rb
    class << self
      def invoke(namespace,args)
        command=find_by_namespace namespace
        command.perform namespace,args
      end
      def find_by_namespace(namespace)
        case namespace
          when :application
            Rails::Command::ApplicationCommand.new
        end
      end
    end

  end

  module AppLoader
    extend self
    def exec_app
      Object.const_set(:APP_PATH, File.expand_path("config/application", Dir.pwd))
    end
  end
end




module Rails

  class << self
    #https://github.com/rails/rails/blob/master/railties/lib/rails.rb#L36
    @application = @app_class = nil
    attr_writer :application
    attr_accessor :app_class
    def application
      @application ||= (app_class.instance if app_class)
    end
  end

  #https://github.com/rails/rails/blob/master/railties/lib/rails/engine.rb
  class Engine
    def call(env)
      req = build_request env
      app.call req.env
    end
    def app
      @app ||= begin
        stack = default_middleware_stack
        config.middleware = config.middleware.merge_into stack
        config.middleware.build endpoint
      end
      #@app = Proc.new{|*args| ['200',[],["hello from rails engine."]] }
    end
    class <<self
      def endpoint
        nil
      end
    end
    def endpoint
      self.class.endpoint || routes
    end
    def routes
      @routes ||= ActionDispatch::Routing::RouteSet.new_with_config(config)
      @routes
    end
    def self.instance
      new
    end
    def build_request(env)
      req = ActionDispatch::Request.new env
      req
    end
    def config
      @config ||= Engine::Configuration.new
    end
    def default_middleware_stack
      ActionDispatch::MiddlewareStack.new
    end

    class Configuration #< ::Rails::Railtie::Configuration
      attr_accessor :middleware
      def initialize
        @middleware = Rails::Configuration::MiddlewareStackProxy.new
      end

    end
  end

  module Configuration
    class MiddlewareStackProxy
      def merge_into(other)
        other
      end
    end
  end

  #https://github.com/rails/rails/blob/master/railties/lib/rails/application.rb
  class Application < Engine
    class << self
      def inherited(base)
        Rails.app_class = base
      end
    end
    def default_middleware_stack
      default_stack = DefaultMiddlewareStack.new
      default_stack.build_stack
    end

    #https://github.com/rails/rails/blob/master/railties/lib/rails/application/default_middleware_stack.rb
    class DefaultMiddlewareStack
      def build_stack
        ActionDispatch::MiddlewareStack.new.tap do |middleware|
          middleware.use ::ActionDispatch::Executor #, app.executor
        end
      end
    end
  end
  #can't find where the Application.inherited was called, temporary hard code here
  Application.inherited(Application)
end


module ActionDispatch
  #https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/http/parameters.rb
  module Http
    module Parameters
      attr_accessor :path_parameters
      def path_parameters
        m=/\/([^\/]+)(?:\/?([^\/]+))?\/?\s/.match env["PATH_INFO"].strip
        {:action=>$2 || 'index',:controller=>$1 || 'Home'}
      end
    end
  end

  #https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/http/request.rb
  class Request
    include Rack::Request::Env
    include ActionDispatch::Http::Parameters
    attr_accessor :controller_instance, :path_info
    def initialize(env)
      super
    end

    def controller_class
      params = path_parameters
      controller_param = params[:controller]
      params[:action] ||= "index"
      const_name = "#{controller_param}Controller"
      #ActiveSupport::Dependencies.constantize(const_name)
      eval const_name
    end
  end
  #https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/http/response.rb
  class Response
    attr_accessor :request
    def self.create(status = 200, header = {}, body = [])
      new status, header, body
    end
    def initialize(status = 200, header = {}, body = [])
    end
  end

  # https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/stack.rb
  class MiddlewareStack
    class Middleware
      attr_reader :klass
      def initialize(klass,args,block)
        @klass=klass
      end
      def build(app,*args,&block)
        klass.new(app, *args, &block)
      end
    end

    attr_accessor :middlewares
    def initialize
      @middlewares= []
    end
    def use(klass, *args, &block)
      middlewares.push(build_middleware(klass, args, block))
    end
    def build_middleware(klass, args, block)
      Middleware.new(klass, args, block)
    end
    def build(app = Proc.new)
      #return Proc.new{|*args| ['200',[],["hello from rails MiddlewareStack."]] }
      builds=middlewares.freeze.reverse.inject(app) { |a, e| e.build(a) }
      builds
    end
  end

  #https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/executor.rb
  class Executor
    def initialize(app, executor=nil)
      @app, @executor = app, executor
    end
    def call(env)
      #return ['200',[],["hello from Executor."] ]
      #state = @executor.run! if @executor
      response = @app.call(env)
    end
  end

  module Routing
    #https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/routing/endpoint.rb
    class Endpoint
      def app;           self;  end
    end

    #https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/routing/route_set.rb
    class RouteSet
      class Dispatcher < Routing::Endpoint
        def serve(req)
          #return ['200',[],["hello from RouteSet Dispatcher."] ]
          begin
            params     = req.path_parameters
            controller = controller req
            res        = controller.make_response! req
            dispatch(controller, params[:action], req, res)
          rescue
            ['200',[],["404"] ]
          end
        end
        def dispatch(controller, action, req, res)
          controller.dispatch(action, req, res)
        end
        def controller(req)
          req.controller_class
        end
      end

      Config = Struct.new :relative_url_root, :api_only
      DEFAULT_CONFIG = Config.new(nil, false)
      def self.new_with_config(config)
        route_set_config = DEFAULT_CONFIG
        new route_set_config
      end
      def initialize(config = DEFAULT_CONFIG)
        @set    = Journey::Routes.new
        @router = Journey::Router.new @set
        #Routes should be set by route mapping registers, hard code here for simplicity
        @set.instance_eval{ @routes << Dispatcher.new }
      end
      def call(env)
        req = make_request(env)
        req.path_info = Journey::Router::Utils.normalize_path(req.path_info)
        @router.serve(req)
      end
      def request_class
        ActionDispatch::Request
      end
      def make_request(env)
        request_class.new env
      end
    end
  end

  module Journey
    #https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/journey/router.rb
    class Router
      attr_accessor :routes
      def initialize(routes)
        @routes = routes
      end
      def serve(req)
        #return ['200',[],["hello from Router."] ]
        find_routes(req).each do |match, parameters, route|
          status, headers, body = route.app.serve(req)
          return [status, headers, body]
        end
      end
      def find_routes(req)
        routes.map{|x|[nil,nil,x]}
      end
      class Utils
        def self.normalize_path(path)
          path = "/#{path}"
          path.squeeze!("/".freeze)
          path.sub!(%r{/+\Z}, "".freeze)
          path.gsub!(/(%[a-f0-9]{2})/) { $1.upcase }
          path = "/" if path == "".freeze
          path
        end
      end
    end

    #https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/journey/routes.rb
    class Routes
      include Enumerable
      attr_reader :routes
      def initialize
        @routes             = []
      end
      def each(&block)
        routes.each(&block)
      end
    end
  end
end

#https://github.com/rails/rails/blob/master/actionpack/lib/abstract_controller/base.rb
module AbstractController
  class Base
    attr_accessor :action_name
    def process(action, *args)
      @action_name = action.to_s
      process_action(action_name, *args)
    end
    def process_action(method_name, *args)
      send_action(method_name, *args)
    end
    alias send_action send
  end
end
module ActionController
  class MiddlewareStack < ActionDispatch::MiddlewareStack
  end

  #https://github.com/rails/rails/blob/master/actionpack/lib/action_controller/metal.rb
  class Metal < AbstractController::Base
    def initialize
      @_request = nil
      @_response = nil
      @_routes = nil
      super
    end
    def dispatch(name,request,response)
      #return ['200',[],["hello from rails Metal Controller."]]
      set_request!(request)
      set_response!(response)
      process(name)
    end
    def self.dispatch(name, req, res)
      new.dispatch name,req,res
    end
    def set_response!(response) # :nodoc:
      @_response = response
    end
    def set_request!(request) #:nodoc:
      @_request = request
      @_request.controller_instance = self
    end
    def self.make_response!(request)
      ActionDispatch::Response.create.tap do |res| res.request= request end
    end
    def self.call(env)
      req = ActionDispatch::Request.new env
      action(req.path_parameters[:action]).call(env)
    end
    def self.action(name)
      lambda { |env|
        req = ActionDispatch::Request.new(env)
        res = make_response! req
        new.dispatch(name, req, res)
      }
    end
  end
  #https://github.com/rails/rails/blob/master/actionpack/lib/action_co
  class Base < Metal
  end

end


def start_server
  Rails::AppLoader.exec_app
  Rails::Command.invoke :application, ARGV
end

#test.rb

require_relative 'mini-rails'

class HomeController < ActionController::Base
  def index
    ['200',[],["hello from Home Index."]]
  end
end

class UserController < ActionController::Base
  def about
    ['200',[],["hello from User About."]]
  end
end

start_server

你也是讀了 Rebuilding Rails 嗎 XD https://rebuilding-rails.com/

lulalala 回复

没有,只看了 Rails 源码。

👍 这个学习起来确实方便。

5 楼 已删除

好啊,求 pr😆

需要 登录 后方可回复, 如果你还没有账号请 注册新账号