Vài tuần trước, tôi viết về cách RubyGems quản lý load path Ruby. Nhưng Rails không dùng RubyGems trực tiếp – nó dùng Bundler quản lý gem.
Nếu không biết Bundler hoạt động, cách gem được kéo vào Rails có vẻ quá ma thuật. Việc thêm dòng vào Gemfile làm code vào app như thế nào? Bundler, Rails, RubyGems làm việc cùng nhau ra sao để dependency dễ dàng?
Tại sao Bundler?
Tôi coi Bundler là gem manager nghiêm ngặt. Tức là, Bundler giúp cài đúng version gem cần, và ép app chỉ dùng những version đó.
Điều đó rất hữu ích. Để hiểu tại sao, quay về thế giới trước Bundler.
Trước Bundler, vẫn dễ cài đúng version gem với script setup:
gem install rails -v 4.1.0
gem install rake -v 10.3.2
...
(Đó là, miễn sao dependency Rails 4.1 không conflict với dependency Rake 10.3.2!)
Nhưng khi bạn làm vài Rails app khác nhau, mỗi app phụ thuộc version gem khác nhau thì sao? Trừ khi cực cẩn thận, bạn gặp gem activation error kinh khủng:
Gem::Exception: can't activate hpricot (= 0.6.161, runtime), already activated hpricot-0.8.3
Ugh. Thông báo đó vẫn khiến tôi ác mộng. Thường nghĩa là ngày cài/uninstall gem, để chính xác version trên máy. Và chỉ cần gem install rake vô tình để phá hoại mọi kế hoạch cẩn thận.
rvm gemsets giúp một thời gian. Nhưng cần thời gian setup, và nếu cài nhầm gemset, quay lại vấn đề cũ. Với Bundler, bạn hiếm khi phải nghĩ về dependency. App thường chỉ chạy. Và Bundler setup nhanh hơn gemsets.
Vậy Bundler làm hai việc quan trọng. Cài tất cả gem cần, và khóa RubyGems lại, để gem đó là duy nhất bạn có thể require trong Rails app đó.
Rails dùng Bundler như thế nào?
Cốt lõi, Bundler cài và isolate gem. Nhưng không chỉ vậy. Code từ gem trong Gemfile vào Rails app như nào?
Xem bin/rails:
#!/usr/bin/env ruby
begin
load File.expand_path("../spring", __FILE__)
rescue LoadError
end
APP_PATH = File.expand_path('../../config/application', __FILE__)
require_relative '../config/boot'
require 'rails/commands'
Nó load Rails bằng require ../config/boot. Xem file đó:
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
require 'bundler/setup' # Set up gems listed in the Gemfile.
À, là Bundler! (Cũng vừa biết bạn chọn Gemfile khác bằng env BUNDLE_GEMFILE. Hay đấy.)
bundler/setup làm vài việc:
- Xóa tất cả path gem khỏi
$LOAD_PATH(đảo ngược việc RubyGems làm load path). - Sau đó, thêm load path chỉ của gem trong
Gemfile.lockvào$LOAD_PATH.
Bây giờ, gem bạn có thể require chỉ là gem trong Gemfile.
Tất cả gem cần đã trên load path. Nhưng dùng RubyGems một mình, vẫn phải require file cần. Tại sao Rails với Bundler không cần require gem?
Xem nhanh config/application.rb, chạy sau boot Rails:
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)
Lại Bundler! Bundler.require require tất cả gem trong group bạn truyền. (Theo "group", tôi chỉ group trong Gemfile.)
Group nào trong Rails.groups?
# Returns all rails groups for loading based on:
# * The Rails environment;
# * The environment variable RAILS_GROUPS;
# * The optional envs given as argument and the hash with group dependencies;
# groups assets: [:development, :test]
# # Returns
# => [:default, :development, :assets] for Rails.env == "development"
# => [:default, :production] for Rails.env == "production"
def groups(*groups)
hash = groups.extract_options!
env = Rails.env
groups.unshift(:default, env)
groups.concat ENV["RAILS_GROUPS"].to_s.split(",")
groups.concat hash.map { |k, v| k if v.map(&:to_s).include?(env) }
groups.compact!
groups.uniq!
groups
end
À, giải thích được. Rails.groups sẽ là [:default, :development] chạy development, [:default, :production] production, etc.
Vậy Bundler nhìn Gemfile gem thuộc group đó, gọi require trên mỗi gem tìm thấy. Nếu gem nokogiri, nó gọi require "nokogiri" cho bạn. Đó là lý do gem thường just work trong Rails, không code thêm.
Biết công cụ
Nếu hiểu công cụ dùng tốt, dễ làm việc hơn. Vì vậy nếu dùng thứ gì luôn, đáng giá mất vài phút đào sâu.
Nếu làm Ruby và Rails, bạn dùng gem mỗi ngày. Hãy dành thời gian học chúng tốt!