- 投稿日:2019-07-14T23:51:49+09:00
Railsでカラムの値によって動的にmixinする
- Userモデル
- job_id: references
- Jobモデル
- klass: string
を考える
class User < ApplicationRecord belongs_to :job end class Job < ApplicationRecord has_many :users def method_missing(name, *args) fail if klass.blank? || self.class.included_modules.include?("Modules::#{self.klass.camelize}".constantize) extend("Modules::#{self.klass.camelize}".constantize) send(name, *args) and return if respond_to?(name) fail end end module Modules module Engineer def bark p 'hoge' end end end engineer = Job.create(klass: 'engineer') user = User.create(job: engineer) user.job.bark # => hogeやや冗長だけどやや安全版↓
class User < ApplicationRecord belongs_to :job end class Job < ApplicationRecord has_many :users def bark fail end def mixined klass.present? ? extend("Modules::#{self.klass.camelize}".constantize) : self end end module Modules module Engineer def bark p 'hoge' end end end engineer = Job.create(klass: 'engineer') user = User.create(job: engineer) user.job.mixined.bark # => hoge完
というのは嘘で素直にSTIしたほうが良いのではないでしょうか。
- 投稿日:2019-07-14T23:21:17+09:00
Rails6が出る前にActionMailboxを試してみる
ActionMailBox とは
Rails6で入るframeworkの一つです。
今までメールの送信機能として ActionMailer ではありましたが、
メール受信に関する機能がなく、そこで入ったのが ActionMailbox です。ドキュメントなど
- RailsGuide
- edge api rails
- GitHub
- この記事書く上で参考にしたもの
機能を3行で
- SendGrid などのメールサービスでメールを受信したときに、Railsの専用エンドポイントに転送する
- ApplicationController と似た ApplicationMailbox でroutingを書く
- ApplicationMailbox を継承したクラスでメール受信時の処理を行う
ActionMailbox を使うとできそうなこと
- GitHubにある、コメントのメールに返信したらGitHub上でもコメントできる、的な機能
- 参考にした動画( https://gorails.com/episodes/action-mailbox-rails-6 )では、これを作っています。
- メールサポートシステム(メールディラーなどの代わり)
- ActionMailer, ActionMailbox を使って、ユーザー情報とも紐づけてサポート対応をより簡単にできるかも。
処理の流れのイメージ
例えば、『Aさんが書いたブログにコメントをされ、その通知をAさんにメールで送信、Aさんがメールに返信したらコメントを返せる』という機能を考えてみます。
- ブログにコメントがされる
- ActionMailer でコメントされた通知をAさんにメールで送る
- to はAさんのメールアドレス(
a-san@example.com
とします)- from には、ブログ・コメントのIDを埋め込んでおきます。
reply-blog_1-comment-2@someapp.example.com
という感じ。- Aさんはは送られてきたメールに返信する
- メールサービスがメールを受信し、railsのアプリケーションに転送
- 受信・転送がされるように設定しておく必要があります。
- ドメインの設定と受信時に転送する設定が必要です。
- 詳しくは、RailsGuide や各メールサービスのドキュメントを見てください。
ApplicationMailbox
の routing でどのmailboxで処理をするか振り分ける
- 基本的には、メールを受信したときの
to
で振り分けをします
- ここでは
reply-blog_1-comment-2@someapp.example.com
というメールアドレスです。FooMailbox#process
でメール受信時の処理をします。
to
(a-sann@example.com
)メールアドレスで、ユーザー情報を検索from
(reply-blog_1-comment-2@someapp.example.com
)メールアドレスで、コメントすべきblog, commentを特定body
にある文章でコメントを追加実装する上で知っておくと理解が早いこと
RailsGuide には、ActionMailbox の最低限の情報が載っているだけです。
全体像理解してからなら十分読めると思いますが、それまではエスパーするのはなかなか難しいです。
ApplicationMailbox
のroutingで、受信したメールのto
で処理を振り分ける# app/mailboxes/application_mailbox.rb class ApplicationMailbox < ActionMailbox::Base routing /^save@/i => :forwards routing /@replies\./i => :replies # routing :all => :foos end # app/mailboxes/forwards_mailbox.rb class ForwardsMailbox < ApplicationMailbox def process # ... end end # app/mailboxes/replies_mailbox.rb class RepliesMailbox < ApplicationMailbox def process # ... end endRailsGuideに書かれてるコードを参考に見てみます。
ActionMailbox::Base
を継承したApplicationMailbox
にrouting
を書きます。
受信したメールのto
が、routingに書いた正規表現にマッチしたら指定のmailboxに振り分けします。
例えば、topic+1@replies.example.com
というメールアドレスでメールを受信した場合、
routing /@replies\./i => :replies
の正規表現にマッチするので、RepliesMailbox
クラスに振り分けられます。Mailboxの実装のサンプル
https://gorails.com/episodes/action-mailbox-rails-6 で実装するものがこのような感じです。
class RepliesMailbox < ApplicationMailbox MATCHER = /\Areply-(.+)@reply.example.com/i # domain部分は例で書いてあるだけです。 before_processing :require_user def process discussion.comments.create( user: user, body: mail.decoded, ) end private def user # ユーザーは事前に同じメールアドレスでサイトに登録されてる必要があります。 @user ||= User.find_by(email: mail.from) end def require_user unless user # bounce_with でbounceの処理をする。ここで#processの処理は停止します # ユーザーが見つからない場合、bounceのメールを送信する bounce_with UserMailer.missing(inboud_email) end end def discussion @discussion ||= Discussion.find(discussion_id) end # メールアドレスからdiscussion_idを取り出す def discussion_id recipient = mail.recipients.find { |r| MATCHER.match?(r) } recipient[MATCHER, 1] end end
- mailbox は controller と似た感じで実装できます
- まず
#process
が呼ばれるbefore_action
->before_processing
request
->inbound_email
inbound_email
:ActionMailbox::InboundEmail
のレコードActionMailbox::InboundEmail
rails action_mailbox:install
で作られる、ActiveRecordのクラス- 受信したメールはここに溜まっていく
- 一定期間したら消えていく
- 上記のサンプルコードでは、受信時のメールアドレス(
to
,recipients
)から duscussion_id を取り出しています。
- このような実装をする場合、メールアドレスが重要なので、まずアプリ側からそういうメールアドレスを作ってユーザーにメールを送信する必要があるでしょう。
- サイト側からメールを送るときの
from
のアドレスが重要ということです。それに返信してもらったら、受信時のto
になるので。- もっと賢い方法をもしご存知でしたらぜひ教えていただけるとありがたいです
User
,Discussion
,Comment
,UserMailer
などは当然自前で実装が必要です。
- RailsGuide のサンプルコードの
Person
,Forward
,Forwards::RoutingMailer
あたりも自前実装のはずです。development環境で試す
https://railsguides.jp/action_mailbox_basics.html#action-mailboxをdevelopment環境で使う
/rails/conductor/action_mailbox/inbound_emails
にActionMailbox::InboundEmail
のcontrollerが用意されています。
/rails/conductor/action_mailbox/inbound_emails/new
でメール入力フォームが表示され、メール受信時の挙動を再現できます。ローカル環境で実際のメールの受信を試してみる
Mailgun や SendGrid のようなメール送受信サービスを使う場合、
外部からのリクエストを受け付けられるようにする必要があります。https://ngrok.com/
ngrokを使うと良いでしょう。
ngrokを使うと、外部からアクセスできるURLが発行できます。いくつか手順があります。
Mailgunを例にします。
- Mailgunのアカウント登録を済ませる
ngrokを
ngrok http 3000
起動してURLを発行するMailgun で、メールを受信したときに Rails にメールを転送するように設定する
- RailsGuide に書くメールサービスごとに設定の仕方が書いてあります。
- Mailgun の場合、 https://railsguides.jp/action_mailbox_basics.html#mailgun
- 設定するURLは
https://<ngrokで発行されたドメイン>/rails/action_mailbox/mailgun/inbound_emails/mime
になります。- pathは、メールサービスごとに違うので注意してください。
Mailgun から発行された API key を Rails の Credentials に設定する
Rails が Mailgun を使用するように設定する
config.action_mailbox.ingress = :mailgun
- https://railsguides.jp/action_mailbox_basics.html#mailgun
Rails を起動する
Mailgun で発行されたドメインに対してメールを送る
これで基本的には動くはずです。
動かない場合、以下を確認してみると原因特定につながるかと思います。
- Mailgunのダッシュボード
- ngrokのダッシュボード
- InboundEmails
おわりに
ActionMailbox の触りの紹介の記事でした。
まだこの記事を書いている段階では Rails6 はリリースされていませんし、ネットには情報がとても少ないです。
ですので、実際運用しようと実装してみると、もっと知らないといけないことはあるかもしれないですし、
ハマりどころもあるかもしれません。ですが、この記事がとりあえず触ってみるきっかけになれば幸いです
- 投稿日:2019-07-14T23:21:17+09:00
Rails6が出る前のActionMailbox入門
ActionMailBox とは
Rails6で入るframeworkの一つです。
今までメールの送信機能として ActionMailer ではありましたが、
メール受信に関する機能がなく、そこで入ったのが ActionMailbox です。ドキュメントなど
- RailsGuide
- edge api rails
- GitHub
- この記事書く上で参考にしたもの
機能を3行で
- SendGrid などのメールサービスでメールを受信したときに、Railsの専用エンドポイントに転送する
- ApplicationController と似た ApplicationMailbox でroutingを書く
- ApplicationMailbox を継承したクラスでメール受信時の処理を行う
ActionMailbox を使うとできそうなこと
- GitHubにある、コメントのメールに返信したらGitHub上でもコメントできる、的な機能
- 参考にした動画( https://gorails.com/episodes/action-mailbox-rails-6 )では、これを作っています。
- メールサポートシステム(メールディラーなどの代わり)
- ActionMailer, ActionMailbox を使って、ユーザー情報とも紐づけてサポート対応をより簡単にできるかも。
処理の流れのイメージ
例えば、『Aさんが書いたブログにコメントをされ、その通知をAさんにメールで送信、Aさんがメールに返信したらコメントを返せる』という機能を考えてみます。
- ブログにコメントがされる
- ActionMailer でコメントされた通知をAさんにメールで送る
- to はAさんのメールアドレス(
a-san@example.com
とします)- from には、ブログ・コメントのIDを埋め込んでおきます。
reply-blog_1-comment-2@someapp.example.com
という感じ。- Aさんはは送られてきたメールに返信する
- メールサービスがメールを受信し、railsのアプリケーションに転送
- 受信・転送がされるように設定しておく必要があります。
- ドメインの設定と受信時に転送する設定が必要です。
- 詳しくは、RailsGuide や各メールサービスのドキュメントを見てください。
ApplicationMailbox
の routing でどのmailboxで処理をするか振り分ける
- 基本的には、メールを受信したときの
to
で振り分けをします
- ここでは
reply-blog_1-comment-2@someapp.example.com
というメールアドレスです。FooMailbox#process
でメール受信時の処理をします。
to
(a-sann@example.com
)メールアドレスで、ユーザー情報を検索from
(reply-blog_1-comment-2@someapp.example.com
)メールアドレスで、コメントすべきblog, commentを特定body
にある文章でコメントを追加実装する上で知っておくと理解が早いこと
RailsGuide には、ActionMailbox の最低限の情報が載っているだけです。
全体像理解してからなら十分読めると思いますが、それまではエスパーするのはなかなか難しいです。
ApplicationMailbox
のroutingで、受信したメールのto
で処理を振り分ける# app/mailboxes/application_mailbox.rb class ApplicationMailbox < ActionMailbox::Base routing /^save@/i => :forwards routing /@replies\./i => :replies # routing :all => :foos end # app/mailboxes/forwards_mailbox.rb class ForwardsMailbox < ApplicationMailbox def process # ... end end # app/mailboxes/replies_mailbox.rb class RepliesMailbox < ApplicationMailbox def process # ... end endRailsGuideに書かれてるコードを参考に見てみます。
ActionMailbox::Base
を継承したApplicationMailbox
にrouting
を書きます。
受信したメールのto
が、routingに書いた正規表現にマッチしたら指定のmailboxに振り分けします。
例えば、topic+1@replies.example.com
というメールアドレスでメールを受信した場合、
routing /@replies\./i => :replies
の正規表現にマッチするので、RepliesMailbox
クラスに振り分けられます。Mailboxの実装のサンプル
https://gorails.com/episodes/action-mailbox-rails-6 で実装するものがこのような感じです。
class RepliesMailbox < ApplicationMailbox MATCHER = /\Areply-(.+)@reply.example.com/i # domain部分は例で書いてあるだけです。 before_processing :require_user def process discussion.comments.create( user: user, body: mail.decoded, ) end private def user # ユーザーは事前に同じメールアドレスでサイトに登録されてる必要があります。 @user ||= User.find_by(email: mail.from) end def require_user unless user # bounce_with でbounceの処理をする。ここで#processの処理は停止します # ユーザーが見つからない場合、bounceのメールを送信する bounce_with UserMailer.missing(inboud_email) end end def discussion @discussion ||= Discussion.find(discussion_id) end # メールアドレスからdiscussion_idを取り出す def discussion_id recipient = mail.recipients.find { |r| MATCHER.match?(r) } recipient[MATCHER, 1] end end
- mailbox は controller と似た感じで実装できます
- まず
#process
が呼ばれるbefore_action
->before_processing
request
->inbound_email
inbound_email
:ActionMailbox::InboundEmail
のレコードActionMailbox::InboundEmail
rails action_mailbox:install
で作られる、ActiveRecordのクラス- 受信したメールはここに溜まっていく
- 一定期間したら消えていく
- 上記のサンプルコードでは、受信時のメールアドレス(
to
,recipients
)から duscussion_id を取り出しています。
- このような実装をする場合、メールアドレスが重要なので、まずアプリ側からそういうメールアドレスを作ってユーザーにメールを送信する必要があるでしょう。
- サイト側からメールを送るときの
from
のアドレスが重要ということです。それに返信してもらったら、受信時のto
になるので。- もっと賢い方法をもしご存知でしたらぜひ教えていただけるとありがたいです
User
,Discussion
,Comment
,UserMailer
などは当然自前で実装が必要です。
- RailsGuide のサンプルコードの
Person
,Forward
,Forwards::RoutingMailer
あたりも自前実装のはずです。development環境で試す
https://railsguides.jp/action_mailbox_basics.html#action-mailboxをdevelopment環境で使う
/rails/conductor/action_mailbox/inbound_emails
にActionMailbox::InboundEmail
のcontrollerが用意されています。
/rails/conductor/action_mailbox/inbound_emails/new
でメール入力フォームが表示され、メール受信時の挙動を再現できます。ローカル環境で実際のメールの受信を試してみる
Mailgun や SendGrid のようなメール送受信サービスを使う場合、
外部からのリクエストを受け付けられるようにする必要があります。https://ngrok.com/
ngrokを使うと良いでしょう。
ngrokを使うと、外部からアクセスできるURLが発行できます。いくつか手順があります。
Mailgunを例にします。
- Mailgunのアカウント登録を済ませる
ngrokを
ngrok http 3000
起動してURLを発行するMailgun で、メールを受信したときに Rails にメールを転送するように設定する
- RailsGuide に書くメールサービスごとに設定の仕方が書いてあります。
- Mailgun の場合、 https://railsguides.jp/action_mailbox_basics.html#mailgun
- 設定するURLは
https://<ngrokで発行されたドメイン>/rails/action_mailbox/mailgun/inbound_emails/mime
になります。- pathは、メールサービスごとに違うので注意してください。
Mailgun から発行された API key を Rails の Credentials に設定する
Rails が Mailgun を使用するように設定する
config.action_mailbox.ingress = :mailgun
- https://railsguides.jp/action_mailbox_basics.html#mailgun
Rails を起動する
Mailgun で発行されたドメインに対してメールを送る
これで基本的には動くはずです。
動かない場合、以下を確認してみると原因特定につながるかと思います。
- Mailgunのダッシュボード
- ngrokのダッシュボード
- InboundEmails
おわりに
ActionMailbox の触りの紹介の記事でした。
まだこの記事を書いている段階では Rails6 はリリースされていませんし、ネットには情報がとても少ないです。
ですので、実際運用しようと実装してみると、もっと知らないといけないことはあるかもしれないですし、
ハマりどころもあるかもしれません。ですが、この記事がとりあえず触ってみるきっかけになれば幸いです
- 投稿日:2019-07-14T23:01:58+09:00
7/14 今日学んだ事(登録機能)
会員登録機能の実装
作業内容
1.usersテーブルにnicknameカラムを追加
2.ビューを追加
3.ストロングパラメータを追加
4.バリテーションの追加1.usersテーブルにnicknameカラムを追加する
usersテーブルにnicknameカラムをstring型で追加する
ターミナル.$ rails g migration Addカラム名Toテーブル名 カラム名:データ型 となるので $ rails g migration AddNicknameToUseres nickname:string とターミナルで実行マイグレーションの実行
ターミナル.$ rake db:migrate・RailsはAdd ~~ ToUsersのように最後に記述するテーブル名でカラムを追加するテーブルを判断しているので、ファイル名は正しく打ち込む
2.ビューの追加
サインアップ画面にnicknameを入力するフォームと、アイコン画像をアップロードできるフォームを作成。
・サインアップ画面はRailsのform_forメソッドを使って記述する。
・ニックネームは『テキストフィールド』のフィールドを生成する
・アイコン画像は『ファイルフィールド』のフィールドを生成する生成方法
テキストフィールド
new.html.erb<%= f.text_field :カラム名 %>ファイルフィールド
new.html.erb<%= f.file_field :カラム名 %>のフォームタグを利用すると生成される。
※new.html.erbは新規作成するテキスト3.ストロングパラメータを追加
まずnicknameとimageが設定されていないため、deviseのstrong_parametersに新しく許可するパラメーターを追加する必要がある。
そこで、deviseのdevise_parameter_sanitizerメソッドを使用する。●devise_parameter_sanitizer
・strong_parametersに対してパラメータをー追加できる。
・before_actionに設定する※
・記述するのはDeviseのコントローラを継承したコントローラかもしくはApplicationControllerコード
devise_parameter_sanitizer.permit(追加したいメソッドの種類, keys: [追加したいパラメーター名])引数の値(処理)
1. :sign_up(新規登録時)
2. :sign_in(ログイン時)
3. :account_update(レコードの更新時)複数パラメータを追加する場合
devise_parameter_sanitizer.permit(追加したいメソッドの種類, keys: [:パラメーター1, :パラメーター2,..])『,』カンマで区切る。
注意
※直接before_actionに設定してはいけない
devise_parameter_sanitizerを呼び出すためのメソッドを作成してそのメソッドを呼び出すようにする。例
application_controller.rbbefore_action :configure_permitted_parameters def configure_permitted_parameters # devise_parameter_sanitizerメソッドを呼び出す endすべてのコントローラがApplicationControllerを継承しているため、この記述ではすべてのコントローラのアクションの前でdevise_parameter_sanitizerメソッドが呼び出される。
devise_parameter_sanitizerメソッドはdeviseで追加されたメソッドなので、Deviseのコントローラ以外で呼び出すことができない。よって、before_actionを適応するコントローラを指定する。before_actionではifというオプションを指定することができる。これはbefore_actionを呼び出す条件を指定するもの。今回はコントローラの種類を指定するので以下のように記述する。
例
application_controller.rbbefore_action :メソッド名, if: :コントローラ名?例
application_controller.rbbefore_action :configure_permitted_parameters, if: :devise_controller?devise_controllerが動いた時のみこのアクションを事前に動かしたいので、ifオプションを利用してそのように設定する。
※configure_permitted_parametersはdeviseで利用出来るパラメーターを設定でるdevise関連のストロングパラメーターの管理メソッド。4.バリテーションの追加
バリテーションとは
オブジェクトがDBに保存される前に、そのデータが正しいかどうかを検証する仕組み今回やりたい事
・nicknameが入力されていないとエラーがでるようにバリテーションを設定する。
・nicknameの入力が必須と分かるようテキストフィールドにプレイスホルダーを設定する●validation(検証)
入力フォームを通じてビューからサーバーへ側へパラメーターが送られてきた際、正常な値か検証できる機能。
●validates :カラム名, presence: true
フォームの中身があるか検出し、無い場合は保存しない。
例
userのnicknameの入力を必須にする場合user.rbclass User < ApplicationRecord validates :nickname, presence: trueと記述する。
● placeholder: ''
読み:(プレイスホルダー)
・フォームの値が空の時に、''で囲んだ文字を薄く表示しておくことができる。
・入力する人が、何を入力するか分かりやすくなる
今回はnicknameの入力欄に反映させたいので、text_fieldメソッドのオプションとして、以下のように利用する。new.html.erb<%= f.text field :nickname, placeholder:'ニックネームを入力(必須)' %>・ニックネームを設定していないとエラーがでるようにした
・ニックネームの入力欄にプレイスホルダーを設定できた以上
- 投稿日:2019-07-14T21:00:40+09:00
Rails newするときの流れ
新しくRailsプロジェクトを作る時の流れをメモとしてざっくり残しておきます。
- Railsをインストール -
$ mkdir ○◯◯ $ cd ◯◯◯ $ bundle init
bundle init
によりGemfileが作成されるので、Gemfileを編集。Gemfile# frozen_string_literal: true source "https://rubygems.org" git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } gem "rails" #ここのコメントアウトを外すそしたら、
$ bundle install --path vendor/bundle
。
--pathでパスを指定することによって、指定されたパスにRailsがインストールされる。- Rails new する -
bundle exec rails new . -B -d mysql -T
Rails new。
Overwrite /Users/apple/Desktop/App/Gemfile? (enter "h" for help) [Ynaqdhm]
と、Gemfileを上書きしていいか聞かれるので、Yes。
Rspecを使いたいのでテストはスキップ。- DBを作成する -
usernameとpasswordを指定。
config/database.ymldefault: &default adapter: mysql2 encoding: utf8 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: #ユーザー名 password: #パスワード socket: /tmp/mysql.sockターミナルで
$ bundle config --local build.mysql2 "--with-cppflags=-I/usr/local/opt/openssl/include" $ bundle config --local build.mysql2 "--with-ldflags=-L/usr/local/opt/openssl/lib"この二つのコマンドを実行し、もう一度
bundle install
します。そして
bin/rails db:create
。上記の2つのコマンドを入力してbundle installという工程を飛ばすとエラーが出ます(自分の環境だと)。
- Rspecの設定をする -
Gemfile・・・ group :development, :test do gem 'rspec-rails' #それぞれ、必要があればバージョン指定をする。 gem 'factory_bot_rails' gem 'spring-commands-rspec' end ・・・Gemfileに上記の3つを追加し、
bundle install
。
$ bin/rails generate rspec:install
RSpecをインストール。.rspec--require spec_helper --format documentation #この行を追加するとテストを実行した際にログが読みやすくなる。次は
$ bundle exec spring binstub rspec
。
このコマンドを打つとbin/rspec
コマンドでテストが実行できるようになる。config/application.rbmodule Test class Application < Rails::Application ... config.generators do |g| g.test_framework :rspec, fixtures: true, view_specs: false, helper_specs: false, routing_specs: false, request_specs: false end ... end end上記の行を追加。
これでRSpecのセットアップは完了です。- Slimを導入する -
Gemfile... gem 'slim-rails' gem 'html2slim'
bundle install
でgemをインストール。
bundle exec erb2slim app/views/layouts --delete
erbファイルを削除。- Bootstrapをインストール -
Gemfilegem 'bootstrap;
bundle install
。
$ rm app/assets/stylesheets/application.css
sassを使うために、application.cssを削除。
application.scssを作成し、application.scss@import 'bootstrap';追加。
- i18nで日本語化 -
$ wget https://raw.githubusercontent.com/svenfuchs/rails-18n/master/rails/locale/ja.yml --output-document=config/locales/ja.ymlrawファイルをja.ymlにダウンロード。
config/initializers下にlocale.rbファイルを作成。config/initializers/locale.rbI18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}').to_s] I18n.config.available_locales = :ja I18n.default_locale = :jaこれで完了です。
- 終わり -
こんな感じで一通り終了です。
いつも忘れてしまうので、ザックリまとめてみました。
ここをこうした方が良いよとか、アドバイスがあったら教えていただけると嬉しいです。
- 投稿日:2019-07-14T20:19:21+09:00
RailsアプリをHerokuにデプロイ(Nginx・Unicorn使用)
目次
1. エラーとの遭遇
めっちゃニッチな需要だけど、簡単なBlogのCRUDを実装したRailsアプリをHerokuにデプロイする際にNginxとUnicornを使用したい状況があった。
これまではheroku-buildpack-nginxのREADMEにある
Existing App
とNew App
通りにやればできたハズ、、、
だったけど、この前同じ手順でデプロイをしたところ、App crushed
が出てしまった...まあ、herokuのrestartでもすればなおるだろうと試してみたが、ダメだった?♀️
とりあえずログを見ようと
$ heroku logs -t
を試したらH10
の文字が...
H10
てたまに出るけど、エラーを直すだけでH10
自体の意味をあまり考えたことがなかった。
H10 - App crashed
なるほど...??確かweb dynoってHTTPのリクエストとかを捌いてんだっけ、、って感じになったので原因を探ってみることにした。
H10
が出た時はとりあえず下記コマンドからconsoleを開いて、BlogをcountしてみたりするとよくみるRailsのエラーが表示されることがある。Terminal$ heroku run rails c
・・・エラーが出ない?
じゃあCRUD機能は問題ないと仮定して、どこがダメなのかな〜とあちこち確認を始めた。
この時はまだ余裕があったので、とりあえず
Procfile
とかconfig/
の中とか確認してみればいっかなーとか考えてbuildpackのドキュメント通りにconfigファイル書くときにタイポでもしたかと目を皿にして確認。・・・ちゃんとできてる?
早いけどこの辺で長期化を覚悟し始めた
- buildpackの読み込みの順番を変えてみる
- Gemfileの確認
- herokuのログの再確認
- rails newから再度やり直し
とかそれぞれ何回もやったけど、ダメだった。
herokuのログには下記の内容が毎秒吐き出されてる...???2019-07-14T02:22:44.859554+00:00 app[web.1]: buildpack=nginx at=app-initialization 2019-07-14T02:22:45.861308+00:00 app[web.1]: buildpack=nginx at=app-initialization 2019-07-14T02:22:46.863727+00:00 app[web.1]: buildpack=nginx at=app-initializationここを確認してみたけど、多分buildpackに従ってNginxを使おうとしてるけど、肝心のNginxが反応ないのかな...?
とか生意気に仮定は立てるけど、どうしたら動くのかが思いつかない...
ここまでで既に数時間経っていたので、先輩に聞いてみた。半日くらいして、眠気で目を血走らせた先輩から「できた」と言われ、マジかと思いつつ確認してみると
確かにできてる...?
よーわかんないけどを連発しながら仮定を説明してもらった。(自分も本当によーわからんかった。??)
とりあえず言われた通り、下記のようにしたらうまく動くようになりました。
2. デプロイまでの手順
- Railsのプロジェクト内で下記を実行。
Terminal$ heroku create # herokuにアプリ枠を作成 $ heroku buildpacks:add heroku/ruby # Rubyのbuildpackを追加 $ heroku buildpacks:add https://github.com/heroku/heroku-buildpack-nginx # HerokuでNginxを使用するbuildpackの追加
ここまでは今までやってきたことと一緒 ?♀️
3. Nginxのconfigurationファイルの作成
ここからが、先輩から教えてもらって追加した設定☝️
config/nginx.conf
を作成する。そういえば今までNginxの設定は特にしてなかったな...?とか思いながら言われた通りにやる。
下記のコードはこの記事からココを参考にしたみたい。(この人の記事よくみる気がする...?)
nginx.confworker_processes <%= ENV['NGINX_WORKERS'] || 4 %>; events { use epoll; accept_mutex on; worker_connections 1024; } http { gzip on; gzip_comp_level 3; gzip_min_length 150; gzip_proxied any; gzip_types text/plain text/css text/json text/javascript application/javascript application/x-javascript application/json application/rss+xml application/vnd.ms-fontobject application/x-font-ttf application/xml font/opentype image/svg+xml text/xml; server_tokens off; log_format l2met 'measure#nginx.service=$request_time request_id=$http_x_request_id'; access_log logs/nginx/access.log l2met; error_log logs/nginx/error.log; include mime.types; default_type application/octet-stream; sendfile on; # Must read the body in 5 seconds. client_body_timeout 5; upstream app_server { server unix:/tmp/nginx.socket fail_timeout=0; } server { listen <%= ENV["PORT"] %>; server_name _; keepalive_timeout 5; root /app/public; # path to your app location / { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_redirect off; proxy_pass http://app_server; } location ~* ^/documentation/(.*) { set $s3_bucket 'my_bucket.s3-website-us-east-1.amazonaws.com'; set $url_full '$1'; resolver 8.8.8.8 valid=300s; resolver_timeout 10s; index index.html; proxy_hide_header x-amz-id-2; proxy_hide_header x-amz-request-id; proxy_hide_header Set-Cookie; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $s3_bucket; proxy_ignore_headers "Set-Cookie"; proxy_buffering off; proxy_intercept_errors on; proxy_redirect off; proxy_pass http://$s3_bucket/$url_full; } location ~* \.(eot|oft|svg|ttf|woff)$ { add_header Access-Control-Allow-Origin *; expires max; log_not_found off; access_log off; add_header Cache-Control public; } location ~* ^/assets/ { gzip_static on; # Per RFC2616 - 1 year maximum expiry expires 1y; add_header Cache-Control public; # Some browsers still send conditional-GET requests if there's a # Last-Modified header or an ETag header even if they haven't # reached the expiry date sent in the Expires header. add_header Last-Modified ""; add_header ETag ""; break; } } }先輩曰く必要ない記述もありそうとのこと。とりあえず動かすだけだから今は気にしないでおく。
4. Unicornの設定
- Gemfileからpumaを削除し、unicornを追記
Gemfilegem 'unicorn'
pumaは使用しないので
config/puma.rb
を削除する。Gemfile.lock更新のため、
$ bundle
しておくここも元々やってたことと一緒だった ?♀️
config/unicorn.rb
を作成する。ここもこれまでと一緒 ?♀️...と思っていたらちょっと違った。
これまではbuildpackドキュメントのUpdate Unicorn Configを参考にしてたけど、下記のようにするみたい。
unicorn.rbworker_processes Integer(ENV["WEB_CONCURRENCY"] || 3) timeout 15 preload_app true worker_processes 4 listen 'unix:///tmp/nginx.socket', backlog: 1024 before_fork do |server,worker| FileUtils.touch('/tmp/app-initialized') end
FileUtils.touch
については、さっきの記事のConfiguring Pumaを読む限り、それぞれの順番を守もらうための設定らしい。?こんな感じ?
The start-nginx script from the Nginx Buildpack will wait until the /tmp/app-initialized file is present before Nginx binds to the public port.
アプリケーションサーバの起動を待ってからNginxと連携するみたい。
- 次に、
Procfile
を作成する。
config/unicorn.rb
を読み込む設定。Procfileweb: bin/start-nginx bundle exec unicorn -c config/unicorn.rb5.デプロイ
やっとデプロイ??
ここはいつも通り、addしてcommitしてpushすればOK?♀️
- herokuでDBを作成する。
Terminal$ heroku run rake db:migrate RAILS_ENV=productionこれもいつも通りなはず。
ここまでできたら、あとは
$ heroku open
などでアプリを開いて実際に動作確認。
ここでエラーが出ている場合は、Railsアプリ内で何か間違っているか、ここまでの手順に何か抜け漏れがあるハズ?6. Nginxの確認方法
しっかりとheroku上でアプリが動くことを確認できたら、いよいよNginxがしっかり動いているか確認してみる。
確認方法は複数あると覆いますが、ここでは
wget
コマンドを使用して確認していきます。下記を実行してみると、下記のような結果が返ってくるハズ。
Terminal$ wget -S --spider <HerokuアプリのURL> Spider mode enabled. Check if remote file exists. --2019-07-14 17:29:09-- <HerokuアプリのURL> Resolving <HerokuアプリのURL>... Connecting to <HerokuアプリのURL>|... connected. HTTP request sent, awaiting response... HTTP/1.1 200 OK Connection: keep-alive Server: nginx # ここにNginxが表示される。 Date: Sun, 14 Jul 2019 08:29:09 GMT Content-Type: text/html; charset=utf-8 X-Frame-Options: SAMEORIGIN X-Xss-Protection: 1; mode=block X-Content-Type-Options: nosniff X-Download-Options: noopen X-Permitted-Cross-Domain-Policies: none Referrer-Policy: strict-origin-when-cross-origin Cache-Control: max-age=0, private, must-revalidate X-Runtime: 0.090119 Via: 1.1 vegur Length: unspecified [text/html] Remote file exists and could contain further links, but recursion is disabled -- not retrieving.Spiderモードだって?
カッコいいけどよく分かんないから調べてみると、こんな感じらしい。
--spider
はwgetコマンドのオプションみたい。これまで何も考えずに使ってた?ファイルをダウンロードせず、URLの存在だけチェックする(“Web spider”として動作する)。例えば、ブックマークをチェックするなら「wget --spider --force-html -i bookmarks.html」のように指定する
- wgetコマンドが使用できない場合は、brewからインストールしましょう。
7. まとめ
長くなりましたが、これでHerokuにデプロイしたアプリでNginxが動いていることがしっかりと確認できました!
先輩のおかげで助かった...
自分だけだと多分ここまでくるのに何日もかかったハズ?※今回herokuにデプロイするために作成したRailsプロジェクト
README
にも簡潔に一連の内容を書いてみました。
https://github.com/kawaaa26/herokuapp_nginx
- 投稿日:2019-07-14T20:12:20+09:00
Railsチュートリアルのために作成したDockerコンテナを起動し、必要なモジュールをインストールする for Windows
拙著RailsチュートリアルのためにDockerコンテナの作成 for Windowsの続きとなります。「Yay! You're on Rails!」と表示されるところまで持っていきます。
前回停止させたコンテナを再び起動
まず、前回停止させたDockerコンテナが存在することを確認します。
powershell$ docker container ls -l CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES e11d56031051 ruby:2.5.1 "/bin/bash" 22 hours ago Exited (0) 21 hours ago RailsTutorialTest
STATUS
も、想定通りExited (0)
になっています。停止状態のDockerコンテナは、
docker start {コンテナ名}
コマンドで起動することができます。powershell$ docker start RailsTutorialTest RailsTutorialTest改めて
docker ls
コマンドを実行します。powershell$ docker container ls CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES e11d56031051 ruby:2.5.1 "/bin/bash" 22 hours ago Up About a minute 0.0.0.0:8080->3000/tcp RailsTutorialTest
RailsTutorialTest
コンテナが正常に起動されています。続いて、
RailsTutorialTest
コンテナのシェルに入りましょう。powershell$ docker container exec -it RailsTutorialTest /bin/bash root@e11d56031051:/#正常に
RailsTutorialTest
コンテナのシェルに入れました。
docker container exec
のオプション
- -it…ホスト側の標準入出力とDockerコンテナの標準入出力を接続する
- -i…ホスト側の標準入力とDockerコンテナの標準入力を接続する
- -t…Dockerコンテナの標準出力とホスト側の標準出力を接続する
- ホスト側からDockerコンテナ上のシェルを操作するために必要な設定
- コマンドを実行するコンテナの名前
- コンテナで実行するコマンド
- コマンドを実行するコンテナの名前の後に入力する
- 今回は
/bin/bash
であるRailsのインストール
bash# apt update # apt install -y nodejs # gem install rails -v 5.1.6aptリポジトリの一覧を更新
まずは
apt update
でaptリポジトリの一覧を更新します。リポジトリを追加・削除した場合には必須となる操作です。Node.jsのインストール
続いて、
apt install -y nodejs
でNode.jsをインストールします。
RailsではrubyのExecJS gemを使用し、ExecJS gemの実行にはJavaScriptランタイムが必要となるためです。CentOS 7.5にRuby on Railsチュートリアルの環境を構築するによると、JavascriptランタイムなしでExecJS gemを実行しようとした場合、以下のようなエラーが発生して実行できないそうです。
bash$ rails server /home/vagrant/.gem/ruby/gems/execjs-2.7.0/lib/execjs/runtimes.rb:58:in `autodetect': Could not find a JavaScript runtime. See https://github.com/rails/execjs for a list of available runtimes. (ExecJS::RuntimeUnavailable) ...Rails本体のインストール
ここまで完了したら、いよいよRailsのインストールです。バージョンはRailsチュートリアルで使っている5.1.6を指定します。
bashSuccessfully installed rails-5.1.6ひとまずここまで到達できました。
Rails本体がインストールされていることの確認
bash# rails -v Rails 5.1.6
rails -v
コマンドにより、インストールされているRailsのバージョンを確認することができます。Rails 5.1.6が正しくインストールされているようです。gemをインストールするためにGemfileを修正する
bash# cd /var/www # rails _5.1.6_ new hello_app以下は、
rails-v
コマンドにより生成されたGemfileとRailsチュートリアルのリスト1.5に記載されていたGemfileの差分です。Gemfilesource 'https://rubygems.org' -git_source(:github) do |repo_name| - repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/") - "https://github.com/#{repo_name}.git" -end - - -# Bundle edge Rails instead: gem 'rails', github: 'rails/rails' -gem 'rails', '~> 5.1.6' -# Use sqlite3 as the database for Active Record -gem 'sqlite3' -# Use Puma as the app server -gem 'puma', '~> 3.7' -# Use SCSS for stylesheets -gem 'sass-rails', '~> 5.0' -# Use Uglifier as compressor for JavaScript assets -gem 'uglifier', '>= 1.3.0' -# See https://github.com/rails/execjs#readme for more supported runtimes -# gem 'therubyracer', platforms: :ruby - -# Use CoffeeScript for .coffee assets and views -gem 'coffee-rails', '~> 4.2' -# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks -gem 'turbolinks', '~> 5' -# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder -gem 'jbuilder', '~> 2.5' -# Use Redis adapter to run Action Cable in production -# gem 'redis', '~> 4.0' -# Use ActiveModel has_secure_password -# gem 'bcrypt', '~> 3.1.7' - -# Use Capistrano for deployment -# gem 'capistrano-rails', group: :development +gem 'rails', '5.1.6' +gem 'puma', '3.9.1' +gem 'sass-rails', '5.0.6' +gem 'uglifier', '3.2.0' +gem 'coffee-rails', '4.2.2' +gem 'jquery-rails', '4.3.1' +gem 'turbolinks', '5.0.1' +gem 'jbuilder', '2.6.4' group :development, :test do - # Call 'byebug' anywhere in the code to stop execution and get a debugger console - gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] - # Adds support for Capybara system testing and selenium driver - gem 'capybara', '~> 2.13' - gem 'selenium-webdriver' + gem 'sqlite3', '1.3.13' + gem 'byebug', '9.0.6', platform: :mri end group :development do - # Access an IRB console on exception pages or by using <%= console %> anywhere in the code. - gem 'web-console', '>= 3.3.0' - gem 'listen', '>= 3.0.5', '< 3.2' - # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring - gem 'spring' - gem 'spring-watcher-listen', '~> 2.0.0' + gem 'web-console', '3.5.1' + gem 'listen', '3.1.5' + gem 'spring', '2.0.2' + gem 'spring-watcher-listen', '2.0.1' end -# Windows does not include zoneinfo files, so bundle the tzinfo-data gem +# Windows環境ではtzinfo-dataというgemを含める必要があります gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]なお、この差分は、以下のコマンドにより取得したものをコピーアンドペーストしたものです。
powershell$ git --no-pager diff HEAD^..HEADbash# cd hello_app # bundle install最初の実行結果は、以下の通りになりました。
bashYou have requested: spring = 2.0.2 The bundle currently has spring locked at 2.1.0. Try running `bundle update spring` If you are updating multiple gems in your Gemfile at once, try passing them all to `bundle update`…おや、うまくいっていませんね。というわけで、以下のコマンドを実行してみます。
bash# bundle update何やら色々とインストールされていますね。最後に以下のメッセージが表示されるまで待ちます。
bashBundle updated!改めて、以下のコマンドを実行します。
bash# bundle install再び何やら色々と表示されましたね。最後に以下のメッセージが表示されるまで待ちます。
bashBundle complete! 15 Gemfile dependencies, 64 gems now installed. Bundled gems are installed into `/usr/local/bundle`
bundle install
が無事完了しました。Yay! You're on Rails!
bundle install
まで終われば、実際に動かすことができるRailsアプリケーションの作成は完了しています。RailsのローカルWebサーバー機能を使い、生成されたRailsアプリケーションを実際に動かしています。bash# rails server以下のメッセージが表示され、ローカルWebサーバーが起動されます。
bash=> Booting Puma => Rails 5.1.6 application starting in development => Run `rails server -h` for more startup options Puma starting in single mode... * Version 3.9.1 (ruby 2.5.1-p57), codename: Private Caller * Min threads: 5, max threads: 5 * Environment: development * Listening on tcp://0.0.0.0:3000 Use Ctrl-C to stopDockerコンテナ側の3000番ポートは、ホスト側の8080番ポートに連結されています。というわけで、ブラウザで
http://localhost:8080/
にアクセスしてみます。Yay! You're on Rails!…というわけで、無事Railsアプリケーションが動作するところまでの環境構築が完了しました。
演習
1. デフォルトのRailsページに表示されているものと比べて、今の自分のコンピュータにあるRubyのバージョンはいくつになっていますか? コマンドラインでruby -vを実行することで簡単に確認できます。
powershell$ ruby -v ruby 2.6.3p62 (2019-04-16 revision 67580) [x64-mingw32]上述の結果になりました。
2.6.3p62
でしょうか。2. 同様にして、Railsのバージョンも調べてみましょう。調べたバージョンはリスト 1.1でインストールしたバージョンと一致しているでしょうか?
powershell$ rails -v rails : 用語 'rails' は、コマンドレット、関数、スクリプト ファイル、または操作可能なプログラムの名前として認識されませ ん。名前が正しく記述されていることを確認し、パスが含まれている場合はそのパスが正しいことを確認してから、再試行してくだ さい。 発生場所 行:1 文字:1 + rails -v + ~~~~~ + CategoryInfo : ObjectNotFound: (rails:String) [], CommandNotFoundException + FullyQualifiedErrorId : CommandNotFoundExceptionそもそもホストのWindows環境にRailsはインストールされていませんでした。
番外. Gemのバージョンを調べてみましょう。
powershell$ gem -v 3.0.2Gemのバージョンは
3.0.2
でした。関連リンク
- DockerでRailsチュートリアル(環境構築編) - 桜色HelloWorld - 行ったことは、ほぼこちらの後追いです。
- Rails 開発環境のバージョン確認 [自分用メモ] - Qiita - @dawn_628 さんによるQiita記事。3年以上前の記事ですが、コマンドラインの入力内容は現在も変わりありません。
- 投稿日:2019-07-14T18:16:14+09:00
ReactでHTMLを作成し、Clickした際にそれぞれの情報をモーダルに表示・非表示させる(Railsで)
はじめに
ReactでHTMLを作成するまでは案外すんなりと行ったのですが、クリックした際に「それぞれの情報を持たせてモーダルを表示する」ことに苦労したので忘備録としてまとめておきます。
最終的にはこのような感じになります。
たくさんの方法を試したので不必要なものもあるかもしれません。
その点は指摘していただけると幸いです。
下の画像をクリックした際にその情報を表示させています。
表示させる内容は個人で変更してください。
1.gemの導入とインストール
この2つのgemをGemfileに加え、
bundle install
してくださいGemfilegem 'react-rails' gem 'webpacker'その後、それぞれインストールしてください
Terminal$ bundle install $ rails webpacker:install $ rails webpacker:install:react $ rails generate react:installするとapp以下に
app/javascript/components
フォルダが作成されます。
私自身、app/assets
以下にJavascriptフォルダがあるのに大丈夫なのかと思いましたが、問題ありません。2. application.html.hamlにtagを追加
application.html.hamlに以下の記述を加えてください
application.html.haml= javascript_pack_tag 'application'この際にTerminalでyarnがどうこうというエラーが出るかもしれません。
その場合$ yarn install
等、各自で調べて解決してください。これらでRails上でReactを使う準備ができましたので、いよいよjsファイルに記述していきます。
3. Components以下にjsファイルの作成
Components以下に
App.js
とGraduate.js
を作成します。
これらの名前は自分で決めてくださって結構です。
ではこちらのコードをコピーしてくださいApp.jsimport React from 'react'; import Graduate from './Graduate'; const lessonList = [ { name: '開成太郎(2017)', school: '一条高校', image: 'https://cdn.pixabay.com/photo/2018/01/03/12/33/graduation-3058263__480.jpg', introduction: '中学一年生の時は全然テストの点数を取れなかったけど、ここに通い始めて2ヶ月ほどで目に見えて点数が上がるようになりました', }, { name: '開成花子(2016)', school: '奈良高校', image: 'https://image.shutterstock.com/image-photo/japanese-student-browse-job-information-260nw-1010242525.jpg', introduction: '学校の授業では物足りず、塾に通うことに決めました。塾では私に合わせた応用問題を別途用意してもらい、そのおかげで合格できました', }, { name: '開成三郎(2019)', school: '郡山高校', image: 'https://image.shutterstock.com/image-photo/high-school-student-graduation-260nw-1242927238.jpg', introduction: '土曜日や日曜日も朝早くから開塾してくれたおかげでたくさんの勉強時間を確保することができました。', }, { name: '開成四郎(2015)', school: '登美ケ丘高校', image: 'https://cdn.pixabay.com/photo/2017/02/05/00/08/graduation-2038864__480.jpg', introduction: '二年生まで部活一筋だったため、勉強の進捗度は他の人に比べて劣っていたけれど、その分個別に補習の時間を組んでくれたおかげで合格できました。', } ]; class App extends React.Component { render() { return ( <div className="performance"> {lessonList.map((lessonItem) => { return ( <Graduate name={lessonItem.name} image={lessonItem.image} school={lessonItem.school} introduction={lessonItem.introduction} /> ); })} </div> ); } } export default App;それでは解説していきます。
import React from 'react'; import Graduate from './Graduate';まずimportとは輸入という意味です。
その名の通り、一行目ではReactを二行目ではGraduateファイルを読み込んでいます。
このおかげでReactを使うことができ、またGraduateにパラメーター(props)を渡すことができます。const lessonList = [ { name: '開成太郎(2017)', school: '一条高校', image: 'https://cdn.pixabay.com/photo/2018/01/03/12/33/graduation-3058263__480.jpg', introduction: '中学一年生の時は全然テストの点数を取れなかったけど、ここに通い始めて2ヶ月ほどで目に見えて点数が上がるようになりました', }, { name: '開成花子(2016)', school: '奈良高校', image: 'https://image.shutterstock.com/image-photo/japanese-student-browse-job-information-260nw-1010242525.jpg', introduction: '学校の授業では物足りず、塾に通うことに決めました。塾では私に合わせた応用問題を別途用意してもらい、そのおかげで合格できました', }, { name: '開成三郎(2019)', school: '郡山高校', image: 'https://image.shutterstock.com/image-photo/high-school-student-graduation-260nw-1242927238.jpg', introduction: '土曜日や日曜日も朝早くから開塾してくれたおかげでたくさんの勉強時間を確保することができました。', }, { name: '開成四郎(2015)', school: '登美ケ丘高校', image: 'https://cdn.pixabay.com/photo/2017/02/05/00/08/graduation-2038864__480.jpg', introduction: '二年生まで部活一筋だったため、勉強の進捗度は他の人に比べて劣っていたけれど、その分個別に補習の時間を組んでくれたおかげで合格できました。', } ];この部分ではそれぞれのハッシュを配列に入れています。これをあとでmapメソッドで一つずづ表示させていきます。
class App extends React.Component { render() { return ( <div className="performance"> {lessonList.map((lessonItem) => { return ( <Graduate name={lessonItem.name} image={lessonItem.image} school={lessonItem.school} introduction={lessonItem.introduction} /> ); })} </div> ); } }この部分でComponentを作成しています。(extendsは広げるという意味)
ConmonentはJavascriptの関数のようなものです。
その中のreturnでHTMLを返し、表示させています。またこのような記法をJSX
と言います。
また、JSXには約束事があり、複数の要素を返すことができません。なので図の場合、performanceクラスを親要素としその中に色々と要素を追加しています。
{lessonList.map((lessonItem)
ではlessonListの数だけ繰り返し、returnを読み込んでいます。
return内でとすることでGraduateコンポーネントを呼び出しています。これが可能なのはimport Graduate from './Graduate';
のおかげです。
sosite
呼び出す際にはname,school,image,introductionのパラメーターを渡しています。(props)
このパラメーターをGraduate.js側で使うわけです。ちなみにJSX内でjavascriptの記法を用いる際は中括弧が必要なため、中括弧内に書いてあります。export default App;App.jsの最後の行ですが、exportとは輸出という意味です。
これのおかげでHTMLファイルでApp.jsを呼び出すことができ、returnとして要素をHTMLに追加することができます。App.jsがGraduateを二行目でimportできているのもGraduate.jsで最後にexport default Graduate;
としているからです。Graduate.jsimport React from 'react'; class Graduate extends React.Component { constructor(props) { super(props); this.state = {isModalOpen: false}; } handleClickLesson() { this.setState({isModalOpen: true}); } handleClickClose() { this.setState({isModalOpen: false}); } render() { let modal; if (this.state.isModalOpen) { modal = ( <div className='modal-area'> <div className='modal-inner'> <div className='modal-header'></div> <div className='modal-introduction'> <h2>{this.props.name}</h2> <p>{this.props.introduction}</p> </div> <button className='modal-close-btn' onClick={() => this.handleClickClose()} > とじる </button> </div> </div> ) }; return ( <div className="graduate"> <img className="graduate__image" src={this.props.image} onClick={() => {this.handleClickLesson()}} /> <div className="graduate__school">{this.props.school} 合格!</div> <div className="graduate__student">{this.props.name}</div> {modal} </div> ); } } export default Graduate;この部分で、実際に表示されるHTMLを作成しています。App.jsから渡されたパラメータ(props)を使って書いていきます。
constructor(props) { super(props); this.state = {isModalOpen: false}; }新しくstate(状態)というものが出てきましたが、propsとstateは少し異なり、stateはそのComponent内で保持されるものであって、propsみたいにComponentからComponentに渡すことはできません。詳しくはこちら
この部分でモーダルの表示・非表示を管理しています。handleClickLesson() { this.setState({isModalOpen: true}); } handleClickClose() { this.setState({isModalOpen: false}); }ここで先ほどのstateを関数(
handleClickLesson
orhandleClickClose
)が呼ばれた時に更新しています。この時に注意して欲しいのが、更新する際はsetState
としないといけないことです。let modal; if (this.state.isModalOpen) { modal = ( <div className='modal-area'> <div className='modal-inner'> <div className='modal-header'></div> <div className='modal-introduction'> <h2>{this.props.name}</h2> <p>{this.props.introduction}</p> </div> <button className='modal-close-btn' onClick={() => this.handleClickClose()} > とじる </button> </div> </div> ) };ここで
isModalOpen
がtrueの場合のみ、変数modalに値を代入し、表示させます。
そしてモーダルの中にonClick={() => this.handleClickClose()}
があると思いますが、このボタンを押すことでisModalOpen
がfalseになり再び非表示になります。
またApp.jsからもらってきたパラメータ(props)を{this.props.name}とすることで代入することができます。
またReactではclassをclassNameと記載します。return ( <div className="graduate"> <img className="graduate__image" src={this.props.image} onClick={() => {this.handleClickLesson()}} /> <div className="graduate__school">{this.props.school} 合格!</div> <div className="graduate__student">{this.props.name}</div> {modal} </div> );最後ですね。
この部分で常時表示させるHTMLをApp.jsからもらったpropsを使って作成しています。
そして{modal}
の部分で先ほどの変数を代入しているわけですね。imgクラスにonClickが設定されているため、画像をクリックするとisModalOpen
がtrueになり、値が代入されたmodalが表示されるわけです。
では最後にHTML側でApp.jsを呼んであげましよう。hamlの場合は
index.haml.haml= react_component("App")htmlの場合は
index.html.erb<%= react_component("App") %>これで完成です。
CSSだけ記載しておきます。
お好みで変更してください。stylesheet.css.performance { .graduate { padding: 30px 0; display: inline-block; width: 25%; text-align: center; &__student { font-size: 15px; padding-top: 10px; text-align: right; padding-right: 20px; } &__school { font-size: 20px; padding-top: 10px; } &__image { cursor: pointer; height: 160px; width: 160px; border-radius: 50%; } .modal-area { z-index: 2; position: fixed; top: 0; right: 0; bottom: 0; left: 0; background-color: rgba(0, 0, 0, 0.6); .modal-inner { position: absolute; top: 8%; right: 0; left: 0; width: 480px; padding-bottom: 60px; margin: auto; background-color: rgb(255, 255, 255); .modal-header { margin-bottom: 60px; } .modal-introduction p { color: #5876a3; width: 384px; line-height: 32px; text-align: left; margin: 36px auto 40px; } .modal-close-btn { font-size: 13px; color: #8491a5; width: 200px; padding: 16px 0; border: 0; background-color: #f0f4f9; cursor: pointer; } .modal-close-btn:hover { color: #8491a5; background-color: #ccd9ea; transition: .3s ease-in-out; } } } } }番外編
Click時にモーダルが表示されるが非表示にならない場合
モーダル実装時にモーダルが閉じないというバグが起こりました。
原因を調べてみると閉じるボタンを押した際に一回閉じてから再度開いています。
これがその時のコードです。<div className="graduate" onClick={() => {this.handleClickLesson()}}> <img className="graduate__image" src={this.props.image}/> <div className="graduate__school">{this.props.school} 合格!</div> <div className="graduate__student">{this.props.name}</div> {modal} </div>何が問題かというと一番の親要素であるgraduateにopenmodalを設定しているせいで、その子要素であるモーダル内のclossmodalを押した際に同時に親要素のopenmodalも呼ばれてしまうからです。
なのでそれぞれのClick機能は親要素、子要素の関係に注意しましょう。参考資料
React公式HP
Progate
https://qiita.com/k-penguin-sato/items/e3cc04f787cf3254cfae
https://qiita.com/kyrieleison/items/78b3295ff3f37969ab50
- 投稿日:2019-07-14T17:31:40+09:00
RailsとElasticsearchで検索機能をつくり色々試してみる - サジェスト機能の追加
はじめに
elasticsearch-railsを使うことが前提の記事になります。記事の中で出てくるサンプルや環境は、RailsとElasticsearchで検索機能をつくり色々試してみる - その1:サンプルアプリケーションの作成をもとにしています。
サジェストを実装する上でのElasticsearchのデータスキーマやアナライザー等の設計部分はElasticsearch キーワードサジェスト日本語のための設計の記事を参考にさせてもらっています。
本記事では、サジェスト機能をelasticsearch-railsを使ってRailsアプリケーションに追加していく方法に焦点を当てて紹介していきます。最終的なソースコードはGitHubに上げておきます。
完成イメージ
こんな感じで検索窓に2文字以上入力すると候補となるキーワードをサジェストする機能を追加します。
全体像
検索履歴を保存してサジェストワードとして使用
実際のアプリケーションを想定してユーザーの検索したキーワードをサジェストワードとして使用するようにします。
要件によっては検索してほしいキーワードをサジェストワードに登録していく方法などいろんな方法がが考えられますが今回はこの方法でいきます。Railsアプリを経由してサジェストワードを取得
ユーザーの環境からElasticsearchが公開されているような場合は、ブラウザから直接Elasticsearchにリクエストを送ることも考えられますが、今回はRailsアプリケーションのみからElasticsearchが公開されている場合を想定して、Railsアプリケーションを経由してサジェストワードを返却していきます。
検索ワードの保存
ここから実装に入っていきます。まずは検索ワードを保存するテーブルを追加し保存していきます。
後ほどElasticsearchにキーワードを登録する際に、検索にひっかからないワードはサジェストのワードからは除外できるように検索されたワードだけでなくhitした件数も保存するようにします。保存用のテーブル追加
$ bundle exec rails g migration create_search_word_logキーワードとhitした件数を保存するカラムを追加します。
db/migrate/20190601132134_create_search_word_log.rbclass CreateSearchWordLog < ActiveRecord::Migration[5.2] def change create_table :search_word_logs do |t| t.string :word t.integer :hit_number t.timestamps end end end$ bundle exec rails db:migrate対応するモデルを追加します。
app/models/search_word_log.rbclass SearchWordLog < ApplicationRecord end保存処理追加
コントローラに検索履歴をためるための処理を追加していきます。
app/controllers/mangas_controller.rbclass MangasController < ApplicationController before_action :set_manga, only: [:show, :edit, :update, :destroy] + after_action :save_search_log, only: [:index] def index @mangas = if search_word.present? Manga.es_search(search_word).page(params[:page] || 1).per(5).records else Manga.page(params[:page] || 1).per(5) end end private + def save_search_log + return if search_word.blank? + SearchWordLog.create(word: search_word, hit_number: @mangas.size) + endサジェスト用のindexの定義と検索クエリの作成
ここまででサジェストに登録するワードの準備できたので、Elasticsearchにどうのように登録していくかの定義と同時に検索のクエリを追加していきます。
検索機能で追加したものと同様にconcernを作成します。app/models/concerns/search_word_log_searchable.rbmodule SearchWordLogSearchable extend ActiveSupport::Concern included do include Elasticsearch::Model # a. サジェスト用のindex index_name "es_search_log_#{Rails.env}" # b. self.analyzer_settingsで下のほうに定義したanalyzerを使えるようにする。 settings analysis: self.analyzer_settings do mappings dynamic: 'false' do indexes :id, type: 'text', analyzer: 'kuromoji' # c. マルチフィールドを定義する indexes :word, type: 'text', fielddata: true, analyzer: 'keyword_analyzer' do indexes :autocomplete, type: 'text', analyzer: 'autocomplete_index_analyzer', search_analyzer: 'autocomplete_search_analyzer' indexes :readingform, type: 'text', analyzer: 'readingform_index_analyzer', search_analyzer: 'readingform_search_analyzer' end indexes :hit_number, type: 'integer' indexes :created_at, type: 'date', format: 'YYYY-MM-dd kk:mm:ss' end end def as_indexed_json(*) attributes .symbolize_keys .slice(:id, :word, :hit_number) .merge( created_at: created_at.strftime("%Y-%m-%d %H:%M:%S") ) end end class_methods do def create_index! client = __elasticsearch__.client client.indices.delete index: self.index_name rescue nil client.indices.create(index: self.index_name, body: { settings: self.settings.to_hash, mappings: self.mappings.to_hash }) end # d. 検索クエリの定義 def es_search(query) __elasticsearch__.search({ size: 0, query: { bool: { should: [{ match: { "word.autocomplete" => { query: query } } }, { match: { "word.readingform" => { query: query, fuzziness: "AUTO", operator: "and" } } }] } }, aggs: { keywords: { terms: { field: "word", order: { _count: "desc" }, size: "10" } } } }) end # e. analyzerの定義 def analyzer_settings { analyzer: { keyword_analyzer: { type: "custom", char_filter: ["normalize", "whitespaces"], tokenizer: "keyword", filter: ["lowercase", "trim", "maxlength"] }, autocomplete_index_analyzer: { type: "custom", char_filter: ["normalize", "whitespaces"], tokenizer: "keyword", filter: ["lowercase", "trim", "maxlength", "engram"] }, autocomplete_search_analyzer: { type: "custom", char_filter: ["normalize", "whitespaces"], tokenizer: "keyword", filter: ["lowercase", "trim", "maxlength"] }, readingform_index_analyzer: { type: "custom", char_filter: ["normalize", "whitespaces"], tokenizer: "japanese_normal", filter: ["lowercase", "trim", "readingform", "asciifolding", "maxlength", "engram"] }, readingform_search_analyzer: { type: "custom", char_filter: ["normalize", "whitespaces", "katakana", "romaji"], tokenizer: "japanese_normal", filter: ["lowercase", "trim", "maxlength", "readingform", "asciifolding"] }, }, filter: { readingform: { type: "kuromoji_readingform", use_romaji: true }, engram: { type: "edgeNGram", min_gram: 1, max_gram: 36 }, maxlength: { type: "length", max: 36 } }, char_filter: { normalize: { type: "icu_normalizer", name: "nfkc_cf", mode: "compose", }, katakana: { type: "mapping", mappings: [ "ぁ=>ァ", "ぃ=>ィ", "ぅ=>ゥ", "ぇ=>ェ", "ぉ=>ォ", "っ=>ッ", "ゃ=>ャ", "ゅ=>ュ", "ょ=>ョ", "が=>ガ", "ぎ=>ギ", "ぐ=>グ", "げ=>ゲ", "ご=>ゴ", "ざ=>ザ", "じ=>ジ", "ず=>ズ", "ぜ=>ゼ", "ぞ=>ゾ", "だ=>ダ", "ぢ=>ヂ", "づ=>ヅ", "で=>デ", "ど=>ド", "ば=>バ", "び=>ビ", "ぶ=>ブ", "べ=>ベ", "ぼ=>ボ", "ぱ=>パ", "ぴ=>ピ", "ぷ=>プ", "ぺ=>ペ", "ぽ=>ポ", "ゔ=>ヴ", "あ=>ア", "い=>イ", "う=>ウ", "え=>エ", "お=>オ", "か=>カ", "き=>キ", "く=>ク", "け=>ケ", "こ=>コ", "さ=>サ", "し=>シ", "す=>ス", "せ=>セ", "そ=>ソ", "た=>タ", "ち=>チ", "つ=>ツ", "て=>テ", "と=>ト", "な=>ナ", "に=>ニ", "ぬ=>ヌ", "ね=>ネ", "の=>ノ", "は=>ハ", "ひ=>ヒ", "ふ=>フ", "へ=>ヘ", "ほ=>ホ", "ま=>マ", "み=>ミ", "む=>ム", "め=>メ", "も=>モ", "や=>ヤ", "ゆ=>ユ", "よ=>ヨ", "ら=>ラ", "り=>リ", "る=>ル", "れ=>レ", "ろ=>ロ", "わ=>ワ", "を=>ヲ", "ん=>ン" ] }, romaji: { type: "mapping", mappings: [ "キャ=>kya", "キュ=>kyu", "キョ=>kyo", "シャ=>sha", "シュ=>shu", "ショ=>sho", "チャ=>cha", "チュ=>chu", "チョ=>cho", "ニャ=>nya", "ニュ=>nyu", "ニョ=>nyo", "ヒャ=>hya", "ヒュ=>hyu", "ヒョ=>hyo", "ミャ=>mya", "ミュ=>myu", "ミョ=>myo", "リャ=>rya", "リュ=>ryu", "リョ=>ryo", "ファ=>fa", "フィ=>fi", "フェ=>fe", "フォ=>fo", "ギャ=>gya", "ギュ=>gyu", "ギョ=>gyo", "ジャ=>ja", "ジュ=>ju", "ジョ=>jo", "ヂャ=>ja", "ヂュ=>ju", "ヂョ=>jo", "ビャ=>bya", "ビュ=>byu", "ビョ=>byo", "ヴァ=>va", "ヴィ=>vi", "ヴ=>v", "ヴェ=>ve", "ヴォ=>vo", "ァ=>a", "ィ=>i", "ゥ=>u", "ェ=>e", "ォ=>o", "ッ=>t", "ャ=>ya", "ュ=>yu", "ョ=>yo", "ガ=>ga", "ギ=>gi", "グ=>gu", "ゲ=>ge", "ゴ=>go", "ザ=>za", "ジ=>ji", "ズ=>zu", "ゼ=>ze", "ゾ=>zo", "ダ=>da", "ヂ=>ji", "ヅ=>zu", "デ=>de", "ド=>do", "バ=>ba", "ビ=>bi", "ブ=>bu", "ベ=>be", "ボ=>bo", "パ=>pa", "ピ=>pi", "プ=>pu", "ペ=>pe", "ポ=>po", "ア=>a", "イ=>i", "ウ=>u", "エ=>e", "オ=>o", "カ=>ka", "キ=>ki", "ク=>ku", "ケ=>ke", "コ=>ko", "サ=>sa", "シ=>shi", "ス=>su", "セ=>se", "ソ=>so", "タ=>ta", "チ=>chi", "ツ=>tsu", "テ=>te", "ト=>to", "ナ=>na", "ニ=>ni", "ヌ=>nu", "ネ=>ne", "ノ=>no", "ハ=>ha", "ヒ=>hi", "フ=>fu", "ヘ=>he", "ホ=>ho", "マ=>ma", "ミ=>mi", "ム=>mu", "メ=>me", "モ=>mo", "ヤ=>ya", "ユ=>yu", "ヨ=>yo", "ラ=>ra", "リ=>ri", "ル=>ru", "レ=>re", "ロ=>ro", "ワ=>wa", "ヲ=>o", "ン=>n" ] }, whitespaces: { type: "pattern_replace", pattern: "\\s{2,}", replacement: "\u0020" }, }, tokenizer: { japanese_normal: { mode: "normal", type: "kuromoji_tokenizer" }, engram: { type: "edgeNGram", min_gram: 1, max_gram: 36 } }, } end end end作成したconcernをモデルで使えるようにinclude
app/models/search_word_log.rbclass SearchWordLog < ApplicationRecord + include SearchWordLogSearchable end定義の解説
b. analyzerの登録
self.analyzer_settings
の箇所を追加することでanalyzer_settings
メソッド内に独自に定義したアナライザーを登録しています。c.マルチフィールドの定義
Elasticsearchへの登録、検索、読み仮名での登録、検索、検索結果の集計とキーワードの表示で別のAnalyzerを使いたいため、マルチフィールドを使いその中で登録と検索で別のAnalyzerを使うようにマッピングを定義しています。
それぞれの用途は以下の表のようになります。各Analyzerで具体的にどのようにキーワードがアナライズされるかの例を記事の最後に載せていますので詳しくはそちらを参照ください。
filed analyzer 用途 word keyword_analyzer 検索した結果の集計とサジェストとして表示する文字列を登録するためのアナライザー word.autocomplete autocomplete_index_analyzer 検索ワードを加工して登録していくためのアナライザー word.autocomplete autocomplete_search_analyzer 前方一致検索を行う際に使用するのアナライザー word.readingform readingform_index_analyzer 読み仮名でhitできるように検索ワードを加工して登録していくためのアナライザー word.readingform readingform_search_analyzer 読み仮名で前方一致検索を行う際に使用するのアナライザー それぞれどこで使用されているかは、全体像で使った図でいうと以下のようなイメージになります。
キーワード登録時のアナライザーのイメージ
検索時のアナライザーのイメージ
docker imageの修正
さきほどのanalyzerの定義でchar_filterに
icu_normalizer
を指定しているのでanalysis-icu
のプラグインを使えるように以下を追加してイメージをビルドしておきます。docker/es/DockerfileRUN bin/elasticsearch-plugin install analysis-icuindex追加
indexの定義が作成できたのでrakeタスクを追加してindexを作成します。
lib/tasks/elasticsearch.rakenamespace :elasticsearch do + desc 'サジェスト用のindex作成' + task :create_suggest_index => :environment do + SearchWordLog.create_index! + end end$ bundle exec rake elasticsearch:create_suggest_indexテスト用に検索履歴を追加
画面上でぽちぽち登録していくか、スクリプトを書いて
search_word_logs
テーブルにデータ登録していきます。とりあえずサンプルとして動かすなら数十件ほど追加すればOKかと思います。データがたたまったところでindexに登録していきます。
検索にhitしたキーワードのみをサジェストワードとして使いたいので、scopeを使用します。
app/models/search_word_log.rbclass SearchWordLog < ApplicationRecord include SearchWordLogSearchable + scope :searchable_word, -> { + where('hit_number > 0') + } endrakeタスクを登録して実行します。
rb:lib/tasks/elasticsearch.rakenamespace :elasticsearch do + desc 'サジェスト用のキーワードを登録' + task :import_suggest_word => :environment do + SearchWordLog.__elasticsearch__.import scope: 'searchable_word' + end endbundle exec rake elasticsearch:import_suggest_word
サジェストを返却するAPIの追加
GET /mangas/suggest?word={keyword}
でサジェストワードを取得できるように修正していきます。ルーティング追加
config/routes.rbRails.application.routes.draw do resources :mangas do + collection do + get :suggest + end end endコントローラーにアクション追加
app/controllers/mangas_controller.rbclass MangasController < ApplicationController ・・・ + def suggest + # concerns に追加した `es_search` メソッドで検索 + suggest_words = SearchWordLog.es_search(params[:word]).aggregations["keywords"]["buckets"] + render json: { suggest_words: suggest_words.map{|word| word["key"]} } + end private ・・・ endポイントとしては、今回定義した
es_search
メソッドではaggregationsで集計してhitした件数が多いキーワードを取得しているため、aggregations
メソッドを使うことで結果を取り出すことができます。
es_search
で以下のような形式のjsonが返却されaggregations["keywords"]["buckets"]
で対象のキーワードの配列を取得しています。{ "took" : 9, "timed_out" : false, "_shards" : { "total" : 5, "successful" : 5, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : 6, "max_score" : 0.0, "hits" : [ ] }, "aggregations" : { "keywords" : { "doc_count_error_upper_bound" : 0, "sum_other_doc_count" : 0, "buckets" : [ { "key" : "スライム", "doc_count" : 3 }, { "key" : "スラムダンク", "doc_count" : 3 } ] } } }View側の修正
最後にview側に修正を入れて行きます。今回はライブラリを使わずにシンプルなJavascriptを追加します。
js追加
app/assets/javascripts/suggest.jsdocument.addEventListener('turbolinks:load', function () { // turbolinksを使用しない場合はここは不要 let timeout = null; function selectSuggest(e) { document.querySelector("#search_word").value = e.target.textContent; document.querySelector(".SearchForm").submit(); } function displayNoneSuggestList () { let classList = document.querySelector(".dropdown").classList; if (classList.contains("is-active")) { classList.remove("is-active") } } function displaySuggestList() { let classList = document.querySelector(".dropdown").classList; if (!classList.contains("is-active")) { classList.add("is-active") } } // apiのレスポンスを元にhtmlを成形してサジェストリストを表示する function updateSuggestWords(words) { if (words.length > 0) { const dropdownMenu = document.querySelector("#dropdown-menu"); let wordList = ""; words.forEach(word => { wordList += `<div class="dropdown-item" role="option">${word}</div>`; }); const html = `<div id="sugget-list" class="dropdown-content">${wordList}</div>`; dropdownMenu.innerHTML = html; displaySuggestList(); const suggestList = document.querySelectorAll(".dropdown-item"); if (suggestList) { // サジェストワードが選択された場合にそのワードで検索を行うようにイベントリスナーに登録 suggestList.forEach(element => { element.addEventListener("click", selectSuggest); }); } } else { displayNoneSuggestList(); } } function getSuggetWords(e) { const value = e.target.value; clearTimeout(timeout); // 2文字以上入力された場合にサジェストワードを取得 if (value.length > 1) { // setTimeoutを使ってapiが呼ばれるまでに時間を置く timeout = setTimeout(function () { fetch(`http://localhost:3003/mangas/suggest?word=${value}`) .then(res => { return res.json(); }) .then(resJson => { updateSuggestWords(resJson.suggest_words); }) .catch(error => console.log(error)) }, 300); } else { displayNoneSuggestList() } } // 検索ワードの入力をイベントリスナーに登録 const inputWord = document.querySelector("#search_word"); inputWord.addEventListener("input", getSuggetWords); });解説
検索ワードが入力されたらapiからサジェストワードを取得してリストを更新するメソッド(
getSuggetWords
)をイベントリスナーに登録します。
getSuggetWords
では入力の度にapiが呼ばれてサジェストが何度も変わるのは使いづらいので、2文字以上入力された場合に0.3秒後にapiを呼ぶようにしています。
apiからのレスポンスをupdateSuggestWords
に渡してhtmlを成型してサジェストリストを追加しています。追加するリストにイベントリスナーを登録してキーワードが選択された場合にselectSuggest
メソッドを呼び検索を実行するようにしています。
このサンプルアプリではCSSは、bulmaを使用っているので、bulmaのdropdownのクラスを使ってスタイルを整えています。view修正
jsで使用するクラスや要素を追加し、jsを読み込みます。
app/views/mangas/index.html.erb<div class="container" style="margin-top: 30px"> - <%= form_tag(mangas_path, method: :get, class: "field has-addons has-addons-centered") do %> - <div class="control"> + <%= form_tag(mangas_path, method: :get, class: "field has-addons has-addons-centered SearchForm", autocomplete: "off") do %> + <div class="control SearchInput"> <%= text_field_tag :search_word, @search_word, class: "input", placeholder: "漫画を検索する" %> + <div class="dropdown"> + <div class="dropdown-menu" id="dropdown-menu" role="menu"> + </div> </div> </div> <div class="control"> <%= submit_tag "検索", class: "button is-info" %> </div> <% end %> </div> ・ ・ ・ + <%= javascript_include_tag 'suggest' %>追加したjsを読み込むためにinitializer修正
config/initializers/assets.rb+ Rails.application.config.assets.precompile += %w(suggest.js)以上で完成です!
おまけ:analyzerの動作確認
今回追加したanalyzerが実際にどのように動作するかを「転生」という単語を例に見ていきます。
documentの登録
まずはdocument登録する際のanalyzerの動きを確認します。
「転生」という単語を登録する場合以下のようにanalyzeされてindexされています。それぞれのAnalyzer APIでの確認結果は以下のようになります。
keyword_analyzer
requestPOST /es_search_log_development/_analyze { "analyzer": "keyword_analyzer", "text": "転生" }response{ "tokens" : [ { "token" : "転生", "start_offset" : 0, "end_offset" : 2, "type" : "word", "position" : 0 } ] }autocomplete_index_analyzer
requestPOST /es_search_log_development/_analyze { "analyzer": "autocomplete_index_analyzer", "text": "転生" }resoponse{ "tokens" : [ { "token" : "転", "start_offset" : 0, "end_offset" : 2, "type" : "word", "position" : 0 }, { "token" : "転生", "start_offset" : 0, "end_offset" : 2, "type" : "word", "position" : 0 } ] }readingform_index_analyzer
requestPOST /es_search_log_development/_analyze { "analyzer": "readingform_index_analyzer", "text": "転生" }response{ "tokens" : [ { "token" : "t", "start_offset" : 0, "end_offset" : 2, "type" : "word", "position" : 0 }, { "token" : "te", "start_offset" : 0, "end_offset" : 2, "type" : "word", "position" : 0 }, { "token" : "ten", "start_offset" : 0, "end_offset" : 2, "type" : "word", "position" : 0 }, { "token" : "tens", "start_offset" : 0, "end_offset" : 2, "type" : "word", "position" : 0 }, { "token" : "tense", "start_offset" : 0, "end_offset" : 2, "type" : "word", "position" : 0 }, { "token" : "tensei", "start_offset" : 0, "end_offset" : 2, "type" : "word", "position" : 0 } ] }documentのsearch
続いてsearch用のanalyzerの動きを見ます。
今度は「転生」、「てんせ」、「ten」のワードで確認します。
以下のイメージのようにanalyzeされています。autocomplete_search_analyzer
「転生」
requestPOST /es_search_log_development/_analyze { "analyzer": "autocomplete_search_analyzer", "text": "転生" }{ "tokens" : [ { "token" : "転生", "start_offset" : 0, "end_offset" : 2, "type" : "word", "position" : 0 } ] }「てんせ」
requestPOST /es_search_log_development/_analyze { "analyzer": "autocomplete_search_analyzer", "text": "てんせ" }{ "tokens" : [ { "token" : "てんせ", "start_offset" : 0, "end_offset" : 3, "type" : "word", "position" : 0 } ] }「ten」
requestPOST /es_search_log_development/_analyze { "analyzer": "autocomplete_search_analyzer", "text": "ten" }{ "tokens" : [ { "token" : "ten", "start_offset" : 0, "end_offset" : 3, "type" : "word", "position" : 0 } ] }readingform_search_analyzer
「転生」
POST /es_search_log_development/_analyze { "analyzer": "readingform_search_analyzer", "text": "転生" }{ "tokens" : [ { "token" : "tensei", "start_offset" : 0, "end_offset" : 2, "type" : "word", "position" : 0 } ] }「てんせ」
POST /es_search_log_development/_analyze { "analyzer": "readingform_search_analyzer", "text": "てんせ" }{ "tokens" : [ { "token" : "tense", "start_offset" : 0, "end_offset" : 3, "type" : "word", "position" : 0 } ] }「ten」
POST /es_search_log_development/_analyze { "analyzer": "readingform_search_analyzer", "text": "ten" }{ "tokens" : [ { "token" : "ten", "start_offset" : 0, "end_offset" : 4, "type" : "word", "position" : 0 } ] }サジェストワード検索時のイメージ
indexとsearchのanalyzerの動きをふまえると、「転生」をindexして「転生」、「てんせ」、「ten」を入力した場合、全て「転生」がサジェストされることがわかります。
- 投稿日:2019-07-14T16:38:33+09:00
kaminariのページネーションを中央寄せにする
- kaminariを導入しているとビューで
pagination
メソッドを呼ぶと思いますが、その場合、自動的にul
要素にpagination
クラスが適用されます。pagination
クラスのCSSを以下のようにカスタマイズすると、ページネーションを中央寄せにすることができます。app/assets/stylesheets/application.scss.pagination { justify-content: center; }
- 投稿日:2019-07-14T15:31:49+09:00
わたしがRailsチュートリアルで学んだこと【7章】
注意:プログラミング歴34日の初心者が書いています
注意:間違っていたら優しく教えてください(喜びます)
「Ruby on Rails チュートリアル実例を使ってRailsを学ぼう」
https://railstutorial.jp/素晴らしいチュートリアルに感謝します。
8.1 セッション
HTTPは、その場限り。以前の情報を全く持たないリクエストです。
そのため、ログイン情報の保持には、セッションという接続を使います。Railsでセッションを実装する方法として最も一般的なのは、cookiesを使う方法です。cookiesとは、ユーザーのブラウザに保存される小さなテキストデータです。
セッションについても例に漏れず、Sessionsコントローラで操作します。
コントローラを追加したら、
routes.rb
に対応するルーティングを追加します。今回は、
new 新しいセッションのページ (ログイン画面)
create 新しいセッションの作成 (実際のログイン)
destroy セッションの削除 (ログアウト)
という3つのアクションを、
/login
というURLに紐づけます。
- 投稿日:2019-07-14T15:31:49+09:00
わたしがRailsチュートリアルで学んだこと【8章】
注意:プログラミング歴34日の初心者が書いています
注意:間違っていたら優しく教えてください(喜びます)
「Ruby on Rails チュートリアル実例を使ってRailsを学ぼう」
https://railstutorial.jp/素晴らしいチュートリアルに感謝します。
8.1 セッション
HTTPは、その場限り。以前の情報を全く持たないリクエストです。
そのため、ログイン情報の保持には、セッションという接続を使います。Railsでセッションを実装する方法として最も一般的なのは、cookiesを使う方法です。cookiesとは、ユーザーのブラウザに保存される小さなテキストデータです。
セッションについても例に漏れず、Sessionsコントローラで操作します。
コントローラを追加したら、
routes.rb
に対応するルーティングを追加します。今回は、
new 新しいセッションのページ (ログイン画面)
create 新しいセッションの作成 (実際のログイン)
destroy セッションの削除 (ログアウト)
という3つのアクションを、
/login
というURLに紐づけます。8.1.2 ログインフォーム
フォームに入力されたログイン情報を保存したい、でもセッションにはUserのときのようなモデル、データベースはありません。
form_for(:session, url: login_path)そのため上記のように、データを保存するリソース名(ここでは
:session
)とそのパスを指定ながら、form_for
でformタグを生成します。rb <%= form_for(:session, url: login_path) do |f| %> <%= f.label :email %> <%= f.email_field :email, class: 'form-control' %> <%= f.label :password %> <%= f.password_field :password, class: 'form-control' %> <%= f.submit "Log in", class: "btn btn-primary" %> <% end %>送信されたデータは、
params[:session][:email]
params[:session][:password]
で受け取ることができます。こうしてみると、リソース名(ここでは
:session
)をシンボルで記述する意味がわかりますね。"session" => { "email" => "foo@bar.com", "password" => "something", }送信されるデータはこういうハッシュになってるということですね。
これが、
login_path
のPOST、つまりcreate
アクションに受け渡されるということです。8.1.3 ユーザーの検索と認証
session
コントローラのcreate
アクションでparamsからデータを受け取ることができます。
render
とredirect_to
について
flash
で表示させるエラーメッセージは、redirect_to
の場合は次のページ移動で消えるのに、rederではページ移動しても消えずに残ったままになってしまう。renderを使用する場合は
flash.now
を使う。renderとは
render
は指定したviewを呼び出します。def new endこれも実は
render
が動いています。
GETアクション内になにも記述がない場合は、def new render 'new' endに勝手に補完されています。
redirect_toとは
redirect_to
はHTTPリクエスト(GET)をサーバーに送ります。def create @user = User.new(user_params) if @user.save flash[:success] = "Welcome to the Sample App!" redirect_to @user else render 'new' end endわかりにくいですが、ここでもRailsの自動補完が効いているのでした。
redirect_to
の行は、以下と同じ意味です。redirect_to user_url(@user)8.1.5 フラッシュのテスト
require 'test_helper' class UsersLoginTest < ActionDispatch::IntegrationTest test "login with invalid information" do get login_path #1 assert_template 'sessions/new' #2 post login_path, params: { session: { email: "", password: "" } } #3 assert_template 'sessions/new' #4 assert_not flash.empty? #4 flashはemptyでない get root_path #5 assert flash.empty? #6 flashはemptyである end end
- ログイン用のパスを開く
- 新しいセッションのフォームが正しく表示されたことを確認する
- わざと無効なparamsハッシュを使ってセッション用パスにPOSTする
- 新しいセッションのフォームが再度表示され、フラッシュメッセージが追加されることを確認する
- 別のページ (Homeページなど) にいったん移動する
- 移動先のページでフラッシュメッセージが表示されていないことを確認する
8.2 ログイン
ApplicationコントローラにSessionヘルパーモジュールを読み込む
ApplicationコントローラにSessionヘルパーモジュールを読み込むと、他の全てのコントローラで使用できるようになります。
それは、他の全てのコントローラが、Applicationコントローラを継承しているからです。8.2.1 log_inメソッド
sessionメソッドについて
session[:name]sessionメソッドはRailsにあらかじめ組み込まれているメソッドです。
名前をつけて、セッションに登録できます。これは、8章冒頭に自分で
rails -g
して作ったSessionsコントローラとは無関係です。module SessionsHelper # 渡されたユーザーでログインする def log_in(user) session[:user_id] = user.id #user.idを`:user_id`という名前で保存 end endどのviewでも使うことができるように、ヘルパーに関数を定義します。
sessionメソッドで作成した一時cookiesは自動的に暗号化されます。すごい。ブラウザからcookiesの情報を調べてみてください。の演習について
AWS Cloud9を使っている人は、
amazonaws
でcookies検索すると出るかと思います。8.2.2 現在のユーザー
「例外を発生させる」or「例外を発生させない」
User.find(session[:user_id]) #例外が発生する User.find_by(id: session[:user_id]) ##例外が発生しない(nillを返す)
find
メソッドは例外を発生させて処理を中断する。
find_by
メソッドはnilを返すため処理は中断されない。「ユーザページの表示」の場合は、ユーザーのidが無いのは「例外」です。(表示するページもなにもない)
一方、「ユーザーのログイン」の場合、まだログインしていないなど、クッキーにデータがあったりなかったり、両方の状態が考えられます。そのため、「例外」での処理はしません。
現在のユーザーを定義する
def current_user if session[:user_id] @current_user ||= User.find_by(id: session[:user_id]) end end「もし、
session[:user_id]
が存在するなら、
@current_user
がnill
ならUser.find_by(id: session[:user_id])
を代入する」@current_user ||= User.find_by(id: session[:user_id])上記は、以下と同じです。
if @current_user.nil? @current_user = User.find_by(id: session[:user_id]) else @current_user endまず、
@current_user
がnil
かどうか(値があるかどうか)チェック。
@current_user
がnil
(つまりログインしてない)なら、セッションIDからuserを検索して@current_user
とします。もし
@current_user
がnil
でないなら、すでにログイン中ですので、そのまま@current_user
を返します。8.2.3 レイアウトリンクを変更する
ログインしている場合orしていない場合で、レイアウトを変更します。
view内で、if
を使って表示する内容を変える。
if
の条件分岐を使用して、current_user
がtrueかfalseか(ログインしているか否か)で分岐させます。ユーザーがログイン中の状態とは「sessionにユーザーidが存在している」こと、つまりcurrent_userがnilではないという状態を指します。
# ユーザーがログインしていればtrue、その他ならfalseを返す def logged_in? !current_user.nil? endログインしているかどうかを確かめるメソッドをヘルパーに定義して、便利にすぐ呼び出せるようにしておきます。
true
やfalse
など真偽値を返すメソッドは名前に「?」をつけて置くのがルールです。リスト 8.21: fixture向けのdigestメソッドを追加する
全く意味がわかりません。誰かたすけてください。
8.3 ログアウト
session.delete(:user_id)上記の
delete
メソッドでセッションを削除できます。
- 投稿日:2019-07-14T14:03:31+09:00
bin/rails g kaminari:views bootstrap4 を叩くと No such file or directory @ rb_sysopen のエラーになる
事象
- kaminariを導入した後に、Bootstrapの見た目にしたいということで
bin/rails g kaminari:views bootstrap4
を叩いてファイル生成するのですが、以下のようなエラーとなりました。$ bin/rails g kaminari:views bootstrap4 Running via Spring preloader in process 2526 Traceback (most recent call last): 37: from -e:1:in `<main>' 36: from /Users/user_name/.rbenv/versions/2.5.1/lib/ruby/2.5.0/rubygems/core_ext/kernel_require.rb:59:in `require' 35: from /Users/user_name/.rbenv/versions/2.5.1/lib/ruby/2.5.0/rubygems/core_ext/kernel_require.rb:59:in `require' 34: from /Users/user_name/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activesupport-5.2.3/lib/active_support/dependencies.rb:285:in `load' 33: from /Users/user_name/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activesupport-5.2.3/lib/active_support/dependencies.rb:257:in `load_dependency' 32: from /Users/user_name/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activesupport-5.2.3/lib/active_support/dependencies.rb:285:in `block in load' 31: from /Users/user_name/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:54:in `load' 30: from /Users/user_name/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:54:in `load' 29: from /Users/user_name/book_match/bin/rails:9:in `<top (required)>' 28: from /Users/user_name/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activesupport-5.2.3/lib/active_support/dependencies.rb:291:in `require' 27: from /Users/user_name/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activesupport-5.2.3/lib/active_support/dependencies.rb:257:in `load_dependency' 26: from /Users/user_name/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activesupport-5.2.3/lib/active_support/dependencies.rb:291:in `block in require' 25: from /Users/user_name/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:30:in `require' 24: from /Users/user_name/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:21:in `require_with_bootsnap_lfi' 23: from /Users/user_name/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/loaded_features_index.rb:92:in `register' 22: from /Users/user_name/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:22:in `block in require_with_bootsnap_lfi' 21: from /Users/user_name/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:22:in `require' 20: from /Users/user_name/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/railties-5.2.3/lib/rails/commands.rb:18:in `<top (required)>' 19: from /Users/user_name/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/railties-5.2.3/lib/rails/command.rb:46:in `invoke' 18: from /Users/user_name/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/railties-5.2.3/lib/rails/command/base.rb:65:in `perform' 17: from /Users/user_name/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/thor-0.20.3/lib/thor.rb:387:in `dispatch' 16: from /Users/user_name/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/thor-0.20.3/lib/thor/invocation.rb:126:in `invoke_command' 15: from /Users/user_name/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/thor-0.20.3/lib/thor/command.rb:27:in `run' 14: from /Users/user_name/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/railties-5.2.3/lib/rails/commands/generate/generate_command.rb:26:in `perform' 13: from /Users/user_name/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/railties-5.2.3/lib/rails/generators.rb:276:in `invoke' 12: from /Users/user_name/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/thor-0.20.3/lib/thor/base.rb:466:in `start' 11: from /Users/user_name/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/thor-0.20.3/lib/thor/group.rb:232:in `dispatch' 10: from /Users/user_name/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/thor-0.20.3/lib/thor/invocation.rb:133:in `invoke_all' 9: from /Users/user_name/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/thor-0.20.3/lib/thor/invocation.rb:133:in `map' 8: from /Users/user_name/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/thor-0.20.3/lib/thor/invocation.rb:133:in `each' 7: from /Users/user_name/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/thor-0.20.3/lib/thor/invocation.rb:133:in `block in invoke_all' 6: from /Users/user_name/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/thor-0.20.3/lib/thor/invocation.rb:126:in `invoke_command' 5: from /Users/user_name/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/thor-0.20.3/lib/thor/command.rb:27:in `run' 4: from /Users/user_name/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/kaminari-0.17.0/lib/generators/kaminari/views_generator.rb:27:in `copy_or_fetch' 3: from /Users/user_name/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/kaminari-0.17.0/lib/generators/kaminari/views_generator.rb:39:in `themes' 2: from /Users/user_name/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/kaminari-0.17.0/lib/generators/kaminari/views_generator.rb:99:in `get_files_in_master' 1: from /Users/user_name/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/kaminari-0.17.0/lib/generators/kaminari/views_generator.rb:99:in `open' /Users/user_name/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/kaminari-0.17.0/lib/generators/kaminari/views_generator.rb:99:in `initialize': No such file or directory @ rb_sysopen - https://api.github.com/repos/amatsuda/kaminari_themes/git/refs/heads/master (Errno::ENOENT)対応策
- 参考:rails g kaminari:views bootstrap3が実行できない
- こちらのページを参考に、また、以前別プロジェクトでkaminariを導入していた時のライブラリのジェネレーターファイルと比較してみたところ、以下のような
require 'open-uri'
の記述がありませんでした。- ということで、
require 'open-uri'
を以下のように追記してから再度実行してみると、上手くいきました。lib/generators/kaminari/views_generator.rbmodule Kaminari module Generators require 'open-uri' # この行を追記 class ViewsGenerator < Rails::Generators::NamedBase$ bin/rails g kaminari:views bootstrap4 Running via Spring preloader in process 2545 downloading app/views/kaminari/_first_page.html.slim from kaminari_themes... create app/views/kaminari/_first_page.html.slim downloading app/views/kaminari/_gap.html.slim from kaminari_themes... create app/views/kaminari/_gap.html.slim downloading app/views/kaminari/_last_page.html.slim from kaminari_themes... create app/views/kaminari/_last_page.html.slim downloading app/views/kaminari/_next_page.html.slim from kaminari_themes... create app/views/kaminari/_next_page.html.slim downloading app/views/kaminari/_page.html.slim from kaminari_themes... create app/views/kaminari/_page.html.slim downloading app/views/kaminari/_paginator.html.slim from kaminari_themes... create app/views/kaminari/_paginator.html.slim downloading app/views/kaminari/_prev_page.html.slim from kaminari_themes... create app/views/kaminari/_prev_page.html.slim
- 投稿日:2019-07-14T11:37:33+09:00
いいね機能を非同期で実装【Rails】【jQuery】
この記事の内容
TODOリストを共有できるアプリを作っていて、いいね機能を非同期にて実装しました。
すでにたくさんのQiita記事がありますが、つまったポイントもあったので、自分なりにまとめ直してみます。
(コンセプトは「人生でやりたいこと100のリストの共有」なので、todoをdreamという言葉を使って表現しています。)前提
Rails 5.2.3
構成
userの詳細ページにdreamリストが表示されています。
viewの構成としては、
/views/users/show.html.erb
内で同じ階層の_dream.heml.erb
が部分テンプレートとして呼ばれ、userのdreamを繰り返し表示しています。CSSフレームワークはMaterializeを使用しています。
アソシエーション:
users - has_many :dreams, has_many :likes
dreams - belongs_to :user, has_many :likes
likes - belongs_to :user, belongs_to :dream流れ
- jQueryの準備
- いいねボタンを作成
- コントローラ記述
- remote: trueにてjs.erbファイルを呼び出し
- js.erbファイル作成
実行
1. jQueryの準備
非同期化するにあたり、Rails内でjQueryを使えるように準備します。
まずはgemの導入です。Gemfilegem 'jquery-rails'ターミナルで
bundle install
します。
そしてapplication.jsに記述を追加します。app/assets/javascripts/application.js//= require jquery3 //= require rails-ujs //= require_tree .順番が重要です。jqueryを最初に読み込む必要があります。
2. いいねボタンを作成
後から使い回ししやすいように、部分テンプレート化しています。
_dream.html.erb
ではテーブルでtodoリストを表示しているので、いいねの項目がtd内に入っています。
id
の記載については5の項目で説明します。app/views/users/_dream.html.erb# いいね機能該当部分 <td id="like-<%= dream.id %>"> <%= render partial: "like", locals: { dream: dream } %> </td>
_like.html.erb
ではすでにいいねがあるかないかで★か☆かを出し分けて(Materializeのアイコンを使用しています)、最後にいいね数をdream.likes.length
で表示しています。app/views/users/_like.html.erb<% if Like.find_by(user_id: current_user.id, dream_id: dream.id) %> <%= link_to "/dreams/#{dream.id}/likes", method: :delete %> <i class="material-icons">star</i> <% end %> <% else %> <%= link_to "/dreams/#{dream.id}/likes", method: :post %> <i class="material-icons">star_border</i> <% end %> <% end %> <%= dream.likes.length %>3. コントローラ記述
いいねのcreateとdestroyを定義していきます。
app/controllers/likes_controller.rbclass LikesController < ApplicationController before_action :set_dream def create @like = Like.create(user_id: current_user.id, dream_id: @dream.id) end def destroy @like = Like.find_by(user_id: current_user.id, dream_id: @dream.id) @like.destroy end private def set_dream @dream = Dream.find(params[:dream_id]) end endルーティングも忘れずに。
config/routes.rbpost '/dreams/:dream_id/likes' => "likes#create" delete '/dreams/:dream_id/likes' => "likes#destroy"この時点で、非同期ではないですがいいね機能が実装できているはず。
4. remote: trueにてjs.erbファイルを呼び出し
いいねボタンの
link_to
にremote: true
を追加します。app/views/users/_like.html.erb<% if Like.find_by(user_id: current_user.id, dream_id: dream.id) %> <%= link_to "/dreams/#{dream.id}/likes", method: :delete, remote: true do %> <i class="material-icons">star</i> <% end %> <% else %> <%= link_to "/dreams/#{dream.id}/likes", method: :post, remote: true do %> <i class="material-icons">star_border</i> <% end %> <% end %> <%= dream.likes.length %>この記述により、通常であれば、
link_to
で呼ばれるアクションに対応するhtml.erbファイルを呼び出すところ、js.erbファイルを呼び出せるようになります。
なのでページ遷移を行わず非同期で通信が行われるようになります。5. js.erbファイル作成
js.erbファイルはその名前の通り、javascriptのファイルでありながら、ERBタグを使うことでrubyのコードを書ける優れものです。
コントローラーで定義したインスタンスを<% %>
、<%= %>
を使うことでそのままjsファイルに記述できます。まずはcreateとdestroyそれぞれのアクションに対応するjs.erbファイルを作成します。
app/views/likes/create.js.erb
app/views/likes/destroy.js.erb
ここで2.で記述していた
id
について説明します。app/views/users/_like.html.erb(再掲)<td id="like-<%= dream.id %>"> <%= render partial: "like", locals: { dream: dream } %> </td>jsでイベントを発火させるためには、idまたはclassでセレクタを指定しますが、今回はどのdreamに対するいいねなのかを判別するために、id内にそのdreamのidを含める必要があります。
上のように書くことによって、id="like-1"
のようなidを指定することができます。ここまで来たらあとはjs.erbファイルの記述だけです。
app/views/likes/create.erb$("#like-<%= @dream.id %>").html("<%= j(render partial: 'users/like', locals: { dream: @dream }) %>");app/views/likes/destroy.erb$("#like-<%= @dream.id %>").html("<%= j(render partial: 'users/like', locals: { dream: @dream }) %>");これだけです。
まずはセレクタの指定ですが、js.erbファイルなのでコントローラで定義した変数が使えます。
id="like-<%= dream.id %>"
に対応するように指定します。そして、jQueryのhtml()メソッドで、指定したセレクタのhtmlを置き換えます。
その置き換える内容が
"<%= j(render partial: 'users/like', locals: { dream: @dream }) %>"
の部分です。
部分テンプレートの_like.html.erb
を呼び出しています。
(renderの前にあるj
は、escape_javascript
のエイリアスで、改行と括弧をエスケープしてくれるメソッドです。)これにより、likeが更新された状態で、ifで条件分岐されたりlikes.countが表示されたりします。
↓
非同期処理の実現!一度流れをつかむことができれば応用が効きそうなので、今後もいろいろなところで使ってみようと思います。
参考
Railsで remote: true と js.erbを使って簡単にAjax(非同期通信)を実装しよう!(いいね機能のデモ付)
分かりにくい点・間違っている点などがありましたらご指摘いただきますよう、よろしくお願いいたします。
- 投稿日:2019-07-14T09:20:35+09:00
Carrierwaveの導入で手間取ったので忘備録。
1.まずは公式ページを。
https://github.com/carrierwaveuploader/carrierwave#installation
2.Imagemagickのインストール
$ brew install imagemagick3.Gemのインストール
$ gem install carrierwave4.Gemfileの編集
gem 'carrierwave' gem 'mini_magick'からの、、bundel install
5.postsテーブルに image column を追加
$ rails g migration add_image_to_posts image:string6.uploader ファイルを作成
$ rails g uploader image7.models/post.rb を編集
以下を追加
mount_uploader :image, ImageUploader8.posts_controller のストロングパラムズを編集
:image,:image_cache を追加
def post_params params.require(:post).permit(:title,:content,:image,:image_cache) end9.image_uploader の編集
include CarrierWave::MiniMagick storage :file def store_dir "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" end # 画像の上限を700pxにする process :resize_to_limit => [700, 700] # jpg,jpeg,gif,pngしか受け付けない def extension_white_list %w(jpg jpeg gif png) end10.new page
<div> <%= f.label :image %> <%= image_tag @post.image.url if @post.image? %> <%= f.file_field :image %> <%= f.hidden_field :image_cache %> </div>11.confirm page
<h1>Are you ok?</h1> <%=form_with(model:@post,url:posts_path,local:true)do |f|%> <%=f.hidden_field :title%> <%=f.hidden_field :content%> <%=f.hidden_field :image_cache%> <p>Title:<%=@post.title%></p> <p>Content:<%=@post.content%></p> <p>Image:<%=@post.image.url %></p> <%=f.submit 'OK!'%> <% end %> <%=form_with(model:@post,url:new_post_path,local:true,method:'get')do |f|%> <%=f.hidden_field :title%> <%=f.hidden_field :content%> <%=f.hidden_field :image_cache%> <%=f.submit 'Back!',name:'back'%> <% end %>12.index page
<div> <% if post.image? %> <%= image_tag post.image.url %> <% else %> <p>No Image</p> <% end %> </div>13.show page
<div> <% if @post.image? %> <%= image_tag @post.image.url %> <% else %> <p>No Image</p> <% end %> </div>以上です。
注意点)
当初、以下のようなサムネイル作成メソッドをimage_uploaderに入れていたが、それが原因で日本語名の画像ファイルが文字化けしてエラーとなっていた。
これを削除すると日本語名の画像ファイルも正常にアップロードできるようになった。version :thumb do process :resize_to_limit => [300, 300] end
- 投稿日:2019-07-14T01:20:09+09:00
herokuにアップする画像を永続的に表示させる方法【Rails】
heroku に画像をアップできるアプリケーションをデプロイしたとしても、アップした画像は数時間で消えるかデプロイするたびに消えてしまいます。
しかし、画像が消えるということを回避できる方法があります。それは、画像をデータベースに格納するときに
バイナリデータ
として保存する方法です。ですが、この記事の方法は「モックアップ用の簡易なアプリケーション」や「アクセスがなく自分しか使わないアプリケーション」といったケースでやるのが好ましいです。
下記の注意点を読んでから、アプリケーションの状況に応じて実施を決断するようにしてください。注意点
以下のサイトの回答者の方の答えが非常にわかりやすくて、画像をデータベースに保存するデメリット(とメリット)についてまとめられているので、参考にしてみてください。
データベースに画像を保存するのはありでしょうか? - teratail
テーブルを作成
今回は、既存の
Postテーブル
に紐付けしたPhotoテーブル
のなかに画像を保存していくとします。
Postテーブル
とPhotoテーブル
の関係は、1対多です。まずは、画像を保存するためのテーブルの作成から行います。
画像を保存するためのカラムは、bynary型
にします。
コンソールに下記のコマンドを打ち込み実行します。$ rails g model Photo
そうすることで、マイグレーションファイルが生成されるので下記のように編集します。
xxxxxxxxxxxxxx_create_photos.rbclass CreateUsers < ActiveRecord::Migration def change create_table : photos do |t| t.bynary :image, nill: false t.references :post, foreign_key: true, null: false t.timestamps null: false end end endマイグレーションファイルを編集したら、下記のコマンドを実行してテーブルを作成しましょう。
$ rails db:migrate
作成した全てのテーブル情報は「schema.rb」ファイルでも確認することができます。
コントローラを編集
コントーラファイルには、「画像をデータベースに保存する」と「画像をデータベースから取り出す」といった2つの動きを追加していきます。
画像をデータベースに保存する
画像をデータベースに保存するには、
read
というメソッドが重要になります。
「photos_controller.rb」ファイルを作成して、下記を追加します。photos_controller.rbdef create @post = Post.new(post_params) # バイナリ化した画像の呼び出し params[:post][:photos_attributes]["0"][:image].each_with_index do |item, i| if i != 0 @post.photos.build end @post.photos[i].image = item.read end end private def post_params params.require(:post).permit(photos_attributes: [:image]) end画像をデータベースから取り出す
画像をデータベースから取り出すには、
send_data
というメソッドが重要です。photos_controller.rbdef send_img tmpbin = Photo.find(params[:id]) send_data(tmpbin.image, :type => 'image/jpeg', :disposition => 'inline') endビューを編集
ビューには、「画像のアップロード」と「画像の表示」とこれまた2つの動きを追加していきます。
画像のアップロード
画像をアップできるようにフォームを設置します。
new.html.erb<%= form_with model: @post, multipart: true do |f| %> <%= f.field_for :photos do |i| %> <%= i.file_field :image, type: :file %> <% end %> <%= f.submit "投稿する" %> <% end %>画像の表示
次に、データベースに保存した画像が表示されるように下記を追加します。
index.html.erb<% @post.photos.each do |photos| %> <%= image_tag url_for(:controller => 'photos', :action => 'send_img', id => photos.id %> <% end %>ルートの設定を追加
最後の仕上げとして、「routes.rb」に下記を追加します。
routes.rbget '/photos/send_img/:id', to: 'photos#send_img'これでバイナリデータとした画像の保存から表示までを終わらせることができました。
heroku にデプロイするアプリケーションで画像投稿機能が付いているものでも、永続的に画像を表示させ続けられることでしょう!参考にさせてもらった記事