前編(翻訳)|TechRacho(テックラッチョ)〜エンジニアの「?」を「!」に〜|BPS株式会社エンジニアの「?」を「!」に。原著者の許諾を得て翻訳・公開いたします。domagude/collabfieldRails5中級チュートリアルはセットアップが短めで、RDBMSにはPostgreSQL、テストにはRSpecを用います。原文が非常に長いので分割します。章ごとのリンクは順次追加します。Railsチュートリアル(https://railstutorial.jp/)(Railsチュートリアル)とは著者も対象読者も異なります。この少し前あたりの手順から、visit_single_post_spec.rbが失敗しています。翻訳中の検証ではscenarioをxscenarioに変えてひとまずペンディングしています。お気づきの点がありましたら@hachi8833までお知らせください。conversationは原則「チャット」と訳しています。このセクションの目標は、2人のユーザーが非公開で会話できるチャット機能の作成です。新しいブランチを切ります。private_conversationモデルを名前空間化するまず、必要なモデルをいくつか定義しましょう。さしあたって2つの異なるモデルが必要です。1つは非公開チャット用、もう1つはプライベートメッセージ用です。モデル名をPrivateConversationやPrivateMessageとすることも一応可能ですが、すぐに小さな問題に突き当たるでしょう。すべてがうまく動いていても、modelsディレクトリの下に同じようなプレフィックスを持つモデル名がいくつもできてしまうところを想像してみてください。間もなくこのディレクトリの管理がつらくてたまらなくなるでしょう。ディレクトリ内がカオスになるのを避けるために、名前空間化を使います。名前空間化するとどうなるかを見ていきましょう。非公開チャットに使うモデルを素直に命名すればPrivateConversationとなり、モデルのファイルはprivate_conversation.rbで、modelディレクトリに置かれます。models/private_conversation.rbこれを名前空間化したものはPrivate::Conversationになります。ファイル名はconversation.rbで、models/privateディレクトリに置かれます。models/private/conversation.rbこれのどこが便利なのかおわかりでしょうか?privateプレフィックスを持つファイルはすべてprivateディレクトリに保存されます。モデルをメインのmodelsディレクトリの下にベタッと並べると読むのがつらくなります。こんなとき、Railsはいつものように開発プロセスを楽しくしてくれます。Railsでは、モデルを保存するディレクトリを指定して、名前空間化されたモデルを作成することができます。名前空間化されたPrivate::Conversationモデルを作成するには、以下のコマンドを実行します。private/conversationPrivate::Messageモデルも同様に作成します。private/messagemodelsディレクトリを見てみると、その下にprivate.rbファイルができているのがわかります。これによってデータベースのテーブル名にプレフィックスを付けることが必須になり、モデルが認識されるようになります。個人的には、こうしたファイルをmodelsディレクトリに置いておくのは好きではなく、モデル自身の内部でテーブル名を指定する方が好きです。=でテーブル名を文字列で指定しなくてはなりません。私と同じようにデータベースのテーブル名をこの方法で指定すると、このモデルは次のようになります(Gist、Gist)。'private_messages'endこれでmodelsディレクトリの下のprivate.rbファイルは不要になったので、削除して構いません。1人のユーザーは非公開チャットを複数行え、チャットには多数のメッセージが含まれます。この関連付けをモデル内で定義しましょう(Gist、Gist、Gist)。'Private::Conversation'...上のclass_nameメソッドは、関連付けられたモデルの名前を定義するのに使われます。することで関連付けに独自の名前が使えるようになり、名前空間化されたモデルであることが認識されます。class_nameメソッドのもうひとつの使い方は「自分自身へのリレーション」の作成です。これは、何らかの階層的な構造を作成して同じモデルのデータを差別化したいときに便利です。foreign_keyは、データベーステーブル内の関連付けカラム名を指定するのに使います。テーブル内のカラムはbelongs_to関連付け側でのみ作成されますが、このカラムを認識させるために、で2つのモデルの同じ値でforeign_keyを定義しました。非公開チャットは2人のユーザー間で行えるようにします。ここでは2人のユーザーをそれぞれsenderとrecipientとします。user1やuser2のような名前にしようと思えばできますが、2人のどちらが最初にチャットを開始したかがわかると何かと便利なので、ここではsenderがチャットの作成者となります。マイグレーションファイルでデータのテーブルを定義します(Gist)。endendprivate_conversationsにはユーザーのidを保存することになります。ユーザーidは、belongs_to関連付けやhas_many関連付けが機能するのに必要ですし、2人のユーザー間のチャットを作成するのにももちろん必要です(Gist)。endendメッセージの内容はbodyデータカラムに保存されることになります。2つのモデル間の関連付けを機能させるためのインデックスやidカラムを追加する代わりに、ここではreferenceメソッドで実装をシンプルにしました。マイグレーションファイルを実行して、developmentデータベースの内部にテーブルを作成します。db:migrate変更をcommitします。tables"「非リアルタイム」チャットウィンドウ非公開チャットのデータを保存する場所ができましたが、これでおしまいではありません。次はどこから手を付けるべきでしょうか?前のセクションでご説明したように、私は機能の表示を先に作成して、それからそれを動かすロジックを書く方法が好みです。私にとっては、動かしたい画面表示が先にある方が達成すべき作業がはっきりするからです。ユーザーインターフェイスができていれば、これこれこういう操作を行ったときにどう動くべきかは画面を見ればわかるので、それを細かな手順に分割するのは簡単です。形になっていないものを相手にプログラミングする方が面倒だと思います。非公開チャットのユーザーインターフェイスを作るためにPrivate::Conversationsコントローラを作成します。先ほどもアプリで名前空間化を行いましたので、関連するパーツもすべて同じように名前空間化したいと思います。することで、ソースコードを直感的に眺めやすくなり、理解もしやすくなります。private/conversationsRailsのジェネレータはなかなかカワイイやつです。名前空間化されたモデルや名前空間化されたビューを生成してくれるので、即開発に取りかかれます。新しいチャットを作成する新しいチャットを開始する何らかの方法が必要です。このアプリでは、自分と好みの似通った人と会話したいと考えるのが自然でしょう。この機能を配置するのに便利な場所は、単一の投稿ページの内部です。posts/show.html.erbテンプレートの中に、チャットを開始するフォームをひとつ作成します。%>の下に以下を追加します(Gist)。%>...このヘルパーメソッドをposts_helper.rbで定義します(Gist)。end...このヘルパーメソッドのspecを作成します。endend...showディレクトリを作成して、対応するパーシャルファイルを作成します(Gist、Gist)。%></div>posts_helper.rbファイルでleave_message_partial_pathヘルパーメソッドを定義します(Gist)。endendヘルパーメソッドのspecを作成します(Gist)。endend...今だけ、PostsControllerに@message_has_been_sentインスタンス変数を定義することにします。この変数は、ユーザーへの最初のメッセージが送信されたかどうかを決定します。contact_userを作成し、leave_message_partial_pathヘルパーメソッドに対応するパーシャルファイルをその下に作成します(Gist、Gist)。%>今度はPostsControllerのshowアクションを設定しましょう。アクション内に以下を追加します(Gist)。conversation_exist?end...このコントローラのprivateスコープでconversation_exist?メソッドを定義します(Gist)。@post.user.id).present?end...このbetween_usersメソッドは、2人のユーザー間の非公開チャットの存在を問い合わせます。これをPrivate::Conversationモデルでスコープとして定義しましょう(Gist)。)end...このスコープが機能しているかどうかをテストしなければなりません。テストデータベース内にサンプルが必要なので、specを書く前にprivate_conversationファクトリーを定義しておきましょう(Gist)。endendファクトリーをネストさせることで、その親の設定を使ってファクトリーを作成してからそれを変更できるようになります。private_conversation_with_messagesファクトリーでメッセージを作成するので、private_messageファクトリーの定義も必要です(Gist)。endend準備がすべて整いましたので、between_usersスコープをspecでテストします(Gist)。endend...Private::Conversationsコントローラでcreateアクションを定義します(Gist)。endend...ここでは、投稿の著者と現在のユーザーの間でのチャットを作成しています。問題がなければ、アプリで現在のユーザーが書いたメッセージが作成され、対応するJavaScriptパーシャルをレンダリングすることで画面に表示されるようになります。そのためのパーシャルを作成します(Gist、Gist)。sent</div>');Private::ConversationsコントローラとPrivate::Messagesコントローラへのルーティングを作成します(Gist)。:create]end...今はまだアクションが少ないので、このような場合はonlyメソッドで書くのが便利です。namespaceメソッドを使うと、名前空間化されたコントローラへのルーティングを簡単に作成できます。specでテストします(Gist)。endend変更をcommitします。form"branch_page.scssファイルにCSSを追加して、フォームのスタイルを少し変更します(Gist)。white;}...この単一投稿ページを表示すると、以下のようにフォームが表示されるはずです。メッセージを投稿の著者に送信すると、フォームは消えます。投稿の著者と既にやりとりしたことがある場合は、以下のように表示されます。変更をcommitします。form"チャットウィンドウを表示する上ではメッセージを1件送信して新しいチャットを作成しました。今はこれ以外に何もできない状態なのでこの機能は何の役にも立ちません。メッセージを読み書きできるチャットウィンドウが必要です。開いているチャットのidは「セッション」の内部に保存されます。これによって、ユーザーがチャットを終了するかセッションが死ぬまでアプリのチャットを継続できるようになります。already_added?を追加してください。次にこのメソッドをprivateスコープで定義します(Gist)。@conversation.idendこれによってチャットのidがセッションに保存されます。チャットidがセッションに追加されていないかどうかを確認するalready_added?メソッドをprivateに配置します(Gist)。session[:private_conversations].include?(@conversation.id)end最後に、ビュー内でチャットにアクセスできる必要があるので、createアクション内のconversation変数(3か所)を@conversationインスタンス変数に書き換えてください。原文にありませんが、変更後のPrivate::ConversationsControllerコントローラを以下に示します。endendチャットウィンドウのテンプレート作成の準備が整いましたので、ウィンドウのパーシャルを作成します(Gist)。-->private_conv_recipientを用いてチャットのrecipient(受け側)を取得しているので、Private::ConversationsHelperでヘルパーメソッドを定義します(Gist)。conversation.opposed_user(current_user)end...opposed_userメソッドが使われているので、Private::Conversationモデルでこのメソッドを定義します(Gist)。recipientend...このメソッドは、非公開チャットの相手ユーザーを返します。このメソッドが正しく機能していることをspecで確認しましょう(Gist)。endend...次に、足りないパーシャルを_conversation.html.erbファイルで作成します(Gist、Gist)。</ul></div>load_private_messagesヘルパーメソッドをPrivate::ConversationsHelperに定義します(Gist)。endend...上はそれまでのメッセージを読み込むリンクを追加します。これに対応するパーシャルファイルをmessages_listディレクトリの下に作成します(Gist)。%>このメソッドがすべて問題なく動くことを確認するspecを書くのもお忘れなく(Gist)。endend...チャットのウィンドウはアプリ全体でレンダリングすることになるので、Private::ConversationsHelperヘルパーメソッドにアクセスできる必要があります。原文から脱落していたコードを補いました)。[]end...続いて、チャットの新しいメッセージフォームで使うパーシャルファイルがまだないので、作成します(Gist)。send-message"></form>このフォームの機能は、もう少し後で動くようにする予定です。それでは、ユーザーが個別の投稿からメッセージを1件送信したら、アプリのチャットウィンドウで表示されるようにする機能を作成しましょう。_success.js.erbファイルを開きます。posts/show/contact_user/message_form/_success.js.erb末尾に以下を追加します。%>このパーシャルファイルの目的は、アプリにチャットウィンドウを追加することです。そのためのパーシャルファイルを定義します(Gist)。すべてのチャットウィンドウを再配置するpositionChatWindows();このコールバック用パーシャルファイルは、今後さまざまなシナリオで再利用することになります。同じウィンドウを何度もレンダリングするのを避けるため、レンダリング前にウィンドウが既にあるかどうかをチェックし、それからウィンドウを拡大してメッセージフォームに自動的にフォーカスを移動します。ファイル末尾にあるpositionChatWindows()関数は、チャットウィンドウの位置がすべて適切な場所に配置されるようにするために呼び出されます。配置を修正しないと、複数のウィンドウが同じ場所に表示されてしまい、使い物にならなくなります。それでは、assetsディレクトリの下に、チャットウィンドウの表示や配置を扱うファイルを作成しましょう(Gist)。gemを使う方法があります。本来このgemはサーバーサイドからJavaScriptにデータを送信するためのものですが、アプリ全体を通してJavaScriptの変数をトラッキングするのにも便利であることに気づきました。このgemの指示を読んでインストールとセットアップを行います。installを実行します。application.html.erbのheadタグにも追記が必要との情報をいただきました。ありがとうございます。'gon'https://twitter.com/eighchaaan/status/980948611553832960viewportの幅はイベントリスナーでトラッキングします。チャットがviewportの左側に近づくと、チャットは非表示になります。非表示のチャットウィンドウを表示するのに十分な空きスペースができると、ふたたびチャットウィンドウを表示します。ページが表示されると、再配置や表示/非表示を行う関数を呼び出して、すべてのチャットウィンドウが正しい位置に表示されるようにします。ここでは、Bootstrapのpanelコンポーネントを用いて、チャットウィンドウの展開/折りたたみを簡単に行えるようにします。ウィンドウはデフォルトでは折りたたまれていて操作できないので、表示/非表示をトグルできるようにするために、javascriptsディレクトリの下に以下のtoggle_window.jsファイルを作成します(Gist)。});});conversation_window.scssファイルを作成します。assets/stylesheets/partials/conversation_window.scssチャットウィンドウのスタイルを設定するCSSを追加します(Gist)。none;}これまでどのHTMLファイルにも定義されていないクラスがこのCSSでいくつか定義されていることにお気づきでしょうか。これらは今後作成するファイルです。viewsディレクトリに作成して、CSSを既存のHTML要素で共有できるようにする予定です。CSSファイルを何度もあちこちに行ったり来たりしなくても済むように、マイナーなHTML要素については、今後のHTML要素で定義されるこれらのクラスをさしあたってここに足しておきます。特定のスタイルがどのように効いているかを知りたければ、いつでもスタイルシートを開いて調べることができます。これまでは新しく作成されたチャットのidをセッション内に保存していましたが、この辺で、この機能を利用してユーザーがチャットを閉じたりセッションが終了するまでチャットウィンドウを開いておけるようにしましょう。ApplicationControllerでフィルタを定義します。:opened_conversations_windows続いて、opened_conversations_windowsメソッドを定義します(Gist)。endend...このincludesメソッドは、関連付けられているデータベーステーブルからのデータをインクルードするのに使います。今後はチャットからメッセージを読み込みます。includesメソッドを使わなければ、チャットメッセージのレコードがこのクエリで読み込まれなくなり、N+1クエリが発生することがあります。このクエリでメッセージが読み込まれないと、メッセージ1件1件で追加クエリが発火するかもしれません。N+1クエリが発生するとアプリのパフォーマンスに著しい影響が生じる可能性があります。ここでは100件のメッセージで100件のクエリを発行するのではなく、最初のクエリ1つだけで任意の数のメッセージを取れるようにしています。application.html.erbファイルで、yieldメソッドの直後に以下を追加します(Gist)。%>...applicationディレクトリを作成し、その下に_private_conversations_windows.html.erbパーシャルファイルを作成します。15に変えないとRSpecが通りませんでした。endこれでブラウザでアプリを操作すると、どのページでもチャットが常に表示されるようになります。変更をcommitします。コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。チュートリアル第2版の監修および半分程度を翻訳、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れて更新翻訳中。enno.jpを運営。実は最近Go言語が好きで、Goで書かれたRubyライクなGoby言語のメンテナーでもある。仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。Storageを使ってはいけない(翻訳)#Tags#会社紹介#自社サービス#勉強会#Rails#PHP#デザイン#実績#週刊RailsウオッチCONTACTお気軽にご相談ください。(翻訳)CONTACTTechRachoでは、パートナーシップをご検討いただける方からのご連絡をお待ちしております。お気軽にご意見・ご相談ください。