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开始搜索优先级最高。

Rails中管理配置ENV环境变量一个小技巧

在我们Rails开发的过程中,经常会遇到一些服务提供了secret token或者密码之类的需要保密的信息,我们在程序读取他们的话,一般都是从ENV里面读取。这样是比较安全的解决方法。 但ENV里面的值如何可以方便统一管理和赋值呢?下面介绍一个方法:

新建一个YAML文件类似这样的:

1
2
3
4
5
6
7
8
9
# config/application.yml
defaults: &defaults
XXX_SECRET_KEY: XXXXXX

development:
<<: *defaults

test:
<<: *defaults

把下面代码加到config/application.rb里面去

1
2
3
4
5
6
7
if File.exists?(File.expand_path('../application.yml', __FILE__))
  config = YAML.load(File.read(File.expand_path('../application.yml', __FILE__)))
  config.merge! config.fetch(Rails.env, {})
  config.each do |key, value|
    ENV[key] ||= value.to_s unless value.kind_of? Hash
  end
end

这样config/application.rb就变成这样了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# config/application.rb
require File.expand_path('../boot', __FILE__)
require 'rails/all'
# Require the gems listed in Gemfile, including any gems# you've limited to :test, :development, or :production.
Bundler.require(:default, Rails.env)

if File.exists?(File.expand_path('../application.yml', __FILE__))
  config = YAML.load(File.read(File.expand_path('../application.yml', __FILE__)))
  config.merge! config.fetch(Rails.env, {})
  config.each do |key, value|
    ENV[key] ||= value.to_s unless value.kind_of? Hash
  end
end

module NewApp
  class Application < Rails::Application
    # ...  
  end
end

这样的话Rails启动的话会自动读取config/application.yml里面的值,然后把里面的key和value设置到ENV里面去, 无需你手动分别在各种不同的环境系统里面修改/etc/environment的值。

当然记住这个文件是不能push到代码仓库的,记得加到.gitignore里面去。

Rails中的Presenter

什么是Presenter

当你使用的models里面有很多view相关的逻辑的时候, 会觉得这块逻辑写在这个地方不太好。 很多人可能会把这些代码移到helper方法里面去,确实是可以的。但是当你view的逻辑复杂后, 可能你需要看看Presenter了。

Presenter是面向对象的方式实现view hepler。我就介绍下如何使用presenter重构helper 和model里面的一些方法。

重构views

例如我们需要显示一个博客文章的状态, 如果文章已经发表的话就显示发表的时间,否则显示为”Draft”,

1
2
3
4
5
class Post < ActiveRecord::Base
  def publication_status
    published_at || 'Draft'
  end
end

这时候看起来没有什么问题,如果需求改成希望把时间显示成xxx hours ago的话,这时候如果使用 helper里面的time_ago_in_words方法再方便不过了。但model里面不支持这个方法,所以我们移到 helper里面去。

1
2
3
4
5
6
7
8
9
module PostHelper
  def publication_status(post)
    if post.published_at
      time_ago_in_words(post.published_at)
    else
      'Draft'
    end
  end
end

这个方法确实解决了这个问题,但是helper有个缺点,把所有的helper方法都放到一个namespace下面了。 如果post有一个这样的方法,那该多方便。

创建第一个Presenter

我们来创建一个PostPresenter(app/presenter目录下面)类来实现上面的需求。

1
2
3
4
5
6
7
8
9
10
11
12
class PostPresenter < Struct.new(:post, :view)
  def publication_status
    if post.published_at?
      view.time_ago_in_words(post.published_at)
    else
      'Draft'
    end
  end
end

presenter = PostPresenter.new(@post, view_context)
presenter.publications_status

view_context是ActionView的一个实例,这样的话我们才能使用time_ago_in_words方法。 但是当我们想获取post的title的时候,这个persenter将无法获得。所以我们需要创建一个 BasePresenter来解决这个问题,所有的Presenter将会继承这个类。

1
2
3
4
5
6
7
8
9
10
class BasePresenter < SimpleDelegator
  def initialize(model, view)
    @model, @view = model, view
    super(@model)
  end

  def h
    @view
  end
end

通过继承SimpleDelegator类并且调用了super(@model)初始化方法,这个保证了如果我们调用的 方法在presenter里面不存在的话,就会调用model对应的方法。

并且定义了一个h方法来代替view。上面的ProjectPresenter将会重构成这样:

1
2
3
4
5
6
7
8
9
class PostPresenter < BasePresenter
  def publication_status
    if @model.published_at?
      h.time_ago_in_words(@model.published_at)
    else
      'Draft'
    end
  end
end

我们可以在controller里面初始化:

1
2
3
4
5
6
class PostsController < ApplicationController
  def show
    post = Post.find(params[:id])
    @post = PostPresenter.new(post, view_context)
  end
end

将presenter移出controller

上面的presenter在controller里面每次初始化感觉比较麻烦,现在我们通过定义一个helper方法, 来简化这个初始化。

