1
0

More than 1 year has passed since last update.

link_toのソースコード読解

Posted at

ネットでオープンソースを読解することで良い勉強になるということを見かけたので、Railsでも代表的なメソッドであるlink_toのソースコードを読解してみようとおもいます。そして最後のほうに、具体例をあげてコードを追ってみようとおもいます。
事前に申し上げておきますが、今回の記事はかなり重厚になることが予想されます。また、privateメソッドが多いため読解するメソッドがコロコロ変わりますのであしからず。

それでは実際のソースコードを以下にペーストします。

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_target(name, options)
  html_options["href"] ||= url

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

では実際にコード読解をしていきましょう

def link_to(name = nil, options = nil, html_options = nil, &block)

引数にはname,options,html_options, &blockが指定されています。第一~三引数までは初期値としてnilが入っています。そのため、html_optionsに値が入ってこなくてもhtml_optionsはnilとして扱われるんですね。

第四引数はブロックです。以前の記事でも書きましたが、blockはdo~endで記述されるコードの塊です。
<%= link_to ~~~ do %>このような書き方ができるのは第四引数でブロックを指定しているからなんです。ブロックは、値ではなく一連の処理の塊です。

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

block_given?というメソッドはrubyのメソッドです。blockがもし存在すれば、値を入れ直すんですね。
ifより前の処理は、多重代入です。こちらの記事を参考にしてみてください。

options ||= {}

これは自己代入というものです。左辺がnilもしくはfalseだったら指定した値を入れてくださいね、という処理です。
今回でいえば、optionsがnilもしくはfalseだったら空のハッシュを入れてくださいねというものです。
詳しく知りたい方は、こちらの記事を参考にしてください

html_options = convert_options_to_data_attributes(options, html_options)

なにやら、見慣れないメソッドであるconvert_options_to_data_attributesが出てきました。
このメソッドは、モジュール内のprivateメソッドです。

この処理は以下のような内容です。

def convert_options_to_data_attributes(options, html_options)
  if html_options
    html_options = html_options.stringify_keys
    html_options["data-remote"] = "true" if link_to_remote_options?(options) || link_to_remote_options?(html_options)

    method = html_options.delete("method")

    add_method_to_attributes!(html_options, method) if method

    html_options
  else
    link_to_remote_options?(options) ? { "data-remote" => "true" } : {}
  end
end

3行目

html_options = html_options.stringify_keys

html_optionsという変数に、引数のhtml_optionsにstringify_keysメソッドを適用しています。このメソッドはハッシュのキーをシンボルから文字列に変えるメソッドです。

4行目

html_options["data-remote"] = "true" if link_to_remote_options?(options) || link_to_remote_options?(html_options)

このコードを理解するには、

def link_to_remote_options?(options)
  if options.is_a?(Hash)
    options.delete("remote") || options.delete(:remote)
  end
end

引数であるoptionsがHashであるかどうかを調べ、Hashであればremoteというkeyを削除して、そのkeyに紐づいた値を返すのがlink_to_remote_options?(options)です。

4行目のコードに戻ります。

html_options["data-remote"] = "true" if link_to_remote_options?(options) || link_to_remote_options?(html_options)

引数であるoptionsがHashであるかどうかを調べ、Hashであればremoteというkeyを削除して、そのkeyに紐づいた値を返している、つまりtrueであるなら、{data-remote: "true"}になるということです。

5行目

method = html_options.delete("method")

おそらく

method = options.delete("method")

の間違いであるとおもいます。以降はこちらのコードをもとに解説していきます

methodという変数に、optionsに"method"というkeyがあれば、keyである"method"を削除してそれに関連するvalueを返します

6行目

add_method_to_attributes!(html_options, method) if method

methodという変数(optionsに"method"というkeyがあれば、keyである"method"を削除してそれに関連するvalueを返す)であるなら、add_method_to_attributes!が適用される。add_method_to_attributes!は以下のようなメソッドです。

def add_method_to_attributes!(html_options, method)
  if method_not_get_method?(method) && !html_options["rel"]&.match?(/nofollow/)
    if html_options["rel"].blank?
      html_options["rel"] = "nofollow"
    else
      html_options["rel"] = "#{html_options["rel"]} nofollow"
    end
  end
  html_options["data-method"] = method
end

このメソッドを理解するには、method_not_get_method?を理解しないといけません。そのメソッドが以下の通りです。

STRINGIFIED_COMMON_METHODS = {
  get:    "get",
  delete: "delete",
  patch:  "patch",
  post:   "post",
  put:    "put",
}.freeze

