什么是 Service 对象? 什么时候使用?

经常在网上看到有人在讨论 Rails Service 对象模型。但是在 Rails 的 guide 里面并未提及这个对象的定义。所以看起来貌似很模糊,到底什 么是 service 对象呢? 什么时候要用呢?

什么是 Service

一般是来说 service 里面包含系统互动的相关处理。简单来说,service 里面一般都是 包含不止一个 model。

例如: 我们包含一个 User 的 Model,这个 User 包含密码。如果用户忘记密码,我们需要 发送一封 email 让他重置密码。这个功能就是 service。

关于重置密码也有可能有其他需求,例如系统管理员可以帮助重置密码。

在上面的例子里面,把 service 放到 MVC 世界解释如下:

  • Model (User) - 一个用户有一个密码。必须不能为空并且长度至少 8 位。这个模型里面并没有 关于各种情况重置密码的处理。
  • View - 忘记密码页面的 form 和成功失败状态的显示
  • Controller - form 提交之后,实例化忘记密码 service 发送邮件并且显示处理的结果。
  • Service - 用户的一个行为,发送邮件并且告诉 controller 处理结果。

目前 Service 存在在哪里?

有下面几种方法是容易把 service 的逻辑封装在 Rails 应用里面了。

  • 臃肿的 Model,紧身的 Controller 因为这个原则迫使很多人把 service 逻辑放在 model 里面。这种会把 model 自己的方法和一些多个 model 之间交互的方法混淆在一起。这些交互的方法可能会成为一个问题,重构和测试将会非常麻烦。

  • Concerns 当很多 model 有一样的方法的时候,我们一般会把这些方法抽取放到一个 module 里面去。concern 在 Rails 4 里面也是被推荐使用的。有时候确实是可以的,但这种方法增加了抽象性,有时候会发现去哪里找一个方法会变得困难。 而且会产生潜在的互相依赖的问题。

  • Observer 和 Callbacks 这是 model 生命周期的 hook 处理。对于上面那个例子,我们可以创建和保存一个 ForgottenPassword 对象, 然后在它的 callback 里面做发送邮件处理。这种缺点是和 model 的生命周期绑定太紧,这种行为可以非常迅速地变得难以跟踪,尤其是与正在运行的请求周期之外的观察者, 而回调可以使事情很难测试或预测行为。

  • 臃肿的 Controller 很多人使用 controller 用来连接对象之间的交互。这种方式看起来可以,但缺点是导致 controller 变得 太大变得笨重,测试也是非常麻烦。如果你在 worker 里面也需要这样的功能,可能会复制导致重复代码。

Service 对象

Service 对象实现了独立的功能。上面的忘记密码的例子可以写成这样:

1
2
3
4
5
6
7
class ForgottenPassword
  def initialize(user_id)
    user = User.find user_id
    mail = UserMailer.password_reset @user
    mail.deliver
  end
end

这个例子很简单,你可以做错误处理,以及设置返回值。这样以便 controller 或者 rake task 调用的地方 使用更加方便。

使用了 service, 可以使 User model 里面不需要和 UserMailer 产生耦合。 User model 的测试代码也将变得轻松,ForgottenPassword 的测试也会变得非常直接。

ForgottenPasswordController 代码将会是下面这个样子:

1
2
3
4
5
6
7
8
9
10
class ForgottenPasswordController
  def create
    begin
      ForgottenPassword.new(params[:user_id])
      # render the view
    rescue
      # Handle exceptions and render the view
    end
  end
end

这样 controller 的测试也将会变得简单,没有必要做交互业务的测试,因为 service 测试以及做了。

什么时候使用?

在我看来,model 间的相互交互处理可以作为独立的 service 来处理。这样做将使你隔离你的测试到 非常离散的和独立的领域。

而且,如果你修改现有的应用, 通过采用 service 会看到这些好处的。开发和维护的功能会更容易。

总结

Service 虽然在 Rails 里面并未提及,但我觉得值得这样去做。这样可以使你的程序更加简单,也有助于 测试和维护,我希望你可以考虑使用 Service。

Enumerable Module 里面比较实用的方法

Enumerable module 里面有很多实用的方法,可能有些方法,我们不是很熟悉。 如果知道的话,可能对于改善我们的代码会有所帮助。我这边提取了几个比较实用的 方法供大家参考。(注意本文是 Ruby 2.2.0 版本测试的结果)

chunk {|elt|…}

chunk 方法会将根据 block 里面表达式的返回值分组处理,注意只会分组连续的元素。 例如:

1
2
> [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5].chunk { |n| n.even? }.to_a
=> [[false, [3, 1]], [true, [4]], [false, [1, 5, 9]], [true, [2, 6]], [false, [5, 3, 5]]]

注意 block 返回值里面是 nil 或 :_separator 的话,表示这个元素将会被删除掉, 对于过滤元素非常实用。 如果读文件的内容的时候,想忽略空行的时候可以使用这个方法过滤。

1
2
3
4
5
6
7
8
[3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5].chunk { |n| n > 5 || nil }.to_a
=> [[true, [9]], [true, [6]]]

[3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5].chunk { |n| n > 5 && nil }.to_a
=> [[false, [3, 1, 4, 1, 5]], [false, [2]], [false, [5, 3, 5]]]

[3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5].chunk { |n| n > 5 || :_separator }.to_a
=> [[true, [9]], [true, [6]]]

还有一个比较特别的 :_alone 表达式表示自己独立分组不和别的元素在一起。 例子:

1
2
3
4
5
[3, 9, 10, 2, 6, 7].chunk { |n| n > 5 }.to_a
=> [[false, [3]], [true, [9, 10]], [false, [2]], [true, [6, 7]]]

[3, 9, 10, 2, 6, 7].chunk { |n| n > 5 ? :_alone : true }.to_a
=> [[true, [3]], [:_alone, [9]], [:_alone, [10]], [true, [2]], [:_alone, [6]], [:_alone, [7]]]

在 rails 开发中也会经常遇到使用 chunk 的情况,例如:想把用户按照月份分组显示出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- @users = User.order('created_at desc')

%h1 User Monthly Breakdown

- @users.chunk { |u| u.created_at.strftime("%B %Y") }.each do |month, users|
  %h3= month
  %table.table
    %thead
      %tr
        %th Name
        %th Email

    - users.each do |user|
      %tr
        %td= user.name
        %td= user.email

partition { |obj| block }

将一个集合根据 block 的返回值,分割成2个。

1
(1..6).partition { |v| v.even? }  #=> [[2, 4, 6], [1, 3, 5]]

max_by, min_by, minmax_by

从名字上就可以猜出来是用来取最大最小值的。

1
2
3
4
5
a = %w(albatross dog horse)
a.max                        #=> "horse"
a.max_by { |x| x.length }   #=> "albatross"
a.min_by { |x| x.length }   #=> "dog"
a.minmax_by { |x| x.length }   #=> ["dog", "albatross"]

zip

将 enum 每个元素和参数的每个对应元素合并。 例子:

1
2
3
4
5
6
a = [ 4, 5, 6 ]
b = [ 7, 8, 9 ]

