Rails 源码解读(一)rails console

rails console 命令时我们经常使用的,它可以让我们通过命令行很方便的与 Rails 应用进行交互,类似于 irb, 其实,rails console 的底层就是用 irb 来实现的,让我们来扒一扒相关的源码。

#rails/railties/lib/rails/commands.rb
ARGV << '--help' if ARGV.empty?

aliases = {
  "g"  => "generate",
  "d"  => "destroy",
  "c"  => "console",
  "s"  => "server",
  "db" => "dbconsole",
  "r"  => "runner"
}

help_message = <<-EOT
Usage: rails COMMAND [ARGS]

The most common rails commands are:
 generate    Generate new code (short-cut alias: "g")
 console     Start the Rails console (short-cut alias: "c")
 server      Start the Rails server (short-cut alias: "s")
 dbconsole   Start a console for the database specified in config/database.yml
             (short-cut alias: "db")
 new         Create a new Rails application. "rails new my_app" creates a
             new application called MyApp in "./my_app"
......
EOT

command = ARGV.shift
command = aliases[command] || command

case command
when 'plugin'
  require "rails/commands/plugin"
when 'generate', 'destroy'
  ...
when 'console'
  ...

ARGV 是一个数组,它包含了从标准输出传来的命令行参数。例如在命令行输入以下命令的时候:

rails console --sandbox

ARGV 的值是: ["console", "--sandbox"]

当 ARGV 为空的时候,即 Rails 命令后面不加任何参数,程序会将 --help 参数自动添加到ARGV 数组中。

command = ARGV.shift
command = aliases[command] || command

ARGV 中的第一个元素被弹出,并传递给 command 变量,case 语句根据 command 的值选择相应的分支。

when 'console'
  require 'rails/commands/console'
  options = Rails::Console.parse_arguments(ARGV)

  # RAILS_ENV needs to be set before config/application is required
  ENV['RAILS_ENV'] = options[:environment] if options[:environment]

  # shift ARGV so IRB doesn't freak
  ARGV.shift if ARGV.first && ARGV.first[0] != '-'

  require APP_PATH
  Rails.application.require_environment!
  Rails::Console.start(Rails.application, options)

进入 console 分支后,此时的 ARGV 数组中只剩下一个元素 --sandbox, 因为元素 “console” 已经在之前被弹出,接着 Rails::Console 类中的 parse_arguments 方法会去解析 ARGV:

#rails/railties/lib/rails/commands/console.rb
require 'optparse'
...
 OptionParser.new do |opt|
  opt.banner = "Usage: rails console [environment] [options]"
  opt.on('-s', '--sandbox', 'Rollback database modifications on exit.') { |v| options[:sandbox] = v }
  opt.on("-e", "--environment=name", String,
          "Specifies the environment to run this console under (test/development/production).",
          "Default: development") { |v| options[:environment] = v.strip }
  opt.on("--debugger", 'Enable the debugger.') { |v| options[:debugger] = v }
  opt.parse!(arguments)
end

if arguments.first && arguments.first[0] != '-'
  env = arguments.first
  if available_environments.include? env
    options[:environment] = env
  else
    options[:environment] = %w(production development test).detect {|e| e =~ /^#{env}/} || env
  end
end

def available_environments
  Dir['config/environments/*.rb'].map { |fname| File.basename(fname, '.*') }
end

通过 ruby 开发命令行工具,optparse 是个非常好的选择,大多数的 ruby 命令行工具的开发都会使用到它。从上面的代码中我们可以看出:

  • --sandbox 表明任何对于数据库的修改操作在退出的时候都会被回滚;
  • rails console 的环境有三种:test/development/production,默认环境是 development

如果第一个参数包含了 ‘-’,则直接将参数返回,否则就说明该参数是用于指定环境的,rails 提供的环境可以在 config/environments 目录中配置,并不仅仅限于 test/development/production 这三种,但如果没有指定自己特有的环境,则在默认的 test/development/production 中选择。

ARGV.shift if ARGV.first && ARGV.first[0] != '-'

在参数被解析完毕之后,如果 ARGV 中包含 ‘-’,则需要把参数弹出,避免将其继续传递给 irb,因为 irb 并不需要也不理解该参数。接下来 就要准备启动 rails console 了。

#rails/railties/lib/rails/commands/console.rb
module Rails
  class Console
    class << self
      def start(*args)
        new(*args).start #初始化操作
      end
    ......
    end

    def initialize(app, options={})
      @app     = app
      @options = options

      app.sandbox = sandbox?
      app.load_console

      @console = app.config.console || IRB
    end
    ......
    def start
      require_debugger if debugger?
      set_environment! if environment?

      #进入rails console后的提示信息
      if sandbox?
        puts "Loading #{Rails.env} environment in sandbox (Rails #{Rails.version})"
        puts "Any modifications you make will be rolled back on exit"
      else
        puts "Loading #{Rails.env} environment (Rails #{Rails.version})"
      end

      if defined?(console::ExtendCommandBundle)
        console::ExtendCommandBundle.send :include, Rails::ConsoleMethods
      end
      console.start  #启动irb
    end
    ......

@console = app.config.console || IRB

该行代码表明了 rails conlose 的底层实现就是通过 irb 完成的。

console.start 相当于 IRB.satrt

comments powered by Disqus