1
2
3
4
5
6
7
module ApplicationHelper
  def present(model)
    klass = "#{model.class}Presenter".constantize
    presenter = klass.new(model, self)
    yield(presenter) if block_given?
  end
end

上面通过传的model来实例化其对应的Presenter,然后通过block的方式实现你需要的调用。

1
2
3
- present(@post) do |post|
  %h2= post.title
  %p= post.author

定制的Presenter

上面的presenter都是默认认为是modle的名字加上Presenter,如果post还有一个定制的 AdminPostPresenter该如何实现呢?其实很简单,present再加一个参数就搞定了,代码如下。

1
2
3
4
5
6
7
module ApplicationHelper
  def present(model, presenter_class=nil)
    klass = presenter_class || "#{model.class}Presenter".constantize
    presenter = klass.new(model, self)
    yield(presenter) if block_given?
  end
end

调用的地方写成present(@post, AdminPostPresenter)这样就可以了。

总结

使用Presenter极大的分离了model里面view相关的逻辑,也给测试带来了方便。也有类似的gem 例如draper实现了差不多的功能。如果你的view和model有相关的代码,可以考虑使用Presenter 来重构一下吧。

参考原文

http://nithinbekal.com/posts/rails-presenters/

为什么要写Git教程

其实市面上已经有很多Git教程了,我也看过好几本,但是说实话我都没有很完整看完一本, 并且有时看过之后由于没有实践,导致就忘记了。虽然在GitCafe工作一直接触Git知识, 但感觉自己还是没有达到精通的程度。

在网上看了Atlassian公司的Git英文教程还不错, 就萌生了通过自己翻译整个教程的过程,重新巩固下Git知识,这样也给自己一点压力去学的更好。

把完成此翻译作为我2015年的第一个目标,我希望在3个月内可以搞定。

数据缓存提升性能gem - Dalli介绍

Rails可能很多人诟病的就是性能问题,所以很多人想出来了改善方法,其中dalli这个gem就是通过memcache来改善的。

地址

https://github.com/mperham/dalli 使用这个gem必须保证安装了memcached

什么数据适合存入cache呢?

一般情况下,数据库里面不怎么改变的数据,并且使用的地方比较多的话是非常适合存入cache的。例如:一个国家的所有省份。

举个例子说明下:

1
2
3
4
5
6
7
class Grade < ActiveRecord::Base
  has_many :skills
end

class Skills < ActiveRecord::Base
  belongs_to :grade
end

如果想取得一个grade所有skills非常简单如下:

1
2
grade = Grade.first
skills = grade.skills

现在每次取一个grade的skills都会访问数据库,可以考虑把skill放到cache里面去了。

1
2
3
4
5
def cached_skills
  Rails.cache.fetch([self.class.name, id, :skills], expires_in: 240.hours) {
    skills.to_a
  }
end

所以我们取skills的话可以通过下面方式取出来

1
grade.cached_skills

这个过程是什么样的呢?当第一次访问的时候,会去访问数据库然后把数据存入到memcached里面去,下次访问的话就直接从cache里面取了,不再访问数据库了。

  • [self.class.name, id, :skills] 这是存储的key
  • cache将会在240小时后过期
  • 如果不转化成数组(to_a)直接将active-record的relations存储到cache的话,每次访问cache这个relations还是会查询数据库的,所以大家务必要注意。

让cache过期

如果某个grade的skills改变了,而grade又没有收到通知,这时候从cache中取出来的将是旧的数据,这个时候我们需要让cache里面这个grade过期或者删除掉

一种方法使用after_commit

在Skill的model里面加上

1
2
3
4
5
6
# in skill model
after_commit :flush_cache

def flush_cache
  Rails.cache.delete([self.grade.class.name, self.grade_id, :skills])
end

这个方法可以保证cache里面的数据都是最新的。

第二种方法是通过改变key和加入touch关键字

model定义如下:

1
2
3
4
5
6
7
class Grade < ActiveRecord::Base
  has_many :skills
end

class Skills < ActiveRecord::Base
  belongs_to :grade, touch: true
end

touch什么意思呢?如果skill更新的话,也会更新grade的updated_at字段

这时候将cached_skills方法修改成下面的:

1
2
3
4
5
def cached_skills
  Rails.cache.fetch([self.class.name, updated_at.to_i, :skills], expires_in: 240.hours) {
    skills.to_a
  }
end

这样的话如果skill发生改变的话,因为把updated_at作为key了,所以从cache里面取的永远是最新的skills。

但这种方法有如下缺点,使用的时候需要小心:

在这种情况下,如果topic改变也会更新grade的updated_at,这样的话,也会影响skills,cache的效果就不明显了。 上面的2种cache过期方法,大家根据实际情况自行选择。

总结

这个gem可以帮助我们减少很多数据库访问,极大的提高性能。大家使用的时候,务必注意cache里面数据保持更新的问题,不然引起很多因为旧数据引起的bug就不太好了。