[1, 2, 3].zip(a, b)      #=> [[1, 4, 7], [2, 5, 8], [3, 6, 9]]
[1, 2].zip(a, b)         #=> [[1, 4, 7], [2, 5, 8]]
a.zip([1, 2], [8])       #=> [[4, 1, 8], [5, 2, nil], [6, nil, nil]]

[译] Rails Sessions 是如何工作的?

看到 Justin Weiss 写的一篇 Rails 的 Session 是如何工作的,觉得写的不错。 就翻译出来,和大家共勉。以下是正文:

如果 Rails 不能告诉你谁在访问你的网站?如果你不知道同一个人在访问2个不同的页面? 如果所有的数据在你返回 response 之前都消失的话?

对于一个静态网站来说也许没有问题。但是大多数应用程序需要存储一个用户的数据的。可能是一个 用户 id 或者 选择的语言偏好,或者他们通过 iPad 访问你的网站,想看看桌面的版本。

session 是一个非常好的地方来放置这些数据。一般是存储一些不止一个请求需要用的数据。

Session 使用起来很简单。

1
session[:current_user_id] = @user.id

但是它们会有点神奇。 session 是什么? Rails 是怎么处理这些数据,对于不同的人显示不同的数据。 还有你怎么决定在哪里存储你的 session 数据呢?

什么是 Session?

session 就是存储多个请求需要共享的数据。你可以设置一些数据在 controller 里面。

1
2
3
4
5
6
app/controllers/sessions_controller.rb
def create
  # ...
  session[:current_user_id] = @user.id
  # ...
end

然后在另外一个 controller 里面读取。

1
2
3
4
5
6
app/controllers/users_controller.rb

def index
  current_user = User.find_by_id(session[:current_user_id])
  # ...
end

这个看起来可能不是很有趣。但是这个协调了浏览器和 Rails 应用之间的联系。这个是通过 cookies 实现的。

当你对一个页面发送请求的话,rails 服务器的response将会返回一个 cookie。

1
2
3
$ curl -I http://www.google.com | grep Set-Cookie

Set-Cookie: NID=67=J2xeyegolV0SSneukSOANOCoeuDQs7G1FDAK2j-nVyaoejz-4K6aouUQtyp5B_rK3Z7G-EwTIzDm7XQ3_ZUVNnFmlGfIHMAnZQNd4kM89VLzCsM0fZnr_N8-idASAfBEdS; expires=Wed, 16-Sep-2015 05:44:42 GMT; path=/; domain=.google.com; HttpOnly

你的浏览器将会保存这些 cookie。直到你的 cookie 过期,每次你发送一个请求,浏览器将会把这个 cookie 发送 到服务器。

