6
2

More than 3 years have passed since last update.

railsのメソッド"link_to"がどう定義されているのか読解してみた

Last updated at Posted at 2020-11-03

優れたコードを読むことがエンジニアとして成長する促進剤になる、と職場のエリートエンジニアの方に教わったので早速、実践!
ついでに共有してみます。

良質なコードを学ぶことで可読性が高く、バグの少ないコードを書けるようになるかと思いますので、ぜひ皆さんも参考にしてみてください。

手始めにlink_toから。

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

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

Ruby on Rails公式APIより、(ブロック=do~endを用いない場合は)nameに表示する文字列、optionsにはurl情報、html_optionsにはclassやrelなどHTMLのaタグに設定したい値が引き渡されます。

optionsとhtml_optionsはハッシュ形式で渡すことが前提となります。

早速、一行ずつ解釈を加えていきます。

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

引数の定義にある=nilの記述は、何も値が代入されていないとき、当該の引数にnilを代入する、という意味。
第4引数にある&blockは"ブロック"を受け取るための記述。
こちらの記事を読むと、ブロックとは引数として値ではなく、コードの塊を渡す際に用いるもので、引き渡されたコードの塊をブロックと呼ぶようです。

link_to do〜endと記述する際の〜がここに代入される。

次の行

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

block_given?(Rubyリファレンスマニュアル)
リファレンスマニュアルより、link_toメソッドにブロックが引き渡されているときにblock_given?はtrueとなりif文の中身が実行されます
つまり多重代入(Rubyリファレンス)される。

例えばblockを用いる場合はRuby on Rails公式APIより、

link_to(options = {}, html_options = {}) do
  # name
end

と記述されます
この場合はlink_to定義の1行目より、本来は表示名称であるnameにoptions(url情報)が、url情報であるoptionsにclassやidを定義するhtml_optionsが引き渡されてしまいます

そこでメソッド使用者の意図した通りに動くように、メソッドの定義通りに代入し直してやるのがこの行の役割。
(例えばurl情報が代入されてしまったnameの値を、optionsに代入しなおしている)

options ||= {}

||=は左辺がfalseもしくは定義されていないときに右辺の値を代入する、という記述。
1行目で、引数に値が渡されていないときにはoptionsにnilを代入するため、optionsの中身はnil、つまりfalseとなります
このときにoptinsには{}(空のハッシュ)が代入されます

html_options = convert_options_to_data_attributes(options, html_options)

convert_options_to_data_attributesというメソッドが呼び出されています。

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".freeze if link_to_remote_options?(options) || link_to_remote_options?(html_options)

    method = html_options.delete("method".freeze)

    add_method_to_attributes!(html_options, method) if method

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

convert_options_to_data_attributesの読解をしてみましょう。

html_optionsが存在する場合は3行目が実行されます。

html_options = html_options.stringify_keys

stringfy_keysはハッシュをシンボル形式から文字列形式に変換するrailsメソッド。

4行目

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

については要するにoptionsもしくはhtml_optionsにremote: trueという設定があれば文字列形式でキーdata-remoteに対してtrueが代入されます

なぜそうなるかは、railsメソッドのlink_to_remote_options?の内容を理解できればわかるかと思います。
else文の解釈のところで詳しく述べるので先にそちらを参照してみてください。

5行目

method = html_options.delete("method".freeze)

ハッシュに対するdeleteは引数の値と一致するキーを削除→削除したキーの値を返します
引数はmethod".freezeとなっています。
freezeによってdeleteの1つ目の機能である削除は行われず、2つ目の機能、すなわちmethodの値のみを返します。

したがって変数methodにhtml_optionsのキーmethodの値が代入されます。
(HTMLメソッドの設定値)
この書き方は面白いですね。

6行目

add_method_to_attributes!(html_options, method) if method

さらにmethodの値が設定されている場合はadd_method_to_attributes!メソッドを呼び出します。
!マークがついているので破壊的メソッド

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"] !~ /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

add_method_to_attributes!メソッドの読解

add_method_to_attributes!メソッド2行目。

ifの条件式はAND条件。
AND条件の1項目はmethod_not_get_method?メソッドの返り値。

このメソッドの定義式は省略しますが、その名の通り、get以外のメソッド(deleteとか)の場合にtrueを返します。
(リーダブルコードにも載っている命名の仕方ですね!)

AND条件の2項目はRubyリファレンスの!の項の3つ目
html_optionsのキーrelの値がnofollowでない場合はtrue。

add_method_to_attributes!メソッド3〜7行目

先述のAND条件が成立した場合に実行。

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

一気に行きましょう。
html_optionsのキーrelの値が空のときはnofollowという文字列を代入。
html_optionsのキーrelの値に何かしらの値が含まれているときは、その設定値に加えてnofollowという文字列を追加。

