iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 22
0
Modern Web

向 Rails 致敬!30天寫一個網頁框架,再拿來做一個 Todo List系列 第 22

[DAY 22]復刻 Rails - Application 啟動過程

關於 Rails web 應用程式的啟動過程,對於初學者來說並不會是首要目標,大多數還是會先從 MVC 架構開始學起,但如果學習到一定的程度,想要更深入了解 Rails 在幹嘛的話,閱讀 Rails 的核心程式碼,也就是 Railties 是不可少的過程

Railties 簡單來說,就是在做應用程式的初始化,並且把所有相關的套件,例如 Active Record 給載入組合在一起,以我們目前所做的 Mavericks 來說,應用程式初始化要做的事情就是設定 autoload 路徑和資料庫的設定,在實作 Mavericks 啟動流程之前,我們可以先來看 Rails 是怎麼處理的

之前我們提過啟動點,在 config.ru 這個檔案裡面

# config.ru

# This file is used by Rack-based servers to start the application.

require_relative 'config/environment'

run Rails.application

這個檔案做了兩件事情,一個是載入 config/environment.rb,另一個是利用 Rack 提供的 run 來執行 Rails.application,相信如果有跟著一起做到今天,這邊大家應該都不陌生,

接著來看看 config/environment.rb 這支檔案,

# config/environment

# Load the Rails application.
require_relative 'application'

# Initialize the Rails application.
Rails.application.initialize!

一樣也是做了兩件事情,一個是載入 application.rb 這支檔案,並且執行 Rails.application.initialize!,仔細看 Rails 有很貼心的在上面做註解

接著繼續看 application.rb 裡面寫了些什麼

require_relative 'boot'

require 'rails/all'

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module Blog
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    # config.load_defaults 6.0

    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration can go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded after loading
    # the framework and any gems in your application.
  end
end

總共 require 兩個部分,一個是 boot.rb,裡面是關於 gem 的載入相關設定,這裡先略過,另一個是載入 rails/all,如果你去翻原始碼,會發現 Rails 在這裡載入所有相關的套件,還記得在鐵人賽一開始有提過,Rails 在做的事情,就是將許多套件整合在一起,而 Rails 真正的核心是 railties(再次強調)

所以對於 Maverciks 來說,我們第一步也需要做差不多的事情,建立一個檔案來負責載入所以的套件,首先建立一個 all.rb

# mavericks/lib/mavericks/all.rb

require 'yaml'
require "mavericks"
require "active_support"
require "active_record"
require "mavericks/routing"
require "mavericks/controller"

接著在 just_do application.rb 那邊也需要修改一下

# 原先是 require 'mavericks'
require 'mavericks/all'

ActiveRecord::Base.establish_connection
ActiveSupport::Dependencies.autoload_paths = Dir["./app/*"]

module JustDo
  class Application < Mavericks::Application
  end
end

重新跑一次伺服器打開網頁,一切應該會運作正常

Application 初始化設定

做完載入套件後,接下來就是將套件組合在一起使用,另外就是要做初始化設定,目前我們有兩個需要做處理

# 資料庫設定
ActiveRecord::Base.establish_connection

# autoload 設定
ActiveSupport::Dependencies.autoload_paths = Dir["./app/*"]

我們需要有個地方來做這些設定,目前的做法是暫時放在 config/application.rb,但實作上應該包裝在一個 method 裡面,剛剛有提到 Rails 的做法是在這裡執行

Rails.application.initialize!

那我們一樣就來實作吧,我們需要建立一個 Application 的 Class,這部分在系列文章一開始的時候,我們是寫在 mavericks.rb 裡面,現在我們要搬移出來

建立一個 Application.rb,並將原先的 mavericks.rb 裡面的程式碼搬移過來

# mavericks/lib/mavericks/application.rb

module Mavericks
  class Error < StandardError; end

  class Application
    # .
    # .
    # (略)
  end
end

然後將 mavericks.rb 做修正,做法跟之前 Active Record 一樣

# mavericks/lib/mavericks.rb

module Mavericks
  autoload :Application, 'mavericks/application'
  
  def self.application
    Application.instance
  end
  
  def self.root
    application.root
  end
end