1
2
3
4
5
> GET / HTTP/1.1
> User-Agent: curl/7.37.1
> Host: www.google.com
> Accept: */*
> Cookie: NID=67=J2xeyegolV0SSneukSOANOCoeuDQs7G1FDAK2j-nVyaoejz-4K6aouUQtyp5B_rK3Z7G-EwTIzDm7XQ3_ZUVNnFmlGfIHMAnZQNd4kM89VLzCsM0fZnr_N8-idASAfBEdS; expires=Wed, 16-Sep-2015 05:44:42 GMT; path=/; domain=.google.com; HttpOnly

很多 cookie 看起来是乱码一样的文字组合,其实应该这样。这个数据并不是给浏览器的使用用户看的, 是给你的 Rails 应用读的,也只有它可以读出来里面的信息。

对于 session 我们需要做什么呢?

这时候,我们有了 cookie,你把数据放在里面,下一个请求的时候就可以读取了。那和 session 有什么不同呢? 默认情况下,在 Rails 里面是没有什么不同的。只不过 Rails 做了更多处理来保证 cookie 数据的安全。 除了这个以外,它工作的和你期待的一样。你的 Rails 应用把一些数据放 cookie,然后你从 cookie 里面读 取这些数据。但是 cookie 也不是 session 最好的存储方案。

  • 在 cookie 里面你最多只能存储 4K 的数据 一般情况下是足够了,但有时也会不够。
  • 每次发送请求都会发送 cookie。 如果 cookie 比较大意味着request和response的请求也会比较大,将会影响网站的访问速度。
  • 如果你不小心泄露了你的 secret_key_base, 那就意味着你的用户可以伪造 cookie 入侵的你网站。
  • 存储一些不安全的数据到 cookie。

如果你对这些问题都很小心的话,不是什么大问题。如果当年由于某些原因不想把 session 数据存储到 cookie里面的 话, rails 也提供了其他方式供你选择。

其他存储方式

虽然session数据不存储在 cookie 里面,但他们工作的流程基本类似。下面是实际的一个例子来解释下:

把 session 数据存储在数据库里面 1. 当年使用 session[:current_user_id] = 1 这个赋值。 2. Rails 将会创建一个 sessions 表产生一个随机的 session Id。(09497d46978bf6f32265fefb5cc52264) 3. 他将会存储 {current_user_id: 1} (Base 64编码)到 session 的 data 属性里面。 4. 而且将会返回 session id,然后将这个 id (09497d46978bf6f32265fefb5cc52264) 存到 cookie 里面发送到浏览器。

下次浏览器发送请求的时候

  1. 将会把 cookie 发送到服务器端:
1
2
Cookie: _my_app_session=09497d46978bf6f32265fefb5cc52264;
path=/; HttpOnly
  1. 当你使用 session[:current_user_id] 取值的时候
  2. Rails 会使用这个 id 去 sessions 表里面查询出对应的 session 值。
  3. 最后从找到的记录的 data 属性里面返回 current_user_id

不管你的 session 存储在 数据库还是 memcache,还是 Redis 里面。大多数都是 按照这样的流程在工作。这个时候 cookie 只是存储的 session ID, rails 会 自己根据这个 ID 去找 session 里面存储的数据。

cookie,cache 还是数据库存储呢?

首先存储到 cookie 是最简单的,他不需要你设置什么额外的操作。如果不想存在 cookie,可以 选择存储到 数据库 或 cache 里面。

存储 session 到 cache

你可能已经使用 memcache 存储你的一些页面缓存。cache 是第二方便存储 session 的地方。 你不需要担心 session 过度增长的问题,因为旧的 session 会自动被删除掉如果 cache 变得很大。 而且他访问速度很快,就相当于访问内存数据一样。 但也有缺点的:

  1. 如果你想保存旧的 session 数据,可能你并不希望被删除掉。
  2. 你的 session 将会占空间,如果你没有足够的内存,会遇到内存很多被 占用的情况,或者 session 很早就过期了。
  3. 如果你想重置你的 cache,可能由于 rails 升级之类导致的,这样会导致所有人的 session 都失效了。

存储到数据库

如果你想保存旧的 session 直到过期之前,你可以考虑存储到数据库。例如 Redis 或者 ActiveRecord。 但是数据库存储也有缺点的:

  1. 如果存储到数据库,session 不会自动清理的。需要你自己手动做的。
  2. 你必须知道如果很多 session 数据的话,数据库有什么影响? 例如: redis 的话,你必须保证有足够大的内存,不要一会就占满内存了。

  3. 你必须得小心,可能由于网络攻击或者网络蜘蛛导致产生大量无用的 session 数据,浪费了你的数据库空间。

大多数问题可能发生的比较少,但你必须有和这个意识。

该如何选择呢?

如果你确定 cookie 里面保存的缺点不会是问题的话,那就首选 cookie吧,毕竟不需要什么特殊配置,而且也不需要 特别维护。

cookie 之外,我一般首选 cache,然后是数据库。因为我认为 session 只是临时数据,所以放 cache 里面好一点。

Rails 开发中经常犯的错误

1 - 在 Model 以外的地方做数据库查询

不好的例子

1
2
3
4
5
6
7
# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index
    @users = User.where(active: true).order(:last_login_at)
  end

end

上面这段代码复用率不高并且不易测试。如果你在其他地方也想做同样的查询,将会产生重复代码。

比较好的例子

上面的查询我们可以将 controller 里面的查询 通过 model 的 scope 来代替。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# app/models/user.rb
class User < ActiveRecord::Base

  scope :active, -> { where(active: true) }
  scope :by_last_login_at, -> { order(:lasst_login_at) }

end

# app/controllers/users_controller.rb
class UsersController < ApplicationController

  def index
    @users = User.active.by_last_login_at
  end

end

记住如果使用 where, order, joins, includes, group, having 等查询的话,记得放 model 里面去。

2 - 在 View 里有业务逻辑处理

不好的例子

1
2
3
4
5
6
7
8
<!-- app/views/posts/show.html.erb -->
...

<h2>
  <%= "#{@comments.count} Comment#{@comments.count == 1 ? '' : 's'}" %>
</h2>

...

上面这段代码看起来没有什么错误,但是看上去 Ruby 代码使得 HTML 可读性不是很好,你应该 停止做这样的事情,这个同样会使得代码复用性不够高,而且不方便独立测试。

比较好的例子

将上面的方法独立到 helper 里面去。

1
2
3
4
5
6
7
8
9
10
11
# app/helpers/comments_helper.rb
module CommentsHelper
  def comments_count(comments)
    "#{comments.count} Comment#{comments.count == 1 ? '' : 's'}"
  end
end

<!-- app/views/posts/show.html.erb -->
<h2>
  <%= comments_count(@comments) %>
</h2>

改完之后会发现 html 变得简单,可读性变好,helper 里面的代码可以被其他地方使用了, 测试也更加方便了。

另外解决这个问题的方法是使用 decorators,具体使用可以参考 draper 这个 Gem。

3 - 使用无意义的变量名或方法名

不好的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# app/models/topic.rb
class Topic < ActiveRecord::Base

  def self.r_topics(questions)
    rt = []

    questions.each do |q|
      topics = q.topics

      topics.each do |t|
        if t.enabled?
          rt << t
        end
      end
    end

    Topic.where(id: rt)
  end

end

这个代码主要不好的地方是你需要花很多时间去理解里面的业务逻辑, r_topics 方法名什么意思? tr 变量干嘛的? 从第一眼里面无法 猜测到作者的企图。

比较好的例子

我们通过使用有意义的名字定义变量和方法,这个将会帮助开发者非常容易理解你的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# app/models/topic.rb
class Topic < ActiveRecord::Base

  def self.related(questions)
    related_topics = []

    questions.each do |question|
      topics = question.topics

      topics.each do |topic|
        if topic.enabled?
          related_topics << topic
        end
      end
    end

    Topic.where(id: related_topics)
  end

end

这样的好处是:

  • 第一次看到这个方法名,就能推测出这个方法会返回什么。 一些关于某个 question 的 topic 集合。
  • related_topics 是存储 question 相关的 topic 的一个数组, 而上面方法里面的 rt 根本就不知道什么意思。
  • 使用 topic 代替变量 t, question 代替 q。会让第一次读你代码的人 非常容易理解,这样就可以让代码来描述自己,这才是最好的。

4 - 使用 Unless 或 否定表达式 在判断条件里

不好的例子

1
2
3
4
5
6
7
8
9
10
# app/services/charge_user.rb
class ChargeUser

  def self.perform(user, amount)
    return false unless user.enabled?

    PaymentGateway.charge(user.account_id, amount)
  end

end

这个代码看起来并不是很难理解,但是使用了unless将会给代码增加些一点复杂度,因为必须要反响思考。

比较好的例子

如果我们使用 if 的话代码会更加容易理解了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# app/models/user.rb
class User < ActiveRecord::Base

  def disabled?
    !enabled?
  end

end

# app/services/charge_user.rb
class ChargeUser

  def self.perform(user, amount)
    return false if user.disabled?

    PaymentGateway.charge(user.account_id, amount)
  end

end

是不是感觉代码可读性更加好了呢?记住尽量使用 if 或 肯定表达式,如果没有肯定表达式, 可以增加一个,像上面 User Model 改的那样。

5 - controller 有很多业务逻辑判断

不好的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# app/models/user.rb
class User < ActiveRecord::Base

  def enable!
    update(enabled: true)
  end

end

# app/controllers/users_controller.rb
class UsersController < ApplicationController

  def enable
    user = User.find(params[:id])

    if user.disabled?
      user.enable!
      message = "User enabled"
    else
      message = "User already disabled"
    end

    redirect_to user_path(user), notice: message
  end

end

这段问题是你在更新 enabled 之前会查询是不是 disabled。这个判断不应该放在 controller 里面和其他代码在一起。

比较好的例子

应该将这个判断逻辑移动到 model 里面去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# app/models/user.rb
class User < ActiveRecord::Base

  def enable!
    if disabled?
      update(enabled: true)
    end
  end

end

# app/controllers/users_controller.rb
class UsersController < ApplicationController

  def enable
    user = User.find(params[:id])

    if user.enable!
      message = "User enabled"
    else
      message = "User already disabled"
    end

    redirect_to user_path(user), notice: message
  end

end

这样 controller 代码将更加简单。

6 - 使用复杂的判断条件

不好的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# app/controllers/posts_controller.rb
class PostsController < ApplicationController

  def destroy
    post = Post.find(params[:id])

    if post.enabled? && (user.own_post?(post) || user.admin?)
      post.destroy
      message = "Post destroyed."
    else
      message = "You're not allow to destroy this post."
    end

    redirect_to posts_path, notice: message
  end

end

上面的判断问了太多的条件了,其实总结起来就是判断这个用户是否可以删除。

比较好的例子

这个判断条件如果我们想以后复用方便的话,最好是移动到 model 里面去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# app/models/user.rb
class User < ActiveRecord::Base

  def can_destroy_post?(post)
    post.enabled? && (own_post?(post) || admin?)
  end

end

# app/controllers/posts_controller.rb
class PostsController < ApplicationController

  def destroy
    post = Post.find(params[:id])

    if user.can_destroy_post?(post)
      post.destroy
      message = "Post destroyed."
    else
      message = "You're not allow to destroy this post."
    end

    redirect_to posts_path, notice: message
  end

end

所以当你什么时候遇到有 && 或 || 的条件判断的时候,你可以考虑把这个条件抽象成一个方法, 为之后复用使用。

7 - 在 Model 的实例方法里面没有必要使用 self

不太好的例子

1
2
3
4
5
6
7
8
# app/models/user.rb
class User < ActiveRecord::Base

  def full_name
    "#{self.first_name} #{self.last_name}"
  end

end

上面的方法比较简单,但没有必要使用 self ,这样会使代码看上去更加简单易读。

比较好的例子

在实例方法里面是没有必要使用 self 的,除非是在赋值的时候必须加上 self。

1
2
3
4
5
6
7
8
# app/models/user.rb
class User < ActiveRecord::Base

  def full_name
    "#{first_name} #{last_name}"
  end

end

8 - 使用条件判断并且返回

不太好的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# app/models/user.rb
class User < ActiveRecord::Base

  def full_name
    if name
      name
    else
      "No name"
    end
  end

end

# app/models/user.rb
class User < ActiveRecord::Base

  def full_name
    name ? name : "No name"
  end

end

这段代码的问题是增加了控制逻辑,其实是没有必要的。

比较好的例子

下面这种很简单的方式就可以搞定了。

1
2
3
4
5
6
7
8
# app/models/user.rb
class User < ActiveRecord::Base

  def full_name
    name || "No name"
  end

end

|| 和 && 操作符是功能非常强大的,他们可以帮助你简化很多代码。

Ruby 2.2 中的新方法

目前围绕 Ruby 2.2 来说,讨论的最多的是垃圾回收的更新。GC 目前会清除掉 symbol,并且 在减少暂停时间的方面有算法的改善。这些改变确实令人感到兴奋,但是其实还是也增加了一些新方法。 让我们看看 2.2 里面增加什么新玩意。

binding#local_variables

如果你想知道一个 scope 范围内有哪些本地变量的话,就可以使用这个方法获取。

1
2
3
4
5
6
7
8
9
10
11
def addition(x, y)
  puts binding.local_variables.inspect
  z = x + y
  puts binding.local_variables.inspect
  z
end

> addition(2, 3)
[:x, :y, :z]
[:x, :y, :z]
=> 5

binding#receiver

这个方法可以让你知道调用方法的对象是谁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Cat
  def self.type
    binding.receiver
  end
end

> Cat.type
Cat

class Tiger < Cat
end

> Tiger.type
Tiger

Dir#fileno

得到文件夹的文件描述符。文件描述符在形式上是一个非负整数。实际上,它是一个索引值, 指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时, 内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。 但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。所以此方法在windows里面运行,将会报错。 习惯上,标准输入(standard input)的文件描述符是 0,标准输出(standard output)是 1, 标准错误(standard error)是 2。

1
2
3
4
> $stdout.fileno
1
> Dir.new('.').fileno
8

Enumerable#slice_after

这个方法是对目前已存在的方法slice_before的相对的实现。我们先看看slice_before是怎么工作的。

slice_before方法是用来按照一定的条件分割数组这样的enumerables。举个例子看下:

1
2
> [1, 'a', 2, 'b', 'c', 3, 'd', 'e', 'f'].slice_before { |e| e.is_a?(Integer) }.to_a
[[1, "a"], [2, "b", "c"], [3, "d", "e", "f"]]

可以看到,这个方法在1,2,3前面做了分割。

其实也是可以传一个参数代替block的,如果是一个参数的话,参数是使用===运算符来检查的(和case一样)。 所以我们可以得到同样的结果如果我们传入Integer参数。

1
2
> [1, 'a', 2, 'b', 'c', 3, 'd', 'e', 'f'].slice_before(Integer).to_a
[[1, "a"], [2, "b", "c"], [3, "d", "e", "f"]]

那再看看slice_after的结果,就可以非常容易理解了。

1
2
> [1, 'a', 2, 'b', 'c', 3, 'd', 'e', 'f'].slice_after(Integer).to_a
[[1], ["a", 2], ["b", "c", 3], ["d", "e", "f"]]

Enumerable#slice_when

一个非常有趣的方法是slice_when,不像slice_after方法,这个方法只接受block。这个block接受的是 2个元素,根据block返回true来分割。

1
2
> [1, 3, 4, 5, 7, 8, 9, 10, 12].slice_when { |a, b| a + 1 != b }.to_a
[[1], [3, 4, 5], [7, 8, 9, 10], [12]]

这个方法也可以用来查找一个数组里面某个数字出现的频率。

1
2
> Array.new(10) { rand(3) + 1 }.sort.slice_when(&:!=).map { |x| [x.first, x.size] }
[[1, 4], [2, 4], [3, 2]]

从结果可以看出,1 和 2 分别出现了 4 次,3 出现了 2 次。

Float#next_float, Float#prev_float

这2个方法返回可表现的下一个或者前一个浮点数。注意并不是所有的浮点数都可以表现出来。

1
2
> 1.0.next_float
1.0000000000000002

可以注意到忽略了1.0000000000000001,因为1.0000000000000001不能通过Float表现出来。

1
2
> 1.0000000000000001
1.0

还有需要注意的是2个值之间的间隔并不一定是相等的。

1
2
3
4
5
> 2.0.prev_float
1.9999999999999998

> 2.0.next_float
2.0000000000000004

这2个方法是用来发现ULP(unit of least precision)的值是非常方便的,但实际开发中有可能很少会用到, 但是知道的话确实对于更好。

File.birthtime, File#birthtime, File::Stat#birthtime

我们已经有了文件的atime,ctimemtime方法,2.2里面增加了文件的birthtime的方法。即文件的创建时间。

1
2
3
4
5
6
7
8
> File.new('test', 'w').birthtime
2015-01-06 19:24:44 -0600

> File.birthtime('test')
2015-01-06 19:24:44 -0600

> File::Stat.new('test').birthtime
2015-01-06 19:24:44 -0600

Kernel#itself

一个返回本身自己的方法。可能你会问,这个方法有什么用?用的比较多的是group方法。例:

1
2
3
4
5
> 1.itself
1

> [2, 3, 3, 1, 2, 3, 3, 1, 1, 2].group_by(&:itself)
{2=>[2, 2, 2], 3=>[3, 3, 3, 3], 1=>[1, 1, 1]}

在map或select方法里面也是可以使用的。

Method#curry(arity)

返回一个方法的proc,arity是Proc接受的参数的个数。如果参数不足,会返回另外一个Proc。 只有当参数足够了的话才会调用方法。例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def foo(a,b,c)
  [a, b, c]
end

proc  = self.method(:foo).curry
proc2 = proc.call(1, 2)          #=> #<Proc>
proc2.call(3)                    #=> [1,2,3]

def vararg(*args)
  args
end

proc = self.method(:vararg).curry(4)
proc2 = proc.call(:x)      #=> #<Proc>
proc3 = proc2.call(:y, :z) #=> #<Proc>
proc3.call(:a)             #=> [:x, :y, :z, :a]

Method#super_method

看着这个方法的名字,估计也猜的出来是干嘛的,这个方法就是调用父类的方法。如果父类 没有这个方法,将会返回nil。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Cat
  def speak
    'meow'
  end
end

class Tiger < Cat
  def speak
    'roar'
  end
end

> Tiger.new.method('speak')
#<Method: Tiger#speak>
> Tiger.new.method('speak').super_method
#<Method: Cat#speak>
> Cat.new.method('speak').super_method
nil

Hash支持字符串 + 冒号作为 key 了

在 Ruby 2.2 之前,如果定义以字符串作为 key 的 Hash 的时候,是不可以写成这样的。 2.2 里面已经支持这种写法了。

1
{"key": "value"}

[译] 在 Ruby 2.2 中的 Symbol GC

什么是 Symbol GC

什么是Symbol GC呢?为什么我们需要在乎它呢?Ruby 2.2 刚刚发布了一个新的功能来对应 不断增长的GC,这是2.2里面比较大的功能。如果你熟悉 Ruby 的话,一定听说过 “symbol DoS” 的问题。一共 symbol 拒绝式攻击,当一共系统创建太多的 symbol 会导致内存不足的问题。 这是因为 Ruby 2.2 之前的版本, symbol是永远存在的。例如:

在 Ruby 2.1 中:

1
2
3
4
5
6
7
8
9
10
# Ruby 2.1

before = Symbol.all_symbols.size
100_000.times do |i|
  "sym#{i}".to_sym
end
GC.start
after = Symbol.all_symbols.size
puts after - before
# => 100001

在这里我们建了100000个 symbols,即使我们运行了 GC, symbol的数量并未发生减少。如果写了一些代码, 将用户的输入参数调用了to_sym方法的话:

1
2
3
def show
  step = params[:step].to_sym
end

这种情况下,有人可以通过发送很多example.com/step=的request请求,因为系统没有对symbol做垃圾回收, 你的程序将会导致内存不足从而引起程序崩溃。像这种根据用户的收入新建symbol是非常危险的,如果symbol没有 被回收的话,但这个问题只发生在 2.2 版本之前。

在Ruby 2.2 中的 Symbol GC

Ruby 2.2 中的 Symbol 可以垃圾回收了。

1
2
3
4
5
6
7
8
9
# Ruby 2.2
before = Symbol.all_symbols.size
100_000.times do |i|
  "sym#{i}".to_sym
end
GC.start
after = Symbol.all_symbols.size
puts after - before
# => 1

因为我们创建的symbol没有被其他对象引用,他们可以被安全的回收。这样可以避免我们意外创建太多 symbol 导致 程序崩溃的问题。然而 Ruby 并没有回收所有的 symbol。

不是所有的 symbol

在 Ruby 2.2以前,我们不能收集symbol是因为Ruby解释器内部使用了。每个symbol都有一个唯一的object ID。例如 ::foo.object_id的值一直是一样,不管你在哪里使用,这是rb_intern工作方式导致的。

在 C-Ruby,当你创建了一个方法将会是存储一个唯一的ID到一个方法表里面。

如果你调用这个方法,Ruby会寻找方法名的symbol,然后得到这个symbol的ID。这个symbol的ID 被用来指向在C方法的静态内存。C方法然后被调用,这就是Ruby怎么调用方法的过程。

如果我们垃圾回收symbol,而这个symbol正好指向一个方法的话,这个方法将不再被能调用,这是不好的。

围绕这个问题,Narihiro Nakamura 介绍了在C世界里面的”永生的 Symbol” 和 Ruby世界 “终将死亡的 Symbol” 的2个概念。

一般来说,当调用Ruby运行to_sym方法,所有的 Symbol 会被动态创建,这些 Symbol 可以被垃圾回收的。 因为这些 Symbol 没有被使用在 Ruby 内部的解释器里面。然而,symbol被用来创建新方法或者内部使用的一些 symbol将不会被回收。例如::foodef foo; end都不会被垃圾回收,然后"foo".to_sym将 有资格被垃圾回收。

知道这种实现方式的话,那还是有可能被Dos如果你根据用户输入动态定义方法的话。

1
2
3
define_method(params[:step].to_sym) do
  # ...
end

因为define_method调用rb_intern,因为我们创建了动态的方法,导致都会转化为永生的symbol用来 方法的查找。但是一般情况下,我们会很少这么使用。

变量也需要使用symbols。

1
2
3
4
5
6
7
8
9
before = Symbol.all_symbols.size
eval %Q{
  BAR = nil
  FOO = nil
}
GC.start
after = Symbol.all_symbols.size
puts after - before
# => 2

即使变量的值是nil,内部还是使用了symbol,这个symbol永远都不会被垃圾回收。 所以除了避免根据用户输入创建方法之外,也要注意避免根据用户输入定义变量的代码出现。

1
self.instance_variable_set( "@step_#{ params[:step] }".to_sym, nil )

为了保证安全,你需要周期性的看下Symbol.all_symbols.size的值(在运行完GC.start之后)来确保 symbol表没有持续增长。未来希望有一些标准告诉我们哪些使用symbol的方式是不安全的,成为一个常识。

需要在乎速度

除了安全之外,最大的原因你应该在乎的是速度。有很多代码将symbol转化成了string,为了避免根据用户输入 动态生成symbol的问题。

使用最多的应该是Rails的ActiveSupport里的HashWithIndifferentAccess类。我使用了benchmark测试了下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
require 'benchmark/ips'

require 'active_support'
require 'active_support/hash_with_indifferent_access'

hash = { foo: "bar" }
INDIFFERENT = HashWithIndifferentAccess.new(hash)
REGULAR     = hash

Benchmark.ips do |x|
  x.report("indifferent-string") { INDIFFERENT["foo"] }
  x.report("indifferent-symbol") { INDIFFERENT[:foo] }
  x.report("regular-symbol")     { REGULAR[:foo] }
end

结果

1
2
3
4
5
6
7
8
Calculating -------------------------------------
indifferent-string   115.962k i/100ms
indifferent-symbol    82.702k i/100ms
regular-symbol   150.856k i/100ms
-------------------------------------------------
indifferent-string      4.144M (± 4.4%) i/s -     20.757M
indifferent-symbol      1.578M (± 3.7%) i/s -      7.939M
regular-symbol      8.685M (± 2.4%) i/s -     43.447M

你会发现使用HashWithIndifferentAccess通过string作为key访问的速度只有正常Hash访问速度的一半, 而使用symbol方式访问的速度比Hash访问的慢5倍。所以通过访问一个hash通过symbol访问速度是最快的, 所以目前Ruby 2.2的话,最好还是使用symbol作为parameters的key。所以没有必要 使用HashWithIndifferentAccess了。

总结

Symbol GC可以让你避免Dos攻击,可以让你更加灵活使用symbol,所以还不赶快升级Ruby到2.2。

Ruby 2.0 的新特性 Refinements 介绍

很多人都知道在 Ruby 里面可以重新定义或者添加方法到已经存在的类里面, 这个叫做 “monkey patch”。但是这种定义是全局性的,所有使用这个类 的用户会被影响到,如果有其他gem定义了同样的方法名,可能还会引起混淆, 所以由此产生了 Refinements。

Refinements 就是用来减少 monkey patch 影响他人而产生的,他提供了一种 在本地继承类的方式。

Refinements 在 Ruby 2.0 里面还只是个实验,当然还是期望这个功能在之后的版本可以一直存在。 但有些行为会被改变,所以当你第一次使用的时候会有警告出现。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
class C
  def foo
    puts "C#foo"
  end
end

module M
  refine C do
    def foo
      puts "C#foo in M"
    end
  end
end

refine 是 module 的方法,refine 只能改变 class ,是不能作用于 module 的。

如果想使用的话,使用 using 就可以了:

1
2
3
4
5
using M

x = C.new

c.foo # prints "C#foo in M"

作用范围 (scope)

你可以激活refinements在任何位置,不一定在类/模块/方法的里面。如果定义在Kernel#eval里面的话, 将会是全局性的。Refinement的作用范围是文件的结束或者eval string的结束位置。

如果这个refinements传入到当前的scope之外的话,将不会被激活。 例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class C
end

module M
  refine C do
    def foo
      puts "C#foo in M"
    end
  end
end

def call_foo(x)
  x.foo
end

using M

x = C.new
x.foo       # prints "C#foo in M"
call_foo(x) #=> raises NoMethodError

如果搞成多个文件的话,refinements只在引入的文件里面有效。

c.rb

1
2
class C
end

m.rb

1
2
3
4
5
6
7
8
9
require "c"

module M
  refine C do
    def foo
      puts "C#foo in M"
    end
  end
end

m_user.rb

1
2
3
4
5
6
7
8
9
require "m"

using M

class MUser
  def call_foo(x)
    x.foo
  end
end

main.rb

1
2
3
4
5
6
require "m_user"

x = C.new
m_user = MUser.new
m_user.call_foo(x) # prints "C#foo in M"
x.foo              #=> raises NoMethodError

因为 m_user.rb 里面使用了 using,所以 refinement 起作用了,call_foo就可以调用了。 下面举些例子说明 refinement 的作用范围,以便更好的理解。

在文件里面
1
2
3
4
5
6
7
8
9
10
11
# not activated here
using M
# activated here
class Foo
  # activated here
  def foo
    # activated here
  end
  # activated here
end
# activated here
在 eval 里面
1
2
3
4
5
6
7
# not activated here
eval <<EOF
# not activated here
using M
# activated here
EOF
# not activated here
在 if 判断条件里面
1
2
3
4
5
# not activated here
if false
  using M
end
# not activated here
定义多个refinements

如果同一个module定义了多个的话,所有的定义都会被激活。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
module ToJSON
  refine Integer do
    def to_json
      to_s
    end
  end

  refine Array do
    def to_json
      "[" + map { |i| i.to_json }.join(",") + "]"
    end
  end

  refine Hash do
    def to_json
      "{" + map { |k, v| k.to_s.dump + ":" + v.to_json }.join(",") + "}"
    end
  end
end

using ToJSON

p [{1=>2}, {3=>4}].to_json # prints "[{\"1\":2},{\"3\":4}]"

你也可以定义在类或模块定义里面

他的作用范围将只是在类或模块定义结束为止。

1
2
3
4
5
6
7
8
9
10
11
# not activated here
class Foo
  # not activated here
  using M
  # activated here
  def foo
    # activated here
  end
  # activated here
end
# not activated here

Resque 和 Sidekiq 的区别

Rails 开发中,经常会使用到后台任务的功能,一般都会用到resque和sidekiq这2个 知名的 gem。

这2个有什么区别呢?我搜了一些网上的资料,总结了一下。

主要区别

他们都使用的 redis 作为任务数据存储,这块是差不多的,主要的区别还是在多任务处理的方式是不一样的。

resque 使用的是一个 worker 通过 fork 方式来产生多个 worker 处理多个任务,而 sidekiq 是一个 worker 使用的 Thread 方式产生多个线程 处理多个任务。

那 fork 方式和 thread 方式有什么区别呢?

fork 方式

fork 一个进程,操作系统会建立一个独立分开的地址空间,并且从父进程复制所有的内存片段到这个地址里面去。 这就意味着对于操作系统来说,对于 fork 的进程切换上下文,因为需要保存和加载所有数据,所以代价更大。 而且如果父进程死掉了,这些 fork 的子进程没有退出的话,将会变成僵尸进程。

thread 方式

多线程的话是共享地址空间,内存并且多线程之间的交互也比较方便。而且你也不用担心僵尸进程的问题,一旦进程死掉, 所有的线程会自动被杀掉。但这种方式也有缺点,你必须保证代码是线程安全的,不然可能会引起麻烦。

总结

  • resque 比 sidekiq 更消耗内存
  • resque 的 worker 代码没有必要担心线程安全问题,但 sidekiq 必须考虑

本文均为本人理解之后整理出来的,如有错误,欢迎大家指出。

Gem如何工作

一般我们在使用Gem的过程中不会遇到什么问题,但如果一旦遇到问题解决问题起来可能会比较麻烦。 如果我们知道Gem怎么工作的话,将会帮助我们解决使用Gem中遇到的问题。

gem install 做了什么

gem就是压缩的代码包。你可以通过gem unpack命令看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ gem unpack resque_unit
Fetching: resque_unit-0.4.8.gem (100%)
Unpacked gem: '/Users/grant/projects/grant/resque_unit-0.4.8'
$ cd resque_unit-0.4.8
$ find .
.
./lib
./lib/resque_unit
./lib/resque_unit/assertions.rb
./lib/resque_unit/errors.rb
./lib/resque_unit/helpers.rb
./lib/resque_unit/plugin.rb
./lib/resque_unit/resque.rb
./lib/resque_unit/scheduler.rb
./lib/resque_unit/scheduler_assertions.rb
./lib/resque_unit.rb
./lib/resque_unit_scheduler.rb
./README.md
./test
./test/resque_test.rb
./test/resque_unit_scheduler_test.rb
./test/resque_unit_test.rb
./test/sample_jobs.rb
./test/test_helper.rb

gem install其实就是做类似这样的事情,取下gem压缩包文件,解压到某个特定目录。 这个特定目录位置可以通过gem environment命令查看INSTALLATION DIRECTORY

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
RubyGems Environment:
- RUBYGEMS VERSION: 2.0.14
- RUBY VERSION: 2.0.0 (2014-11-13 patchlevel 598) [x86_64-darwin14.0.0]
- INSTALLATION DIRECTORY: /Users/grant/.rbenv/versions/2.0.0-p598/lib/ruby/gems/2.0.0
- RUBY EXECUTABLE: /Users/grant/.rbenv/versions/2.0.0-p598/bin/ruby
- EXECUTABLE DIRECTORY: /Users/grant/.rbenv/versions/2.0.0-p598/bin
- RUBYGEMS PLATFORMS:
- ruby
- x86_64-darwin-14
- GEM PATHS:
- /Users/grant/.rbenv/versions/2.0.0-p598/lib/ruby/gems/2.0.0
- /Users/grant/.gem/ruby/2.0.0
- GEM CONFIGURATION:
- :update_sources => true
- :verbose => true
- :backtrace => false
- :bulk_threshold => 1000
- "gem" => "--no-ri --no-rdoc"
- REMOTE SOURCES:
- https://rubygems.org/

gem就在INSTALLATION DIRECTORY的gems目录下面:

1
2
3
$ cd /Users/grant/.rbenv/versions/2.0.0-p598/lib/ruby/gems/2.0.0
$ ls
bin       build_info  bundler     cache       doc     gems        specifications

这个path在不同环境下面的路径是不一样的。所以gem environment将会非常方便让你 知道gem的代码在哪里。

gem 代码怎么require的呢?

rubygems覆盖了ruby的require方法来实现加载gem的功能。看下面的注释就知道他怎么工作了。

1
2
3
4
5
6
7
8
9
10
11
12
core_ext/kernel_require.rb
##
# When RubyGems is required, Kernel#require is replaced with our own which
# is capable of loading gems on demand.
#
# When you call <tt>require 'x'</tt>, this is what happens:
# * If the file can be loaded from the existing Ruby loadpath, it
#   is.
# * Otherwise, installed gems are searched for a file that matches.
#   If it's found in gem 'y', that gem is activated (added to the
#   loadpath).
#

如果你想加载active_support,RubyGems首先使用Ruby的require方法从loadpath里去找, 这个将会出现一个错误。

1
2
3
4
LoadError: cannot load such file -- active_support
from (irb):17:in `require'
from (irb):17
from /usr/local/bin/irb:11:in `<main>'

通过require找不到的话,rubygems会从Gems文件里面找active_support.rb

1
2
irb(main):001:0> spec = Gem::Specification.find_by_path('active_support')
=> #<Gem::Specification:0x3fe52159e714 activesupport-4.1.8>

如果找到的话,将会激活这个gem,然后把这个gem加到load path里面去。

1
2
3
4
5
6
irb(main):004:0*  $LOAD_PATH
=> ["/Users/grant/.rbenv/plugins/rbenv-gem-rehash", "/Users/grant/.rbenv/rbenv.d/exec/gem-rehash", "/Users/grant/.rbenv/versions/2.0.0-p481/lib/ruby/site_ruby/2.0.0", "/Users/grant/.rbenv/versions/2.0.0-p481/lib/ruby/site_ruby/2.0.0/x86_64-darwin14.0.0", "/Users/grant/.rbenv/versions/2.0.0-p481/lib/ruby/site_ruby", "/Users/grant/.rbenv/versions/2.0.0-p481/lib/ruby/vendor_ruby/2.0.0", "/Users/grant/.rbenv/versions/2.0.0-p481/lib/ruby/vendor_ruby/2.0.0/x86_64-darwin14.0.0", "/Users/grant/.rbenv/versions/2.0.0-p481/lib/ruby/vendor_ruby", "/Users/grant/.rbenv/versions/2.0.0-p481/lib/ruby/2.0.0", "/Users/grant/.rbenv/versions/2.0.0-p481/lib/ruby/2.0.0/x86_64-darwin14.0.0"]
irb(main):005:0> spec.activate
=> true
irb(main):006:0>  $LOAD_PATH
=> ["/Users/grant/.rbenv/plugins/rbenv-gem-rehash", "/Users/grant/.rbenv/rbenv.d/exec/gem-rehash", "/Users/grant/.rbenv/versions/2.0.0-p481/lib/ruby/gems/2.0.0/gems/i18n-0.6.11/lib", "/Users/grant/.rbenv/versions/2.0.0-p481/lib/ruby/gems/2.0.0/gems/thread_safe-0.3.4/lib", "/Users/grant/.rbenv/versions/2.0.0-p481/lib/ruby/gems/2.0.0/gems/activesupport-4.1.8/lib", "/Users/grant/.rbenv/versions/2.0.0-p481/lib/ruby/site_ruby/2.0.0", "/Users/grant/.rbenv/versions/2.0.0-p481/lib/ruby/site_ruby/2.0.0/x86_64-darwin14.0.0", "/Users/grant/.rbenv/versions/2.0.0-p481/lib/ruby/site_ruby", "/Users/grant/.rbenv/versions/2.0.0-p481/lib/ruby/vendor_ruby/2.0.0", "/Users/grant/.rbenv/versions/2.0.0-p481/lib/ruby/vendor_ruby/2.0.0/x86_64-darwin14.0.0", "/Users/grant/.rbenv/versions/2.0.0-p481/lib/ruby/vendor_ruby", "/Users/grant/.rbenv/versions/2.0.0-p481/lib/ruby/2.0.0", "/Users/grant/.rbenv/versions/2.0.0-p481/lib/ruby/2.0.0/x86_64-darwin14.0.0"]

这时候active_support就在加载路径里面,你可以在任何地方使用它了。

总结

通过上面基本知道rubygems是怎么工作的,这样遇到相关的问题的时候,你可以根据上面的一些信息挖掘出更深层次的原因。

详解Gemfile

定义

Gemfile - 一个为了Ruby程序描述gem依赖的文件格式

概要

Gemfile里面描述了需要运行相关Ruby代码的gem依赖,一般是将此文件放到根目录。 例如Rails程序,是放在Rakefile的同一个目录下面。

语法

Gemfile相当于Ruby代码,并且有一些用于gem依赖的方法。

全局的Sources

一般写在Gemfile的头部,指定Gem的源地址,例如:

1
2
3
4
source "https://rubygems.org"

### 中国的用户可以使用这个地址
source('http://ruby.taobao.org')

一般情况下,不太建议指定多个source。如果一个gem在多个source里存在,bundler会打印一些警告出来。 如果单个gem使用不一样的source,建议使用:source选项或者source block(后面会详细介绍)。

认证

有些gem的source需要用户密码才能使用,可以通过使用bundle config命令或者直接写在source的地址里面 2种方式。

1
bundle config https://gems.example.com/ user:password

直接写在source的地址里面

1
source "https://user:password@gems.example.com"

如果2种方式都设定了的话,下面一种方式的优先级高于bundle config。

Ruby

如果你的应用需要指定一个特定的Ruby版本或者engine。

VERSION(必须指定)

你的应用需要的Ruby版本。如果你的应用使用了另外一个engine如JRuby或者Rubinius,得需要和这个版本保持兼容。

1
ruby "1.9.3"
ENGINE (:engine)

应用可以指定的Ruby Engine(如JRuby或者Rubinius)。如果engine指定了的话,对应的enine版本同样也必须指定。

ENGINE VERSION (:engine_version)

Ruby Engine的版本。这个必须与engine成对出现。

1
ruby "1.8.7", :engine => "jruby", :engine_version => "1.6.7"
PATCHLEVEL (:patchlevel)

指定Ruby对应版本的patchlevel。

1
ruby "2.0.0", :patchlevel => "247"

GEMS (#gem)

指定gem的方法,有下面的参数。

NAME (必须指定)

gem的名字

1
gem "nokogiri"
VERSION

gem的版本

1
2
gem "nokogiri", ">= 1.4.2"
gem "RedCloth", ">= 4.1.0", "< 4.2.0"

指定版本有好几种方式:

  • 固定版本
1
gem "nokogiri", "1.4.2"
  • 大于等于某个版本
1
gem "nokogiri", ">= 1.4.2"
  • 在指定版本范围内
1
gem "RedCloth", ">= 4.1.0", "< 4.2.0"
  • 约等于某个版本
1
gem "nokogiri", "~>1.4.2"

~>这个的版本有特别的意思:~> 2.0.3表示版本在>= 2.0.3< 2.1之间。 ~> 2.1表示版本在>= 2.1< 3.0之间。 ~> 2.2.beta会匹配这个版本2.2.beta.12

REQUIRE AS (:require)

用来指定require指定文件或者指定为false使应用不会自动require。

1
2
3
gem "redis", :require => ["redis/connection/hiredis", "redis"]
gem "webmock", :require => false
gem "debugger", :require => true
  • true 指应用自动require该gem。在使用该gem的地方无需自己手动require了。
  • false 应用不会自动require该gem。在使用该gem的地方需自己手动require。
  • 单个文件名或文件数组。一般gem的主文件名和gem名不一致的时候,需要在这里指定成文件名。

所以下面3个的requires其实是一样的。

1
2
3
gem "nokogiri"
gem "nokogiri", :require => "nokogiri"
gem "nokogiri", :require => true
GROUPS (:group or :groups)

每个gem可以指定在任何的一个或者多个group里面。如果gem没有指定group的话,默认是在default group里。

1
2
gem "rspec", :group => :test
gem "wirble", :groups => [:development, :test]

bundler有2个主要的方法Bundler.setup和Bundler.require可以访问group。

1
2
3
4
5
6
7
8
9
10
11
12
# setup adds gems to Ruby's load path
Bundler.setup                    # defaults to all groups
require "bundler/setup"          # same as Bundler.setup
Bundler.setup(:default)          # only set up the _default_ group
Bundler.setup(:test)             # only set up the _test_ group (but `not` _default_)
Bundler.setup(:default, :test)   # set up the _default_ and _test_ groups, but no others

# require requires all of the gems in the specified groups
Bundler.require                  # defaults to just the _default_ group
Bundler.require(:default)        # identical
Bundler.require(:default, :test) # requires the _default_ and _test_ groups
Bundler.require(:test)           # requires just the _test_ group

bundle install可以指定--without的参数来忽略某些group的gem的安装。

1
2
bundle install --without test
bundle install --without development test

如果你运行完bundle install --without test以后,bundle会记住你最后一次忽略test group的配置。 下次你如果直接运行bundle install,默认就会带--without test配置来安装gem。

并且如果调用bundler.setup没有传入参数,将会加载所有除了设定的--withoutgroup的gem。

另外运行bundle install将会下载所有的gem及其依赖的gem并且建立一个gem列表,这就意味着你不能在不同的group 加载不同版本的相同gem。

PLATFORMS (:platforms)

指定某个gem只能运行的平台。

有很多的Gemfile平台如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
ruby        C Ruby (MRI) or Rubinius, but NOT Windows
ruby_18     ruby AND version 1.8
ruby_19     ruby AND version 1.9
ruby_20     ruby AND version 2.0
ruby_21     ruby AND version 2.1
ruby_22     ruby AND version 2.2
mri         Same as ruby, but not Rubinius
mri_18      mri AND version 1.8
mri_19      mri AND version 1.9
mri_20      mri AND version 2.0
mri_21      mri AND version 2.1
mri_22      mri AND version 2.2
rbx         Same as ruby, but only Rubinius (not MRI)
jruby       JRuby
mswin       Windows
mingw       Windows 32 bit 'mingw32' platform (aka RubyInstaller)
mingw_18    mingw AND version 1.8
mingw_19    mingw AND version 1.9
mingw_20    mingw AND version 2.0
mingw_21    mingw AND version 2.1
mingw_22    mingw AND version 2.2
x64_mingw   Windows 64 bit 'mingw32' platform (aka RubyInstaller x64)
x64_mingw_20  x64_mingw AND version 2.0
x64_mingw_21  x64_mingw AND version 2.1
x64_mingw_22  x64_mingw AND version 2.2

和group差不多,你可以指定一个或多个。

1
2
3
gem "weakling",   :platforms => :jruby
gem "ruby-debug", :platforms => :mri_18
gem "nokogiri",   :platforms => [:mri_18, :jruby]
SOURCE (:source)

如果某个gem需要单独从另外的source去下载,可以通过指定这个参数实现。

1
gem "some_internal_gem", :source => "https://gems.example.com"

如果找不到,将会去global source那里去找。

GIT (:git)

你可以指定一个gem是从指定的git repo那里下载。这个repo可以是公开的 (http://github.com/rails/rails.git)也可以是私有(git@github.com:rails/rails.git) 的,如果是私有必须保证你的ssh配置有权限clone该repo。

1
gem "rails", :git => "git://github.com/rails/rails.git"

这个repo必须至少有一个文件在项目的根目录下,后缀名是.gemspec,这个文件里面包含的是 各个gem指定的信息。

如果这个repo没有.gemspec文件,bundler将会试着新建一个空的文件。

如果repo里面有.gemspec文件,那必须保证该文件里面的版本和gem指定的版本要一致。 不一致的话,bundle会有警告。

1
2
3
gem "rails", "2.3.8", :git => "git://github.com/rails/rails.git"
# bundle install will fail, because the .gemspec in the rails
# repository's master branch specifies version 3.0.0

如果.gemspec文件没有的话,gem的版本必须指定,bundle将会根据这个版本新建.gemspec文件。

git这个参数还有额外的参数:

  • branch, tag, 和 ref。 你只能指定其中一个参数。默认使用的是:branch => "master"
  • submodules。 如果指定:submodules => true的话,bundle会默认扩展这个repo里面的submodule。
GITHUB (:github)

如果git repo是github的公开项目的话你只需指定用户名项目名即可。如果项目名和用户名一样,那只需指定其中一个即可。

1
2
gem "rails", :github => "rails/rails"
gem "rails", :github => "rails"

和下面这个是一样的。

1
gem "rails", :git => "git://github.com/rails/rails.git"

也是可以指定分支的:

1
gem "rails", :github => "rails/rails", :branch => "branch_name"
PATH (:path)

你可以指定某个目录下的gem,基本是和git参数一样,也是需要项目里有.gemspec文件。 但需要注意的是与git不一样的是:bundle不会编译path方式指定的gem里面的c扩展。

1
gem "rails", :path => "vendor/rails"
SOURCE, GIT, PATH, GROUP 和 PLATFORMS 的block

就是把source,git,path,group和platform可以通过block分组设置了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
source "https://gems.example.com" do
  gem "some_internal_gem"
  gem "another_internal_gem"
end

git "git://github.com/rails/rails.git" do
  gem "activesupport"
  gem "actionpack"
end

platforms :ruby do
  gem "ruby-debug"
  gem "sqlite3"
end

group :development do
  gem "wirble"
  gem "faker"
end

需要的注意的是git,设置了branch等参数的话,里面所有的gem都是会继承的。

GEMSPEC (#gemspec)

用来指定查找gem里面的gemspec文件。 gemspec有:path, :name和:development_group参数,这些参数用来查找gemspec文件的。

SOURCE 优先级

bundle使用如下的source优先级:

  1. gem本身自己定义的:source, :path, 或者 :git参数
  2. 对于依赖的一些gem, :source, :path, 或者 :git里面对应的版本优先级高于rubygems.org
  3. 对于多个全局SOURCE的情况,将会是从最后加入的一个source开始搜索优先级最高。