2行目のif文と組み合わせて解釈すると、get以外のメソッドのときはrelに値が設定されていようと、なかろうとrel="nofollow"となります。

ちなみにnofollowの意味は"リンクをクロールの対象から外す"だそうです。

なぜget以外のメソッドにnofollowを付与すると良いかはよく分からず、推察しかできていないので省略します。
またrel自体の使いみちについてはこちらを参照

add_method_to_attributes!メソッド8行目。

html_options["data-method"] = method

逆にgetメソッドと指定されている場合は、2行目のif文によってelse文に分岐。
上記のコードが実行されます。

html_optionsのキー"data-method"にHTTPメソッドの種類が代入されます。
"data-"となっているのでカスタム属性として定義されるということですね。

ようやくadd_method_to_attributes!メソッドの解釈が終わりました。

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".freeze if link_to_remote_options?(options) || link_to_remote_options?(html_options)

    method = html_options.delete("method".freeze)

    add_method_to_attributes!(html_options, method) if method

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

7行目。

4、6行目で2つの値を代入したハッシュhtml_optionsを返します。

8~9行目

html_optionsに値が渡されていない場合は

link_to_remote_options?(options) ? { "data-remote" => "true".freeze } : {}

が実行される。

三項演算子ですね。
まずはlink_to_remote_options?が呼び出される。

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

link_to_remote_options?メソッド

optionsの値がハッシュであるとき、if文の中身が実行されます

ただしremoteの設定があったとしてもremote: falseと定義されていた場合は、1項目の値はfalseとなります。
そこで2項目が呼び出され、今度はfreezeの記述がないためremoteの設定は削除されます

要するにlink_to_remote_options?により、remoteの指定がない OR falseの場合はfalseが、remoteの値がtrueの場合はtrueが返されます。

再度、convert_options_to_data_attributesメソッドのelse文の読解

三項演算子により、1項目でtrueが返される、すなわちremote: trueの設定がされている場合は、シンボル"data-remote" => trueがconvert_options_to_data_attributesの返り値となります。

convert_options_to_data_attributes内のif文のいずれであってもhtml_optionsのキーdata-remoteの値が設定される理由としては、link_toメソッドの引数、optionsとhtml_optionsいずれにremoteの設定値が渡された場合でも、カスタム属性remoteの値をhtml表示してやるためと思われます。

link_toへの値の引き渡しの書き方パターンについてはここでは省略します。

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

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

4行目までで、html_optionsの値が整形されました。

次は5行目。

url = url_for(options)

url_forメソッドが呼び出されます。

def url_for(options)
    if options[:only_path]
      path_for options
    else
      full_url_for options
    end
end

path_forが相対パスを出力するメソッド、full_url_forが絶対パスを出力するメソッド。
optionsのキーonly_pathの値がtrueの時に相対パスを出力する。

link_toの6行目

html_options["href"] ||= url

先程、生成したパスをhtml_optionsのキーhrefに代入。
ここからわかることは、link_toの第三因数にhrefをキーとしてやれば強制的にURLを指定することもできるということですね。

使いどきがあるかどうかはともかく…

最後7行目

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

[content_tagはviewヘルパー]。(https://apidock.com/rails/ActionView/Helpers/TagHelper/content_tag)
htmlタグを出力します。

第一引数がhtmlタグの種類、この場合はですね。

第二引数は表示する文字列。
この場合、仮にnameが存在しない場合は自動的にURLが文字列として表示されます。

html_optionsにハッシュ形式でclass: "XXX"などと渡すとtextのように表示してくれます。

また&blockと書いてあることからわかるように、link_toにブロックが渡されていた場合(link_to~do ~endという形式で書かれていた場合)、content_tagにそのままブロックが引き渡されます。

以上で、link_toの読解終わりです。

大まかな流れ

①代入された引数の順番を整える
②urlやmethodなどの値を整形
③content_tagで表示

以上です。

今回チャレンジしてみた感想

一点目
そもそもメソッドの定義を知ることで、思った通りの出力を出せるようになりそうですね。
例えば、ビューヘルパーであればトライアンドエラーでコードを書く⇔ビルドを繰り返して思った表示を出力していた労力を抑えることができます。

二点目
||=や&blockといった書き方については見かけていたものの、なくてもなんとかコードは書けるので意味を理解しておらず良いきっかけになりました。

例えば||=だと、もし変数が未定義またはnilだったら代入、という意味ですが、知らずに書くならばif文を使って複雑な書き方をしてしまうところを、1行でシンプルに表すことができます。

最初に述べた通り、良質でスマートなコードの書き方を学ぶことでメンテナンス性が高く、エラーも起こりづらいコードを書けるようになるのではないでしょうか?


今後も継続して学び続けようと思います!

6
2
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
6
2