Rails 源代码 100 天(1)

基于我之前的一些思考,我现在觉得了解 Rails 源代码是一个应该上马的项目了。如果你也有看 Rails 源代码的需求,可以 Follow 这个项目。

当我有了这个需求之后,我就在想自己要如何开始呢?有这样几点思考和大家分享:

  1. 越早开始越好,如果我从100天(不过是3个月的时间)开始,现在这个项目已经完成了。
  2. 从相对独立、短小的 helper methods 开始。如果我从 ActiveRecord 中的某个方法开始,那么这个方法通过一层一层的调用别的方法,可以整个 Active Record gem 都被调用了一遍1
  3. 了解源代码也是学习 Ruby 代码(如何组织代码,如何使用 Ruby 语法)的好方式。
  4. 解说完代码之后提炼一下我们能从中学到什么。

话不多说,那我们就开始吧。我们先从一个非常常用的 helper method —— link_to 开始,来找找感觉。

先写几个link_to的例子:

1
2
3
4
5
6
7
8
9
10
link_to '1st link', 'https://example.com'
link_to 'https://google.com' do
'<span>2nd click</span>'
end
link_to '3rd link', products_path
link_to '4th link', controller: 'products'
link_to '5th link', products_path, class: 'my-class'
link_to '6th link', {controller: 'products'}, class: 'my-class', id: 'my-id'
link_to '7th link', product_path(1), method: :delete, data: {confirm: 'Are you ok?'}
link_to '8th link', products_path, target: '_blank', rel: 'nofollow'

如果看不过来,可以不用全看,但有个问题值得想一想:一个方法的参数结构为什么可以这么多样?

我们可以通过link_to 的源代码来解释这一点。为了方便解说,我们把代码复制过来:

1
2
3
4
5
6
7
8
9
10
11
12
# rails/actionview/lib/action_view/helpers/url_helper.rb
def link_to(name = nil, options = nil, html_options = nil, &block)
html_options, options, name = options, name, block if block_given?
options ||= {}

html_options = convert_options_to_data_attributes(options, html_options)

url = url_for(options)
html_options["href".freeze] ||= url

content_tag("a".freeze, name || url, html_options, &block)
end

这个方法接受4个参数,每种参数都不是必须的。

从方法内部的第一行可以看出,如果有block,参数的次序就要轮转一下。所以,在上面举例过程中的2nd link中用括号传入的第一个参数的参数名其实叫name,只不过在link_to方法的内部,通过这行代码被赋值给了options:

html_options, options, name = options, name, block if block_given?

这里有一个题外的补充,你可以用 block,也可以用 yield 来代表传入的代码块,但两者有不同的效果,简单来说,block 代表的是个 Proc 对象,而yield的使用会直接对 block 进行 call。如果你不在定义方法时明确地给出block这个参数(加一个 &是为了更明确的表明那是个 code block),那么你就只能使用 yield。另外,在某些 ruby style guide 中,使用 block 会建议改为 yield。当然,block_given? 对于两者都是通用的。

在接下来的这这行代码:

1
html_options = convert_options_to_data_attributes(options, html_options)

Rails 的工作是整理出相关的字段,把那些原本应该是 options 的,但是为了传参方便被归为了 html_options 的字段拿掉,比如method字段。

再接下来是生成 url 字符串的过程,并且用将所有整理好的参数传入content_tag方法来生成<a>便签:

1
2
3
4
url = url_for(options)
html_options["href".freeze] ||= url

content_tag("a".freeze, name || url, html_options, &block)

这具体是怎么生成的,以及上一段中 Rails 封装的方法convert_options_to_data_attributes到底在做什么,我们在下一节继续探索。与此同时还有一个问题值得我们关注,为什么 Rails 对一些特定的字段要freeze,比如"a".freeze"href.freeze"

1. 这是我从一个 stack overflow 回答中得到的提示