🌷

Rails チュートリアル 8章

に公開

まえがき

Railsチュートリアル8章において押さえておいた方がいいポイントをまとめ、8章の学習内容を俯瞰し整理することを目的とする。


変更を加えたファイル

ファイル名 説明
routes.rb 新しくセッションコントローラのアクションについてのパスを追加
application_controller.rb 新たにSessionヘルパーを読み込むことで、全コントローラ、全ビューからヘルパー使用可能に
_header.html.erb ログイン時、未ログイン時のメニューを変更し、かつ"アカウント"メニューに関してはJSを適用しドロップダウンメニューとするため大改造を施した
application.html.erb JSを導入するためにhead部分にimportmapに関するタグを呼び出した
custom.scss デバイス幅によって適用されるかが決まるフッタースタイルやドロップダウンのスタイルなどを追加
importmap.rb カスタムJSコード群をRailsに認識させるため、ディレクトリのパスを追記
application.js ドロップダウンメニューのJSをインポートするために追記
user.rb 任意の文字列に対してハッシュ値を返すクラスメソッドを追記
users.yml テストに使うユーザ情報のハッシュを定義する。また、このファイルの名前(ハッシュ名)という方法でテストファイルにおいてハッシュを読み出すことが可能である。
test_helper.rb テスト用にテストセッションん上にユーザがいるか判断するメソッドis_logged_in?を追記
users_signup_test.rb テストセッションにおいてユーザがログインできたか確認するメソッドis_logged_in?呼び出しを追記

追加したファイル

ファイル名 作成方法 説明
sessions_controller_test.rb generate controllerで自動作成 デフォルト状態からGETリクエストにおいて指定するパスを名前付きルーティングに変更
sessions_controller.rb generate controllerで自動作成 セッション表示のためのnewアクション、セッション取得のためのcreateアクション、セッション終了(ログアウト)のためのdestroyアクションを定義した。
users_login_test.rb generate integration_testで自動作成 ログインにおける一環の操作をテストするファイルであり、今回は複雑化したため、うまくクラスを作成しながらまとめた。
sessions_helper.rb generate controllerで自動作成 ログイン中のユーザのid取得用のlog_inメソッド,ログイン中のユーザ情報を取得するためのcurrent_userメソッド,ユーザがログイン中かどうか論理値を返すlogged_in?メソッド,現在のユーザをログアウトさせるlog_outメソッドを追加した。
menu.js ディレクトリcustomと共に手動作成 ドロップダウンメニューを作成するために追加

JavaScriptで動的なコンテンツ作成(ドロップダウンメニュー)

実際にチュートリアルで出てくるJSのコードの意味がわからなかったので解説。
まず一番外側のこの構造について。

menu.js
document.addEventListener("turbo:load", function(){});
  • documentとは現在読み込んでいるページ全体のことを指すらしい
  • addEventListenerとは指定したイベントをトリガーに何かしらの操作を行う設定を書くための組み込み関数のこと。第1引数にイベント、第2引数に操作(関数)を記述する。
  • 第2引数のfunction(){}はJSで言うところの無名関数の扱い。
  • イベント"turbo:load"とはページが読み込まれるというイベントのことを指す

その内側についても見ていく。前後半でそれぞれhamburger,accountを似たような形で定義しているようである。よって大まかには前半の方だけ構造理解する。

menu.js
 let hamburger = document.querySelector("#hamburger");
     if(hamburger){
         hamburger.addEventListener("click",function(event){
             event.preventDefault();
             let menu = document.querySelector("#navbar-menu");
             menu.classList.toggle("collapse");
         });
     }

わかることは以下。

  • JSにおいて変数の定義はletを用いて行う。
  • querySelectorとは引数としてCSSセレクタ(タグ名、クラス名、id名)を指定することでマッチするセレクタを発見できた場合はその内容を返す。例えばhamburgerという名前のidを持つ<a>を見つけたらその要素を渡す。
  • JSでifを行う場合、判定対象の変数に何か値が入っていればtrue。つまり、nullの時のみfalseを返す。
  • イベント名:clickはその属性がクリックされるというイベントを指す。
  • eventとはイベントが発生した際にブラウザが自動で取得してくれる値であり、自身で定義する必要はない。clickというイベントが起きたのであれば、eventにはクリックされた要素などの情報が入る。
  • preventDefault();はこの要素が押された際のデフォルトのブラウザの動きを止めるためのコードである。例えば、この要素が<a href="#">であったなら押すとリンクへジャンプしてしまうが、今回みたいのは「押したな?」という事実のみであり、この操作を止めなければならない。
  • .classList.toggle();とは、引数に指定した名前のクラスが存在しなければ付け加えて、すれば削除するという機能を実現するメソッドである。

次に、繰り返しを避けてより簡単にしたバージョンのうち目新しい部分の解説。

menu.js
function addToggleListnener(selected_id, menu_id, toggle_class){
    let selected_element = document.querySelector( `#${selected_id}` );
    selected_element.addEventListener("click", function(event){
        event.preventDefault();
        let menu = document.querySelector( `#${menu_id}` );
        menu.classList.toggle(toggle_class);
    });
}

//クリックをリッスンするトグルリスナーを追加する
document.addEventListener("turbo:load", function(){
    addToggleListnener("hamburger", "navbar-menu", "collapse");
    addToggleListnener("account", "dropdown-menu", "active");
});

この中で目新しいのはquerySelector内の変数の書き方くらいであるが、これは

"#{変数名}" #Ruby
`${変数名}` #JavaScript

というだけであり、変数を展開可能な状態にしているだけである。

