Rails 源代码 100 天(3)

今天我们来探索一下content_tag方法。

这个方法很重要,因为 ActionView 中很多方法,比如我们之前提到的link_to,最终都会使用此方法来生成 html 元素。

在开始之前,我们可以想像,content_tag在执行过程中肯定是首先用字符串的形式,拼接起了一个 html 元素,比如'<a href="http://example.com"></a>',然后将其转化为页面上的 html。

了解 content_tag 的用法

从文档中可以看出content_tag(name, content_or_options_with_block = nil, options = nil, escape = true, &block)接受5个参数,其中最后一个是代码块。而第二个参数叫content_or_options_with_block,结合文档理解这个参数名的意思是:如果你传入了 block 那么这个参数被用作 options,反之则是 content。

那么下面举几个例子:

1
2
3
4
content_tag(:p, '1st line', class: 'my-class', id: 'my-id')
content_tag(:p, content_tag(:span, '2nd line'), class: 'my-class', id: 'my-id')
content_tag(:p, class: 'my-class', id: 'my-id') { some_code }
content_tag(:p, {class: 'my-class', id: 'my-id'}, nil, false) { some_code }

根据我们之前在了解link_to时得到的经验,content_tag方法也一定会根据是否传入了 code block 来决定是否转换传入的参数。从content_tag 的源代码可以看出:

1
2
3
4
5
6
7
8
def content_tag(name, content_or_options_with_block = nil, options = nil, escape = true, &block)
if block_given?
options = content_or_options_with_block if content_or_options_with_block.is_a?(Hash)
tag_builder.content_tag_string(name, capture(&block), options, escape)
else
tag_builder.content_tag_string(name, content_or_options_with_block, options, escape)
end
end

被转换的参数就是我们刚刚提到的第二个参数content_or_options_with_block,他会被传给原本的第三个参数options。这里有一个有意思的事情,Rails 不会处理escape参数的位置,也就是说,如果你想传入 escape,你需要把 escape 作为第 4 个参数,而不能作为第 3 个参数,然后等着 Rails 帮你挪到第 4 位。所以,就如上面的例子中的最后一个一样,你需要给用一个占位的值,作为第三个参数:

1
content_tag(:p, {class: 'my-class', id: 'my-id'}, nil, false) { some_code }

一旦这些参数被传入,content_tag做的事情其实很简单,把他们整理之后传入一个叫content_tag_string的方法。而这个新出现的方法在做的事情,就是我们在开始提到的,拼接一个可以被编译为 html 元素的 string。

在我们开始探索接下来的content_tag_string方法之前,让我们来看看content_tag方法是怎么整理参数的:

  1. 如果没有 code block,那么直接原样传入content_tag_string
  2. 如果有 code block,不仅将之前说的content_or_options_with_block传给options,并且把capture(&block)作为content_tag_string的第二个参数。

这里我们可以看看capture是什么:

1
2
3
4
5
6
7
def capture(*args)
value = nil
buffer = with_output_buffer { value = yield(*args) }
if (string = buffer.presence || value) && string.is_a?(String)
ERB::Util.html_escape string
end
end

按照文档的说明,这个方法将传入的页面模版转化为 String 对象,这样如果把 capture 的返回结果赋值给一个变量,它就可以用在页面的任何地方,而不用重新写 Template。比如这样的例子:

1
@greeting = capture {"Hello World at #{Time.now}"}

而后你就可以把@greeting用在页面的任何地方,比如:

1
2
3
4
<title><%= @greeting %></title>
<body>
<div><%= @greeting %></div>
</body>

实际上,文档中所说的这个 String 对象其实是个ActiveSupport::SafeBuffer对象,但其父类确实是String。这个对象是由 ERB::Util.html_escape 方法返回的。不过我们这里暂时不去探索 ERB::Util.html_escape了,但可以想像,其内部是在处理字符,转换一些编码(escape)。

content_tag_string方法

content_tag_string方法其实是TagBuilder这个类的一个实例方法,在content_tag中,首先利用 tag_builder 这个方法构造了一个TagBuilder的实例,而后调用content_tag_string方法

1
2
3
4
5
def content_tag_string(name, content, options, escape = true)
tag_options = tag_options(options, escape) if options
content = ERB::Util.unwrapped_html_escape(content) if escape
"<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name]}#{content}</#{name}>".html_safe
end

最后,这个方法把 tag_options 和其他字符拼在一起,形成一个新的 String 对象(其实还是ActiveSupport::SafeBuffer对象)。这个对象也就是content_tag方法的返回值。