這裡做了一個 class method 回傳 Application.instance,這個 instance 其實就是利用 Mavericks 來生成的專案 application,也就是 just_do 這個應用程式,為了能做到這樣的效果,我們會用到 inherited 這個 Hook method 來取得子類別,並且用這個子類別 new 一個物件

有點複雜?直接來看程式碼

# mavericks/lib/mavericks/application.rb

module Mavericks
  class Error < StandardError; end

  class Application
    # .
    # .
    # (略)
    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['development']['database']
      ActiveRecord::Base.establish_connection(database_adapter: database_adapter, database_name: database_name)
      ActiveSupport::Dependencies.autoload_paths = Dir["#{@root}/app/*"]
    end


    def root
      @root
    end
    # .
    # .
    # (略)
  end
end

我們如果要透過 Mavericks::Application 來建立 繼承的子類別物件,就可以透過 inherited 這個 Hook method 來知道是誰繼承了 Mavericks::Application(這邊繼承的子類別就是 JustDo::Application),也就是 method 裡面傳的參數 klass

也就是說

@instance = klass.new

實際上就等於

@instance = JustDo::Application.new

接著在 initialize! 這個 method 我們做了一些有趣的事情,像是我們利用 Ruby 的 caller 來取得檔案的位置,caller 會回傳一個陣列裡面包含呼叫這個 method 所經過的檔案路徑,這裡的 caller.first 會回傳 environment.rb 的路徑位置,我們也知道 environment.rb 位於專案目錄底下的 just_do/config/environment.rb,所以往上兩層就是這個專案的根目錄

# 往上兩層就是專案的根目錄
@root = Pathname.new(File.expand_path("../..", config_environment_path))

有了根目錄的位置以後,要取得其他檔案就相對容易許多

# 透過 @root 知道 database.yml 位置
raw = @root.join('config/database.yml').read

# 透過 @root 可以 autoload 所需要的檔案,例如: controller
ActiveSupport::Dependencies.autoload_paths = Dir["#{@root}/app/*"]

另外我們已經將解析 database.yml 搬到 Application.initialize! 實作,所以原先的active_record/base.rb 也要修改一下,來減少耦合

# mavericks/lib/active_record/base.rb

    def self.establish_connection(options)
      database_name = options[:database_name]
      case options[:database_adapter]
      when 'postgresql'
        @@connection = ConnectionAdapter::PostgreSQLAdapter.new(database_name)
      when 'sqlite'
        @@connection = ConnectionAdapter::SQLiteAdapter.new(database_name)
      end
    end

關於資料庫開發環境切換

現在我們已經可以利用 Mavericks.root 來取得專案的根目錄,也可以用 Mavericks.application 來取得應用程式相關資訊,但我們希望可以做到像 Rails 一樣,透過 Rails.env 來設定環境變數,這樣我們就可以隨著環境不同,改變應用程式的資料庫,做法其實很簡單

# mavericks/lib/mavericks.rb

module Mavericks
  autoload :Application, 'mavericks/application'

  def self.application
    Application.instance
  end

  def self.root
    application.root
  end

  def self.env
    ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
  end
end

我們這裡加上一個 class method 叫 env,接著預設給 development,接著修改一下 application.rbinitialize!

    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/*"]
    end

回到 just_do

我們回到 just_do 修改一下專案的程式碼,對於 Mavericks 來說,啟動的順序會是這樣

# just_do/config.ru

# 改為先呼叫 config/environment 做初始化設定
require_relative 'config/environment'

# 改為呼叫 Mavericks.application
run Mavericks.application
# just_do/config/environment.rb
# 
# Load the Mavericks application.
require_relative 'application'

# Initialize the Mavericks application.
Mavericks.application.initialize!
# just_do/config/application.rb

require 'mavericks/all'

module JustDo
  class Application < Mavericks::Application
  end
end

啟動伺服器跑跑看,如果沒有錯誤的話代表成功了!幾乎跟 Rails 一模一樣

Mavericks 程式碼
https://github.com/apayu/mavericks


上一篇
[DAY 21] 復刻 Rails - 用 Rails 的方式整理程式碼 Active Support
下一篇
[DAY 23] 復刻 Rails - 用 Rails 的方式整理程式碼 ActionController
系列文
向 Rails 致敬!30天寫一個網頁框架,再拿來做一個 Todo List30

尚未有邦友留言

立即登入留言