また、今回記述したJSコードは全ビューにおいて適用される。というのも、application.jsに記述された内容は全ビューに適用されることになるのだが、ここに今回記述したmenu.jsへのパスを追加したためである。


sessionって何??userとの扱いの差は??

sessionとuserの違いは、データに永続性があるかどうか。前者にはデータベースがなく、後者にはある。言い換えれば、前者はモデルを持たず、後者は持っている。postするときにparamsに含まれる情報の中のユーザ情報が入るハッシュの名前はモデルがあればそのモデルのインスタンス名が当てられるのだが、sessionはそれを持たないためsessionにおいてユーザー情報を取得するには何か名前を与える必要がある。例えば、このセッションの情報というのは、ログインの際にsubmitされるタイミングでparamsに保存されるのだが、いったいこの情報をどのコントローラのどのアクションに投げたらいいのかというのを決定するのが、以下のコードである。

new.html.erb
<%=form_with(url: login_path, scope: :session) do |f|%>

このコードのscopeの部分に命名をしてやる。また、用いるコントローラのパスをurlの部分で指定する。また、form_withメソッドは基本的にpostに対応しているため、特に、newとcreateどちらのアクションを呼べばいいのかで衝突が起こることはなく、createが選ばれる運びとなる。それ以外のフォームのコード記述はSignupの時のものと同じ。

コントローラーsessionで使うアクションとリクエスト

アクション名 HTTPリクエスト 役割
new GET 新しいセッションページの取得(ログイン)
create POST 新しいセッションの作成(ログイン)
destroy DELETE セッション削除(ログアウト)

flashの特性

flashは新しいリクエストが発されたときに自動で消えることになる。
すなわち!リクエストを発するか発さないかで正しくメッセージが消えるかどうかが変わる!同じように、あるURLへの移動を指示するようなコードであっても別の挙動をしうる。

  • redirect_to
    ブラウザに別のURLへの移動しろというレスポンスが返り、新しいHTTPリクエストとして解釈される
  • render
    同じリクエストのままレスポンスを返すため、新しいHTTPリクエストとしては解釈されず、flashの後renderしてもメッセージは消えてくれない。

flash vs flash.now

いつまで残る? 補足
flash 次のリクエストまで redirect_toで新しくHTTPリクエストが送られるなら基本こっちを使う。
flash.now 今のリクエストの中で renderを用いてリクエストなしのビューの再構成を行う際にはこっちでないとだめ。

ちなみにflashを設定する際のキーは自分で設定することができ、大体は以下のような感じなので、実際にこのキー値を用いてコードを書くこと自体は少ない。

flash_example
flash[:<自分で設定したキー>]= "Message"
flash_example
<% flash.each do |message_type, message| %>
.
.
.
<% end %>

Userクラス(model)にメソッドを追加

クラスメソッドとして追加したいので定義の仕方に注意。
以下のように書くとインスタンスメソッドとして定義されてしまい、User.funcのように用いることができなくなる!

user.rb
class User < ApplicatoinRecord
    def func()
    end
end

よってこのように定義することで初めてUser.funcのような形で呼び出せるクラスメソッドが定義できる。

user.rb
class User < ApplicatoinRecord
    def User.func()
    end
end

今回新たに追加したクラスメソッドは任意の文字列に対してハッシュ値を得たいというものであったため、いちいちインスタンスを生成せずとも適用できたほうが便利である。そのためクラスメソッドを選んだ形である。


他にしたこと

  • emailから取得してきた本物のパスワードとセッションで入力されたパスワードが一致するかauthenticateメソッドで確認することで、ログインの成功の可否を判断する
  • ログイン用のテストの流れは以下のような工程にした
    1. ログイン用のパス(sessions/new)を開く
    2. フォーム表示
    3. セッション用パスにpost
    4. フラッシュメッセージ確認
    5. 別ページに移動
    6. フラッシュメッセージ消失確認
  • sessionハッシュにログイン中のユーザのを記録するシンボル:user_idにそのユーザのidを取得、代入するlog_inメソッドをsessions_helperに追加。
  • セッション固定攻撃に備えて、reset_sessionメソッドはログイン前に必ず実行する
  • log_inメソッドが実行されていれば、sessionハッシュのシンボル:user_idに値が入っている。これを用いて現在ログイン中のユーザ情報を取得できるcurrent_userメソッドをsessions_helperに追加。
  • ユーザがログインしているか否かを理論値で返すlogged_in?メソッドを追記。中で、先ほど定義したcurrent_userメソッドを用いている。
  • 本来はgetリクエストしかできないリンク(HTMLタグで言うなら<a>,メソッドでいうなら<%=link_to%>)をdeleteとして用いるためにturbo-methodというものを用いる。これを用いることでdeleteリクエストを発して実際にsessions/destroyを実行できる。
  • Railsにおいてはimportmapというgemを用いることで、JSの導入してきた。具体的には、importmap.rbにJSコードがあるパスを指定することによる。
  • ハンバーガーメニュー用のマークアップを追加(挙動についてはJSで定義)
  • デバイス幅によって適用されるかが決まるフッタースタイルやドロップダウンのスタイルなどをscssに追加
  • テストにおいて、assert_select タグ名, パス, ココ!! 第3引数にcount: 0のように書くと、渡したパターンに一致するリンクが0個かどうか判定してくれる
  • 現在のクラスの1階層上のスーパークラスのsetupメソッドを呼び出すにはsuperと記述すればいい。また、サブクラスでは上位クラスの機能が自動でついていることを忘れずに!

Discussion