def method_not_get_method?(method)
  return false unless method
  (STRINGIFIED_COMMON_METHODS[method] || method.to_s.downcase) != "get"
end

このメソッドを読解していきます。
methodという変数(optionsに"method"というkeyがあれば、keyである"method"を削除してそれに関連するvalueを返す)が偽の値だったらfalseを返す。

STRINGIFIED_COMMON_METHODS[method]は、keyであるmethodに対応したvalueを返します。
そしてmethodという変数を文字列化しそれを小文字化する。これらがgetを含んでいない値を返す。それがmethod_not_get_method?メソッドです。
add_method_to_attributes!メソッドに戻ります。

def add_method_to_attributes!(html_options, method)
  if method_not_get_method?(method) && !html_options["rel"]&.match?(/nofollow/)
    if html_options["rel"].blank?
      html_options["rel"] = "nofollow"
    else
      html_options["rel"] = "#{html_options["rel"]} nofollow"
    end
  end
  html_options["data-method"] = method
end

2行目から見ていきます。

if method_not_get_method?(method) && !html_options["rel"]&.match?(/nofollow/)

先ほど見たようにmethod_not_get_method?(method)は、getを含んでいない値を返すメソッドです。
!html_options["rel"]&.match?(/nofollow/)ではhtml_optionsの中のrelというkeyに対応するvalueがあり、nofollowという文字列を含んでいればtrueを返し、なければnilを返します。ぼっち演算子が使われていますからね。
そして文頭で「!」があるのでhtml_optionsにrelとnofollowという文字列がなければtrueを返し、あればnilもしくはfalseを返します。
つまり、このコードでは「methodにgetがなくて、かつhtml_optionsにrelに対応するvalueとnofollowという文字列がなければ」という条件文です。

    if html_options["rel"].blank?
      html_options["rel"] = "nofollow"
    else
      html_options["rel"] = "#{html_options["rel"]} nofollow"
    end
  end
  html_options["data-method"] = method
end

html_optionsのrelというkeyに対応したvalueが空だったら、keyに対応したvalueにnofollowを格納する
html_optionsのrelというkeyに対応したvalueが空じゃなかったら、keyに対応したvalueとnofollowという文字列を付け加えます。

methodがgetで、かつhtml_optionsにrelに対応するvalueとnofollowという文字列があれば、
html_optionsのdata-methodにgetメソッドが適用されます。
nofollowを指定することで、指定されたリンクをgoogle検索エンジンがクローリングできないよう明示的に示すためのものがnofollow属性です。

convert_options_to_data_attributesのコードへ戻ってきました

def convert_options_to_data_attributes(options, html_options)
  if html_options
    html_options = html_options.stringify_keys
    html_options["data-remote"] = "true" if link_to_remote_options?(options) || link_to_remote_options?(html_options)

    method = html_options.delete("method")

    add_method_to_attributes!(html_options, method) if method

    html_options
  else
    link_to_remote_options?(options) ? { "data-remote" => "true" } : {}
  end
end

このコードでは6行目のコードでは、これまで述べてきたようにhtml_optionsが処理されて、その処理を返します。

最後に理解しなければいけないプライベートメソッドであるurl_targetメソッドがあります。
そのコードがこちらです。

def url_target(name, options)
  if name.respond_to?(:model_name) && options.empty?
    url_for(name)
  else
    url_for(options)
  end
end

引数であるnameが指定したメソッドを呼び出せて、optionsが空であるなら、nameをもとにしたURLが作成される。
引数であるnameが指定したメソッドを呼び出せないのなら、optionsをもとにしたURLが作成される。

link_toの読解へ

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_target(name, options)
  html_options["href"] ||= url

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

5行目urlという変数にurl_targetで作成されたURLが格納されます。
そのurlという変数は、html_options["href"]のvalueが空であったら、そのURLが代入されます。

content_tagメソッドを適用して、HTML形式のリンクが作成されています。

次の章でまとめます。

link_toのソースコードのまとめ

再掲

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_target(name, options)
  html_options["href"] ||= url

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

2行目でブロックがあれば引数に値を代入し直す。
3行目では、optionsの値がnilがfalseであればハッシュを変数に入れる
4行目では、html_optionsが条件によって処理される
5行目では、引数nameと引数optionsのどちらかに合致したURLが生成される
最終行では、content_tagメソッドを使って、タグを動的に生成してくれています。

以上です。
長かったですし、あっちこっち飛んでしまったので理解しにくかったかもしれません。
でも、あらゆる知識をしれたり、link_toの内部の挙動が知れて楽しかったのでこれからも続けていきたいと思います。
何か間違いがございましたら、ご教示いただけますと幸いです。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0