20210724のRailsに関する記事は18件です。

"after_sign_in_path_for" device実装 ログイン後の遷移先を指定する

"after_sign_in_path_for" の使い方について 使用例 application_controller protected def after_sign_in_path_for(resource) root_path end 説明 「after_sign_in_path_for」の役割は、ログイン後に遷移先を指定することができます。 今回はroot_pathをしているので、ログイン後ホーム画面に遷移します。 今回出てきた単語について ・「protected」 ・「(resourse)」 ・「root_path」
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

"after_sign_in_path_for" device実装 ログイン後の遷移先を指定する[Ruby on Rails]

"after_sign_in_path_for" の使い方について 使用例 application_controller protected def after_sign_in_path_for(resource) root_path end 説明 「after_sign_in_path_for」の役割は、ログイン後に遷移先を指定することができます。 今回はroot_pathなので、ログイン後ホーム画面に遷移します。 今回出てきた単語について ・protected ・(resourse) ・root_path
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

herokuデプロイで環境変数関連で積んだ話

開発環境 ruby:3.0.1 rails:6.0.4 heroku Mysql 起こったこと ruby on railsにてアプリケーションを作成して一度テストでherokuにデプロイしてみようと思い下記コマンドを実行 git push heroku master とワクワクしながらデプロイしてみて無事に成功したと思い、さっそくURLをたたいてみたところ We're sorry, but something went wrongとでかでかと表示されて「いやなんでだよ!」と思いながら、とりあえず正しく表示されないってことはどこか間違いやエラーがているのではと思い、デプロイした際に出てくる細かい文字は読み返して見たところ気になる記述がありました。 remote: -----> Preparing app for Rails asset pipeline remote: Running: rake assets:precompile remote: rake aborted! remote: NoMethodError: undefined method `[]' for nil:NilClass ##省略 remote: ! remote: ! Precompiling assets failed. remote: ! remote: ! Push rejected, failed to compile Ruby app. remote: remote: ! Push failed remote: Verifying deploy... remote: remote: ! Push rejected to hogehoge. remote: To https://git.heroku.com/hogehoge.git ! [remote rejected] master -> master (pre-receive hook declined) error: failed to push some refs to 'https://git.heroku.com/appname.git' 「え?なにこれ?」って一瞬思いましたが、よくよく見てみるとNoMethodError: undefined method `[]' for nil:NilClassと表示されており本番環境では aws s3に画像を保存するように記述しており、そこで設定した環境変数をちゃんと代入できないと思い下記コードを実行 rails c これでAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_S3_REGION, AWS_S3_BUCKETのすべての環境変数がちゃんと設定できているか確認しました。 つまり環境変数はちゃんと定義されており、問題ないと思い再度プッシュしてみましたが、やはり先ほどと同じエラーが出ます。 そしてherokuにおいてある環境変数を全部大文字から小文字にして、再度プッシュしたらちゃんとアプリケーションが表示されました。 以上です、まだまだ知識不足だと痛感しました・・・
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Ruby]曜日の設定

はじめに 本記事では、初学者なりに私が何度も何時間も考え、 ようやく解消したものになります。 備忘録として残したいと思いましたので投稿いたします。 曜日の設定 私がトライしたものは、 以下の画像の曜日を設置するものでした。 *すでに完成したものを添付しております。ちょっと遊んでましたw 回答(曜日追加部分についての回答) コントローラー def get_week wdays = ['日','月','火','水','木','金','土'] @week_days = [] @todays_date = Date.today plans = Plan.where(date: @todays_date..@todays_date + 6) 7.times do |x| today_plans = [] plans.each do |plan| today_plans.push(plan.plan) if plan.date == @todays_date + x end wday_num = Date.today.wday + x if wday_num >= 7 wday_num = wday_num -7 end days = {month: (@todays_date + x).month, date: (@todays_date + x).day, plans: today_plans, wday: wdays[wday_num]} @week_days.push(days) end end end ビュー <div class='calendar'> <% @week_days.each do |day| %> <div class='item'> <div class='date'> <%= day[:month] %>/<%= day[:date] %>・<%= day[:wday] %> </div> <ul class='content'> <% if day[:plans].length != 0 %> <% day[:plans].each do |plan| %> <li class='plan-list'>・<%= plan %></li> <% end %> <% end %> </ul> </div> <% end %> </div> ミスしたこと ビューは特に問題なかったですが、 コントローラーの記述がなかなかできずにいました。 ①曜日を自動的記述できない 日月火水・・・と記述することがなかなかできず、 繰り返し処理することだけが頭にあったため、 ひたすらwday_num = Date.today.wday + 1 と記述していました。 結果として、 毎日が翌日の曜日になるという表示になっていました。 ex.金金金金・・・・・ ②xという存在 まず、日付については繰り返し処理が完成していたため、 それに気づかなかったのは反省点でした。 ①で繰り返し処理ということがわかっていたのに、 それ同様に曜日も設定することの切り返しができていなかった。 ③そもそも作るアプリケーションの理解が浅かった まず、自分はカレンダーを想像していたため、 1ヶ月分まるまるの表示が必要になると勘違いをしてしまっていました。 つまり、30回、31回繰り返し処理が必要だと勘違いをしていたわけです。 今後は、どんなアプリケーションなのかをイメージし、概要を把握した上で、 取り組むことが必要だと感じました。 ④配列の理解の浅さ wdays = ['日','月','火','水','木','金','土'] wdays[0] # =>'日'を取得できる 初歩の初歩。 ここを理解していたかと言われたら本当はそうではなかったです。 プライドを捨て、できないこと、理解していなかったところは、 理解できるように前に進みます。 終わりに たくさんいろんな人にアドバイスをいただきながら完成しました。 これを完成させるのに20時間。。 できない自分に腹が立ち、それとともに自信が少しなくなりました。 ただ、 これがプログラミング、 これから就職したらこのようなことが起こるのだと感じました。 また、リアルに近いものを体験できたことは非常に価値のある時間だったとも感じました。 諦めず、自分をコントロールしながら乗り越えていけるよう引き続き頑張ります!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MVCモデルを理解する。

始めに....。 ・MVCモデルを理解したいので記事にしたいと思います。 MVCモデルとは ~Model、View、Controllerに分割をしてコーディングを行うモデルのことを指す。~ 役割 ・Model --->システムの中でビジネスロジックを担当。 ・View --->表示、入出力の処理を担当。 ・Contoroller --->ユーザーの入力に基づき、ModelとViewを制御。 *記事参考、図引用 (https://qiita.com/s_emoto/items/975cc38a3e0de462966a) *上の図 MVCモデルの概念図 (出典:http://www.slideshare.net/MugeSo/mvc-14469802) MVCモデルの挙動 Railsチュートリアルをやった際に出てきた図をおさらいしたいと思います。 「/users にあるindexページをブラウザで開く」という操作をしたとき、内部では何が起こっているかについてMVCで説明。 1.ブラウザから「/users」URLをリクエストし、サーバーに送信する。 2.「/users」リクエストはRailsのルーティング機構(ルーター)でUsersコントローラー内のindexアクションを割り当てる。 3.indexアクションが実行されるとUserモデルに問い合わせをする。 4.Userモデルは問い合わせに対して全てのユーザーをデータベースから取り出す。 5.データベースから取り出したユーザーの一覧をUserモデルからコントローラーに返す。 6.Usersコントローラーはユーザーの一覧を変数に保存しビューに渡す。 7.ビューが起動してERB(Embedded RuBy: ビューのHTMLに埋め込まれているRubyコード)を実行してHTMLを生成する。 8.コントローラーはビューで生成されたHTMLを受け取りブラウザに返す。 *記事、図 参考と引用 (https://railstutorial.jp/chapters/toy_app?version=6.0#sec-mvc_in_action) MVCのメリット ・機能が分割され独立しているので変更や修正がしやすく、何かあったとしても影響が出にくい。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails referenses

⚫︎テーブルに外部キーを追加する  ex)articlersテーブルにmodel_name:referencesを指定しmodelに対する外部キー(今回はuser_id) のカラムがbignit型で追加されそれに加えてindexが貼られる。 *indexはレコード数が多くなっても高速に検索できるようにする仕組みです。 referencesを使い簡単に実装できる。 ⚫︎articlesのmodelとテーブル作成 $ bundle exec rails g model Article title:string body:text user:references *user:referencesの部分は本来user_id:bignitを作成するところをuser:referencesで作成 qiita.rb class CreateArticles < ActiveRecord::Migration[6.0] def change create_table :articles do |t| t.string :title t.text :body t.references :user, null: false, foreign_key: true t.timestamps end end end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails6+jQuery】ページの最上部に戻るボタンの実装

1.背景 サイトを閲覧する際に目にする「PAGE TOP」などの戻るボタン。 今回、Railsで制作したアプリに戻るボタンを実装したので備忘録としてまとめます。 2.環境 mac.os バージョン10.15.6 Ruby 2.7.2 Rails 6.1.3.1 psql (PostgreSQL) 12.6 3.手順 ①jQueryの導入 今回はyarnを用いてjQueryの導入を行いました。 ターミナル. yarn add jquery ※yarnとは:JavaCcriptのパッケージマネージャのこと。 ②environment.jsを編集 config/webpack/environment.jsを編集します。 const { environment } = require('@rails/webpacker') const webpack = require('webpack') environment.plugins.append('Provide', new webpack.ProvidePlugin({   // ここから編集 $: 'jquery/src/jquery', jQuery: 'jquery/src/jquery' })) module.exports = environment ③application.jsを編集 app/javascript/packs/application.jsを編集します。 // コード省略 // 下記を追記 require ('jquery') ④jQueryを記述したいファイルの作成 今回は、application.jsと同じ階層にcustom.jsというファイル名で作成しました。 app/javascript/packs/custom.js そして、application.jsに記述します。 // コード省略 require ('jquery') // 下記を追記 require('custom.js') ⑤挙動の確認 app/javascript/packs/custom.jsにテストコードを書き、OKと表示されるか確認します。 $(function() { console.log("OK"); }); rails sでアプリを起動して検証の画面からconsoleタブを選択しました。 ⑥ボタンの実装(HTML/CSS) 今回はfontawesomeを使用し、下記のようなボタンを実装します。 (1) HTML app/views/layouts/application.html.erbのタグ内にfontawesomeを導入するためのコードを記述します。 また、タグ内にボタンを記述します。 // <head>タグ内 <link href="https://use.fontawesome.com/releases/v5.15.3(※バージョンを入れる)/css/all.css" rel="stylesheet"> // <body>タグ内 <div id="page_top"><a href="#"></a></div> (2) CSS(scss) app/assets/stylesheets/application.scssにコードを記述します。 // トップに戻るボタン #page_top{ width: 50px; height: 50px; position: fixed; right: 20px; bottom: 30px; background: #3f98ef; opacity: 0.6; border-radius: 50%; } #page_top a{ position: relative; display: block; width: 50px; height: 50px; text-decoration: none; text-align: center; } #page_top a::before{ font-family: "Font Awesome 5 Free"; content: '\f106'; font-weight: 900; font-size: 25px; color: #fff; position: absolute; width: 25px; height: 25px; top: 10px; bottom: 25px; right: 0; left: 0; margin: auto; } ⑦ボタンの実装(jQuery) (1) 非推奨の書き方をした→修正 app/javascript/packs/custom.jsにコードを記述します。 $(function(){ var pagetop = $('#page_top'); // ボタン非表示 pagetop.hide(); // 100px スクロールしたらボタン表示 $(window).scroll(function () { if ($(this).scrollTop() > 100) { pagetop.fadeIn(); } else { pagetop.fadeOut(); } }); pagetop.click(function () { $('body, html').animate({ scrollTop: 0 }, 500); return false; }); }); しかし、こちらのように書いたところ、scrollとclickに打ち消し線が入っていました。 調べたところ、非推奨だと打ち消し線が入るとのことだったのでon()を使った書き方に変更しました。 $(function(){ var pagetop = $('#page_top'); // ボタン非表示 pagetop.hide(); // scrollをon('scroll')に変更 $(window).on('scroll', function () { if ($(this).scrollTop() > 100) { pagetop.fadeIn(); } else { pagetop.fadeOut(); } });     // clickをon('click')に変更 pagetop.on('click', function () { $('body, html').animate({ scrollTop: 0 }, 500); return false; }); }); (2) 実装したはずが最初の1回しか実装できていない→修正 今回やりたかったのが、 最初はボタンを非表示 (100px)スクロールしたらボタンが表示される ボタンを押すと最上部に戻る ...でしたが、アプリを起動した最初の1回だけ思い通りの実装になり、 その後は他のページでもトップページ(アプリを起動した際に最初に表示されるページ)でも ボタンが表示されたままで、「jQueryが効いていないな・・・?」と思うことがありました。 原因を調べると、Turbolinksの影響があることがわかりました。 ※Turbolinksとは:Rails4から標準装備されている、ページの遷移を高速化する仕組みのこと。 全てのリンククリックに対するページ遷移を自動的にAjax化(Webブラウザ上で非同期通信を行い、ページの再読み込みなしにページを更新)することで高速化を図る。 Turbolinksはa要素のクリックイベントをフックして、遷移先のページをAjaxで取得します。そして取得したページが要求するJavaScriptやCSSが現在のものと同一であれば現在のものをそのまま使用し、titleやbody要素のみを置き換えます。(→ページ遷移は発生しない。JavaScriptやCSSをブラウザが評価しないので高速化ができる。) 今回の戻るボタンは<a href="#"></a>とaタグを使用しました。なのでボタンを押すとTurbolinksの仕組みが動きます。その影響で、 jQueryのコードの処理が実行されないという現象が起きていました。 (こちらのjQueryのreadyイベントが発火しないに該当します。) $(document).ready(function(){ // 処理内容 }); // (document).readyは省略も可能 $(function(){ // 処理内容 }); 最初だけjQueryが動作しているように見えたのは、戻るボタン(=aタグ)を押す前で Turbolinksが動いていなかったためだと考えられます。 今回はturbolinks:loadというオプションを使用することで正常な読み込みができるようになりました。 // turbolinks:loadを追記+(document).onという書き方に変更 $(document).on('turbolinks:load', function () { var pagetop = $('#page_top'); // ボタン非表示 pagetop.hide(); // 100px スクロールしたらボタン表示 $(window).on('scroll', function () { if ($(this).scrollTop() > 100) { pagetop.fadeIn(); } else { pagetop.fadeOut(); } }); pagetop.on('click', function () { $('body, html').animate({ scrollTop: 0 }, 500); return false; }); }); 4.まとめ 今回色々なサイトを参考に実装しましたが、使用言語のバージョンを踏まえた実装を行うことが大切だと思いました。 5.参考 1.【Rails6】Webpackerを用いてjQueryをインストールする手順を簡単にまとめてみた 2.Rails 6: Webpacker+Yarn+Sprocketsを十分理解してJavaScriptを書く: 前編(翻訳) 3.[jQuery] トップへ戻るボタンの実装サンプル 4.【jQuery入門】on()によるイベント処理の使い方まとめ! 5.jQueryでクリックイベントで処理を実行する:on(), click() 6.【Rails】初心者向け!画面遷移の高速化を行うTurbolinksについて図を用いて詳しく解説 7.大場寧子他, 現場で使えるRuby on Rails5速修実践ガイド, マイナビ出版, 2018. 6.最後に 記事の感想や意見、ご指摘等あれば伝えていただけるとありがたいです。 読んでいただき、ありがとうございました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】ファイルを分割し、ローカル引数を渡す

はじめに この記事を読めば、 ファイルを分割し、ローカル引数を渡す方法 について理解できましす。 ※この記事ではerbを使用しています。 やり方 分割前 index.html.erb <h2>レシピ一覧</h2> <% @posts.each do |post| %> <p><%= post.title %></p> <% end %> 分割後 index.html.erb <h2>レシピ一覧</h2> <%= render "posts", posts: @posts %> _posts.html.erb <% posts.each do |post| %> <p><%= post.title %></p> <% end %> 解説 <%= render "posts", posts: @posts %> render "posts" _posts.html.erbを呼び出しています。 posts: @posts postsに@postsを代入しています。 _posts.html.erb内で@postsをpostsとして使えます。 このページのみではわかりませんが、他の要素を代入したりする際にかなり有効です。 _posts.html.erbでは@postsがpostsに変わっているので気をつけましょう。 さいごに ファイルを分割し、ローカル引数を渡す方法 について解説しました。 参考になったら、LGTMしていただけると幸いです! 最後まで読んでいただきありがとうございました!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails5.2から6.1に更新する際に躓いたところ(zeitwerk)

Rails5.2から6.1に更新する bulk insertを使いたかった。 gem単体でも入れられるらしいけど、まあついでに。 6.xからはオートロード(requireしなくてもクラス参照できるアレ)の仕組みが刷新されている = zeitwerk さっとさらった概要 以前は下記参照ルールだったらしい。 使われている定数の名前 -> 定義しているファイル名の特定 新しいオートロードでは以下参照ルール。 定義しているファイル名の特定 -> 使われている定数の名前 今まで参照できていた、Concernsに入れてるクラスが参照できなくなった???(本題) controller/concerns/gas_api_callable.rb # GAS APIコールする共通モジュール module GasAPICallable extend ActiveSupport::Concern ### code #### end end controller/concerns/gas_controller.rb # GasAPICallableを使って色々するコントローラー class GasController < ApplicationController include GasAPICallable ### code ### end ------> uninitialized constants "GasAPICallable" エラー発生。 ファイル名のCapitalize(大文字化)から定数名を探すことで、「API」というアクロニム(略称)がひろえなくなっているみたい。(根本原因) gas_api_callable.rb -> GasApiCallable ----> GasAPICallableはあるけど、GasApiCallableはないよ???? -> const未定義エラー 対処1 アクロニムをやめる(大文字略称をやめる) controller/concerns/gas_api_callable.rb # GAS APIコールする共通モジュール - module GasAPICallable + module GasApiCallable extend ActiveSupport::Concern ### code #### end end controller/concerns/gas_controller.rb # GasAPICallableを使って色々するコントローラー class GasController < ApplicationController - include GasAPICallable + include GasApiCallable ### code ### end 参照できた。 対処2 アクロニムをzeitwerkに伝える。 config/initializeers/zeitwerk.rb ActiveSupport::Inflector.inflections(:en) do |inflect| inflect.acronym "API" end ※ただしこの場合すべてのAPIという単語をAPIとして定義しなければならない(Apiは探索できない) 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

開発環境のカラム情報が本番環境 AWS EC2のDBに反映されなかったときの対処法

はじめに 個人開発したアプリケーションをAWS EC2で本番環境にデプロイをしました。 その後、追加機能としてチャットルーム機能を実装。roomsテーブルを追加しました。 なので再びEC2にデプロイ。 EC2本番環境で新しい機能、rooms/newページでチャットルーム作成をして、rooms/createのページアクセスすると以下のエラーが発生。 room/createのページにアクセスできませんでした。(チャットルームが作成できない状況) ポイントとして ・開発環境(localhost:3000)とherokuでは問題なくrooms/newからrooms/createにアクセスできる。でもEC2ではできない。 ということ。 今回の原因として、roomsテーブル作成後、新しくnameカラムを追加しました。結果、そのカラム追加方法に原因があったようです。そのためEC2のDBにnameカラム反映されなかったみたいでした。内容を詳しく後述します。 まず、使用している開発環境及び追加したテーブル&カラムは以下の通りです。 開発環境 Ruby 2.6.5 Ruby on Rails 6.0.3.7 MySQL 5.6.51 Github 2.30.1 heroku 7.54.0 AWS EC2 自動デプロイツール : Capistrano Webサーバー : nginx APサーバー :Unicorn 追加したテーブル rooms Column Type Options name strings null: false has_many: users through: :room_users has_many: room_users room_users(中間テーブル) Column Type Options room references foreign_key: true user references foreign_key: true has_many: users has_many: rooms 新しくテーブル、そのあとにカラムを追加後、EC2上に再度デプロイ。本番環境でチャットルーム新規作成ページにてルームを作成しようとすると、 と表示されてしまった。 このエラーを解決するのに丸一日以上費やしたので、今後のためにも解決した方法をこの記事に載せることにしました。 やっかいだったのが、AWSの情報ってググってもまだ情報量が少ないので答えになかなか辿り着けませんでした。 そこで僕は、エラー解決でどうしようもないときたまにお世話になっている、MENTAを使うことにしました。 原因 EC2上で tail -f production.log でログを調べると ActiveModel::UnknownAttributeError in RoomsController#create unknown attribute 'name' for Room. ログの中にこいつが見つかりました。前述しましたがこれが今回の原因です。 nameカラムがEC2上のデータベースにないですよ、という意味。 試したこと まず、試したこととして、以下のコマンドで、Nginxの読み直しと再起動をしました。
 sudo systemctl reload nginx sudo systemctl restart nginx その後、以下のコマンドで、プロセスを確認。 ps aux | grep unicorn kill プロセス番号 でプロセスをkillしました。 再度デプロイしてみます。 bundle exec cap production deploy もう一度アクセスしてみましたが、変わりませんでした。 前述しましたが、ググってもAWSの情報はまだ少ないので、解決方法になかなか辿り着けませんでした。 メンターからのアドバイス そこで今回、MENTAでお願いをしたメンターより、以下のアドバイスをいただきました。 「もし、nameカラムを追加したマイグレーションファイルが一番直近のものでかつロールバック可能であればロールバックして再度マイグレーションを適用するのがいいと思います。 一番簡単なのは一度データベースを作り直すことです。ただし、データが消えるのでデータが消えてもいい場合のみです。」 「基本的にデータベースのロールバックというのはやらない方がいいです。 これをやっていいのは開発環境くらいで本番環境では常にマイグレーションファイルを追加する運用が最も安全かと思います。」 僕は、nameカラムはマイグレーションファイル作成後、必要だと気づいたので、 ①roomのマイグレーションファイルをrails db:rollbackでdownさせ ②downさせたマイグレーションファイルにnameカラムを追記 ③再びrails db:migrate →結果これが良くなかったことだったと判明 =次回からは、カラム追加時は新しくマイグレーションファイルを別で作成するようにします! 解決方法手順 本場環境のデータベースを作り直すのであれば下記の方法で作り直せます。 DISABLE_DATABASE_ENVIRONMENT_CHECK=1 ./bin/rake db:drop ./bin/rake db:create ./bin/rake db:migrate しかしコマンド実行後、以下のエラーメッセージが出力 エラー内容 FATAL: Listen error: unable to monitor directories for changes. Visit https://github.com/guard/listen/wiki/Increasing-the-amount-of-inotify-watchers for info on how to fix this. ディレクトリの変更を監視することができませんという意味らしい… エラーの原因 railsコンソールが起動できなかったのは、1つの実ユーザIDに対して生成できるinotifyのインスタンスの数の上限が決まっており、その上限に達してしまった為とのことです。 ここで、inotifyとはLinuxファイルやディレクトリのイベントを監視する機能です。 (参考URL: https://qiita.com/yn-misaki/items/c850a07f7858437e4d26) echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p 再度試すも、 ターミナルで出たエラー内容 Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2) Couldn't drop database 'original35700_development' rake aborted! Mysql2::Error::ConnectionError: Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2) -e:1:in `<main>' Tasks: TOP => db:drop:_unsafe というエラーが表示 MySQLに繋げられていないようでした。 メンターから以下のコマンドを実行するよう指示。 RAILS_ENV=production DISABLE_DATABASE_ENVIRONMENT_CHECK=1 ./bin/rake db:drop メンターから「既に削除できているっぽいので下記のコマンドを実行してください。」 とのこと RAILS_ENV=production ./bin/rake db:create RAILS_ENV=production ./bin/rake db:migrate この2つのコマンドを実行し、再度EC2上にデプロイしたところ新たにEC2上でDBが反映され解決! 開発環境でテーブル作成後、あとからカラムを追加するときはrollbackではなく、addでマイグレーションファイルを新たに作成し追加するようにしましょう。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[WIP] ◤絶対に失敗しない◢ RailsアプリにAuth0をサクッと導入する

※ 2021/07/24現在、この記事は執筆中です ログアウト時にエラー発生 Auth0とは Auth0は誰でも簡単に導入できる認証・認可プラットフォームです。 引用: Auth0 -公式- Railsアプリであれば従来deviseなどのgemを使い認証機能を実装したり、omniauth-twitterやomniauth-googleなどのgemでSNSログイン機能を実装していたと思います。 そんな機能をローコードで実現できるのがAuth0です。 (※ 間違いあればご指摘ください?) 作業環境 MacBook Air (M1, 2020) Ruby 3.0.2 Rails 6.1.4 前提条件 既にRailsアプリが存在しているものとします。 (認証・認可機能は未実装) ちなみにAuth0実装前のルーティングはたったこれだけ。 config/routes.rb Rails.application.routes.draw do root 'home#index' get 'dashboard', to: 'dashboard#show' end 作業手順 1. Auth0アカウントを作成する Auth0 公式HP にアクセスし、スクショの通りに進めます。 無事にアカウント作成できました✨ 2. Rails に Auth0 を導入する 2-1. Auth0 でアプリケーションの作成 次は、先ほどのダッシュボードから作業します。 これでAuth0のアプリケーション作成は完了です✨ 2-2. コールバックURL、ログアウトURLの設定 コールバックURLとは、Auth0がユーザーの認証後にリダイレクトする、あなたのアプリケーション内のURLです。 ログアウトURLとは、ユーザーが認可サーバーからログアウトした後に、Auth0が戻ることができるアプリケーション内のURLです。 引用: Auth0 -公式ドキュメント- ここではAllowed Callback URLsにhttp://localhost:3000/auth/auth0/callback、Allowed Logout URLsにhttp://localhost:3000と設定しておきましょう。 2-3. Gemのインストール お次はGemfileにomniauth-auth0とomniauth-rails_csrf_protectionを追加します。 Gemfile + gem 'omniauth-auth0', '~> 3.0' + gem 'omniauth-rails_csrf_protection', '~> 1.0' # prevents forged authentication requests terminal % bundle install 2-4. Auth0設定ファイルの作成 Auth0の設定ファイルを作成します。 まずはconfigディレクトリにauth0.ymlという設定ファイルを作成します。 terminal % touch config/auth0.yml そして作成したファイルには下記のように記述します。 config/auth0.yml development: auth0_domain: YOUR_DOMAIN auth0_client_id: YOUR_CLIENT_ID auth0_client_secret: <YOUR AUTH0 CLIENT SECRET> ※ ここに出てくるYOUR_DOMAIN, YOUR_CLIENT_ID, YOUR AUTH0 CLIENT SECRETは下記に記載してあります ※ ただし、このファイルはシークレットキーなどの秘匿情報が記載されているため、Git管理しないよう取扱い注意☝️ 次にconfig/initializersディレクトリにauth0.rbという設定ファイルを作成します。 ※ こちらはauth0.ymlじゃなくてauth0.rb、拡張子はrbなのでお間違い無く。 terminal % touch config/initializers/auth0.rb 作成したファイルには下記のように記述します。 config/initializers/auth0.rb AUTH0_CONFIG = Rails.application.config_for(:auth0) Rails.application.config.middleware.use OmniAuth::Builder do provider( :auth0, AUTH0_CONFIG['auth0_client_id'], AUTH0_CONFIG['auth0_client_secret'], AUTH0_CONFIG['auth0_domain'], callback_path: '/auth/auth0/callback', authorize_params: { scope: 'openid profile' } ) end 2-5. Auth0用コントローラーの作成 次にAuth0用のコントローラーを作成します。 下記コマンドを実行しましょう。 terminal % rails generate controller auth0 callback failure logout --skip-assets --skip-helper --skip-routes --skip-template-engine そしてコントローラーの中身は下記のように編集します。 app/controllers/auth0_controller.rb class Auth0Controller < ApplicationController def callback # OmniAuth stores the informatin returned from Auth0 and the IdP in request.env['omniauth.auth']. # In this code, you will pull the raw_info supplied from the id_token and assign it to the session. # Refer to https://github.com/auth0/omniauth-auth0#authentication-hash for complete information on 'omniauth.auth' contents. auth_info = request.env['omniauth.auth'] session[:userinfo] = auth_info['extra']['raw_info'] # Redirect to the URL you want after successful auth redirect_to '/dashboard' end def failure # Handles failed authentication -- Show a failure page (you can also handle with a redirect) @error_msg = request.params['message'] end def logout # you will finish this in a later step end end 次はルーティングの設定です。 下記の3つを追加します。 config/routes.rb Rails.application.routes.draw do ... + get '/auth/auth0/callback' => 'auth0#callback' + get '/auth/failure' => 'auth0#failure' + get '/auth/logout' => 'auth0#logout' end 2-6. セキュアモジュールの作成 次はセキュアモジュールを作成します。ここでいうセキュアモジュールとは、deviseでいうauthenticate_user!メソッドに当たります。つまるところログイン必須機能です。 app/controllers/concernsディレクトリにsecured.rbを作成します。 terminal % touch app/controllers/concerns/secured.rb 中身は下記の通りにしてください。 app/controllers/concerns/secured.rb module Secured extend ActiveSupport::Concern included do before_action :logged_in_using_omniauth? end def logged_in_using_omniauth? redirect_to '/' unless session[:userinfo].present? end end これでAuth0の導入は完了です? お疲れ様でした! 3. Auth0 を配置 さて、ようやくAuth0を配置するフェーズに来ましたね? やるべきことはこれだけ。 ログイン・ログアウトボタンの設置 ログイン必須アクションにセキュアモジュールを導入 ログイン後のビュー修正 では順を追って進めていきましょう。 3-1. ログイン・ログアウトボタンの設置 今回は単なる説明なので、どちらもホーム画面に設置します。 app/views/home/index.html.erb <h1>Home#index</h1> <p>Find me in app/views/home/index.html.erb</p> <%= button_to 'Login', 'auth/auth0', method: :post %> <%= button_to 'Logout', 'auth/logout', method: :get %> 3-2. ログイン必須アクションにセキュアモジュールを導入 このアプリにはホーム画面とダッシュボードしか用意していません。 今回は仮に、ホーム画面はログインしていなくてもアクセスできるものとし、ダッシュボードはログインしていないとアクセスできないものとします。 なのでセキュアモジュールを導入するのはダッシュボード閲覧のコントローラー#アクションですね。 ダッシュボードではログインユーザーの情報を表示したいので、ログインユーザーデータを持ったインスタンス変数@userを定義しましょう。 app/controllers/dashboard_controller.rb class DashboardController < ApplicationController include Secured # セキュアモジュールを導入 def show @user = session[:userinfo] end end 3-3. ログイン後のビュー修正 ここではシンプルにログインユーザーの名前を表示することにします。 app/views/dashboard/show.html.erb <h1>Dashboard#show</h1> <p>Find me in app/views/dashboard/show.html.erb</p> <div> <p>User Profile: <%= @user['name'] %></p> </div> これで準備完了?‍♂️ 4. Auth0 を実感 いよいよAuth0を実感する瞬間がやってまいりました! http://localhost.3000にアクセスし、ログインボタンを押してください。 見事なまでのログイン画面が表示され、ログイン後はユーザー名が表示されるはずです✨ できた? 5. おわりに いかがでしたか? かなり簡単にAuth0を導入できたのではないでしょうか? このままでは肝心のRailsアプリケーションにユーザー情報を持っていなかったり、Google以外のSNSアカウント認証ができなかったりと修正したい部分は多々ありますが、最低限の流れを掴んでいただけたら幸いです。 自分自身、Auth0を導入するのが今回初めてだったので、これからも気付きがあれば随時追記していこうと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

◤絶対に失敗しない◢ RailsアプリにAuth0をサクッと導入する

Auth0とは Auth0は誰でも簡単に導入できる認証・認可プラットフォームです。 引用: Auth0 -公式- Railsアプリであれば従来deviseなどのgemを使い認証機能を実装したり、omniauth-twitterやomniauth-googleなどのgemでソーシャルログイン機能を実装していたと思います。 そんな機能をローコードで実現できるのがAuth0です。 (※ 間違いあればご指摘ください?) 作業環境 MacBook Air (M1, 2020) Ruby 3.0.2 Rails 6.1.4 前提条件 既にRailsアプリが存在しているものとします。 (認証・認可機能は未実装) ちなみにAuth0実装前のルーティングはたったこれだけ。 config/routes.rb Rails.application.routes.draw do root 'home#index' get 'dashboard', to: 'dashboard#show' end 作業手順 1. Auth0アカウントを作成する Auth0 公式HP にアクセスし、スクショの通りに進めます。 無事にアカウント作成できました✨ 2. Rails に Auth0 を導入する 2-1. Auth0 でアプリケーションの作成 次は、先ほどのダッシュボードから作業します。 これでAuth0のアプリケーション作成は完了です✨ 2-2. コールバックURL、ログアウトURLの設定 コールバックURLとは、Auth0がユーザーの認証後にリダイレクトする、あなたのアプリケーション内のURLです。 ログアウトURLとは、ユーザーが認可サーバーからログアウトした後に、Auth0が戻ることができるアプリケーション内のURLです。 引用: Auth0 -公式ドキュメント- ここではAllowed Callback URLsにhttp://localhost:3000/auth/auth0/callback、Allowed Logout URLsにhttp://localhost:3000と設定しておきましょう。 2-3. Gemのインストール お次はGemfileにomniauth-auth0とomniauth-rails_csrf_protectionを追加します。 Gemfile + gem 'omniauth-auth0', '~> 3.0' + gem 'omniauth-rails_csrf_protection', '~> 1.0' # prevents forged authentication requests terminal % bundle install 2-4. Auth0設定ファイルの作成 Auth0の設定ファイルを作成します。 まずはconfigディレクトリにauth0.ymlという設定ファイルを作成します。 terminal % touch config/auth0.yml そして作成したファイルには下記のように記述します。 config/auth0.yml development: auth0_domain: YOUR_DOMAIN auth0_client_id: YOUR_CLIENT_ID auth0_client_secret: <YOUR AUTH0 CLIENT SECRET> ※ ここに出てくるYOUR_DOMAIN, YOUR_CLIENT_ID, YOUR AUTH0 CLIENT SECRETは下記に記載してあります ※ ただし、このファイルはシークレットキーなどの秘匿情報が記載されているため、Git管理しないよう取扱い注意☝️ 次にconfig/initializersディレクトリにauth0.rbという設定ファイルを作成します。 ※ こちらはauth0.ymlじゃなくてauth0.rb、拡張子はrbなのでお間違い無く。 terminal % touch config/initializers/auth0.rb 作成したファイルには下記のように記述します。 config/initializers/auth0.rb AUTH0_CONFIG = Rails.application.config_for(:auth0) Rails.application.config.middleware.use OmniAuth::Builder do provider( :auth0, AUTH0_CONFIG['auth0_client_id'], AUTH0_CONFIG['auth0_client_secret'], AUTH0_CONFIG['auth0_domain'], callback_path: '/auth/auth0/callback', authorize_params: { scope: 'openid profile' } ) end 2-5. Auth0用コントローラーの作成 次にAuth0用のコントローラーを作成します。 下記コマンドを実行しましょう。 terminal % rails generate controller auth0 callback failure logout --skip-assets --skip-helper --skip-routes --skip-template-engine そしてコントローラーの中身は下記のように編集します。 app/controllers/auth0_controller.rb class Auth0Controller < ApplicationController def callback auth_info = request.env['omniauth.auth'] session[:userinfo] = auth_info['extra']['raw_info'] redirect_to '/dashboard' end def failure @error_msg = request.params['message'] end def logout reset_session redirect_to logout_url end private AUTH0_CONFIG = Rails.application.config_for(:auth0) def logout_url request_params = { returnTo: root_url, client_id: AUTH0_CONFIG['auth0_client_id'] } URI::HTTPS.build(host: AUTH0_CONFIG['auth0_domain'], path: '/v2/logout', query: to_query(request_params)).to_s end def to_query(hash) hash.map { |k, v| "#{k}=#{CGI.escape(v)}" unless v.nil? }.reject(&:nil?).join('&') end end 次はルーティングの設定です。 下記の3つを追加します。 config/routes.rb Rails.application.routes.draw do ... + get '/auth/auth0/callback' => 'auth0#callback' + get '/auth/failure' => 'auth0#failure' + get '/auth/logout' => 'auth0#logout' end 2-6. セキュアモジュールの作成 次はセキュアモジュールを作成します。ここでいうセキュアモジュールとは、deviseでいうauthenticate_user!メソッドに当たります。つまるところログイン必須機能です。 app/controllers/concernsディレクトリにsecured.rbを作成します。 terminal % touch app/controllers/concerns/secured.rb 中身は下記の通りにしてください。 app/controllers/concerns/secured.rb module Secured extend ActiveSupport::Concern included do before_action :logged_in_using_omniauth? end def logged_in_using_omniauth? redirect_to '/' unless session[:userinfo].present? end end これでAuth0の導入は完了です? お疲れ様でした! 3. Auth0 を配置 さて、ようやくAuth0を配置するフェーズに来ましたね? やるべきことはこれだけ。 ログイン・ログアウトボタンの設置 ログイン必須アクションにセキュアモジュールを導入 ログイン後のビュー修正 では順を追って進めていきましょう。 3-1. ログイン・ログアウトボタンの設置 今回は単なる説明なので、どちらもホーム画面に設置します。 app/views/home/index.html.erb <h1>Home#index</h1> <p>Find me in app/views/home/index.html.erb</p> <%= button_to 'Login', 'auth/auth0', method: :post %> <%= button_to 'Logout', 'auth/logout', method: :get %> 3-2. ログイン必須アクションにセキュアモジュールを導入 このアプリにはホーム画面とダッシュボードしか用意していません。 今回は仮に、ホーム画面はログインしていなくてもアクセスできるものとし、ダッシュボードはログインしていないとアクセスできないものとします。 なのでセキュアモジュールを導入するのはダッシュボード閲覧のコントローラー#アクションですね。 ダッシュボードではログインユーザーの情報を表示したいので、ログインユーザーデータを持ったインスタンス変数@userを定義しましょう。 app/controllers/dashboard_controller.rb class DashboardController < ApplicationController include Secured # セキュアモジュールを導入 def show @user = session[:userinfo] end end 3-3. ログイン後のビュー修正 ここではシンプルにログインユーザーの名前を表示することにします。 app/views/dashboard/show.html.erb <h1>Dashboard#show</h1> <p>Find me in app/views/dashboard/show.html.erb</p> <div> <p>User Profile: <%= @user['name'] %></p> </div> これで準備完了?‍♂️ 4. Auth0 を実感 いよいよAuth0を実感する瞬間がやってまいりました! http://localhost.3000にアクセスし、ログインボタンを押してください。 見事なまでのログイン画面が表示され、ログイン後はユーザー名が表示されるはずです✨ できた? 5. おわりに いかがでしたか? かなり簡単にAuth0を導入できたのではないでしょうか? このままでは肝心のRailsアプリケーションにユーザー情報を持っていなかったり、Google以外のソーシャルログインができなかったりと修正したい部分は多々ありますが、最低限の流れを掴んでいただけたら幸いです。 自分自身、Auth0を導入するのが今回初めてだったので、これからも気付きがあれば随時追記していこうと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

最低限覚えておきたいRSpecの基本構成

今までRSpecに関する記事を複数書いてきましたが、RSpecの基本構成に関する記事を上げてなかったので、今回はかんたんにまとめていきます。 かなり初歩的な内容になりますので、RSpecの基本的な部分が分かっている方は、他の記事をご覧ください。 [Rails]RSpecでテストを行う準備(FactoryBot使用) [Rails]RSpecでモデルのテスト(FactoryBot使用) [Rails]RSpecでよく使うマッチャー10選 [RSpec]request specでcontrollerテストをやってみる RSpecの構成 それではさっそく、RSpecの構成について説明していきます。 RSpecでは、テストしたい項目を入れ子構造で、分類していきます。 入れ子構造にすることで、テストを実行した際にどこでエラーが出ているかがすぐにわかるので、便利ですし、テストコードを書いている際もどこのテストをしているのかわかりやすいです。 構成の部分で使用されるものは下記です。 describe (テストの1番おおきいくくり  :必須) context (describeをさらに細分化する場合にのみ使用) it (具体的なテストをここで実施します :必須) before (itの前に実行したい項目がある場合にのみ使用) describeとitが必須でそれ以外は状況に合わせて使用していきます。 四則演算の例 では、四則演算を例にコードを記述していきます。 describe まず、describeは1番大きいくくりなので、四則演算を入れていきます。 describe '四則演算' do # この中にいろいろと入れていく end context 四則演算という大きなくくりができたので、足し算、引き算といくくりでさらに細分化したいので、contextを使っていきます。 describe '四則演算' do # 足し算に関するテスト context '足し算' do # テストはこの中で実施 end # 引き算に関するテスト context '引き算' do # テストはこの中で実施 end end it では、足し算と引き算という項目まで細分化できたので、足し算と引き算に関する具体的なテストをitで記述していきます。 describe '四則演算' do context '足し算' do it '1 + 1は2になるか' do # エクスペクテーションとマッチャーを使ってテストコード記述 expect(1 + 1).to eq 2 end it '100 + 1は101になるか' do # エクスペクテーションとマッチャーを使ってテストコード記述 expect(100 + 1).to eq 101 end end context '引き算' do it '7 - 5は2になるか' do # エクスペクテーションとマッチャーを使ってテストコード記述 expect(7 - 5).to eq 2 end end end このような入れ子構造がRSpecの基本構造です。 足し算というくくりの中に何個でもitを入れていくことが可能です。 エクスペクテーションとマッチャーってなんやねんって方はこちらどうぞ! [Rails]RSpecでよく使うマッチャー10選 before beforeの説明がまだだったので、beforeに関しても触れておきます。 itの前に実行したいことがあれば、記述していきます。 テストしたいページに移動やサインインがよく使われます。 そのページに遷移することで、そのページでのテストが実行できます。 describe 'トップ画面のテスト' do # itが実行される前にroot_pathに移動しています。 before do visit root_path end context "表示の確認" do it "top画面に投稿一覧へのリンクが表示されているか" do expect(page).to have_link "", href: posts_path end it "root_pathが / であるか" do expect(current_path).to eq('/') end end end まとめ RSpecの構成 describe (テストの1番おおきいくくり  :必須) context (describeをさらに細分化する場合にのみ使用) it (具体的なテストをここで実施します :必須) before (itの前に実行したい項目がある場合にのみ使用) 今回は超初歩的なことをまとめてみました。 RSpecのコードをよくわからず、コピペしていた方などの参考になれば幸いです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

kaminariとoffset_value

gem kaminariの連番表示に悩まされた備忘録です。 状況背景 チーム開発の課題でページネーションを実装 表示していたアイテムに連番を表示させる 12件/ページ が実装条件 アイテムの表示にはコレクションレンダリングを使用 次のページに行くと連番が1に戻ってしまう kaminari導入前は連番は_counter変数で表示していた offset_valueを使い連番表示するのが実装条件 結論 ページネーションで連番を表示するには下記で解決できた _counter変数 + xx.offset_value + 1 解決までの道のり 今回のハマりポイントは 1. offset_valueについて調べても情報が少ない 2. offset_valueずっと0なんだけど 3. 2ページ目にすると今度は12で固定なんだけど 4. そもそもoffsetって何? 5. あ、わかったかも? という感じで順を追っていきます。 offset_valueの情報が少ない 大体どんな機能があるのかと思ってGoogle先生rails offset_valueで質問してみたけど offset_value自体がkaminariの独自?のものだと知らなかったので、見つけるのはkaminariの基本的な使い方に関する物が多くoffset_valueについて詳しい記事は見当たらなかった。(今思うと納得) offset_value ずっと 0 問題 とりあえず情報が無いならとりあえず動かして見れば良い という事でビューにxx.offset_valueを入れてみる 表示されるのは「0」 イメージでは「1~12」とか羅列されるのかと思ってたので思考が5秒止まった。 「え、何これ、どうしろと?」 2ページ目は12で固定問題 「まぁ0はわかった。じゃあ2ページ目はどうなるの?」 今度は12のまま変化なし。 ビューにbinding.pry入れても数値確認しても変わらないし 「あーもう無理だ分からん。詰んだ。」と同時に1つ疑問を残していた事に気付く。 「そもそもoffsetってなに?」問題 普通に「あーoffsetねー」みたいにしてたけど、「ってかoffsetってなによ?」だったのでこちらで調べる。(「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典) こちらによると「offset」とは 基準点からの距離で位置を表現したものが「オフセット」です。 ポイントは ・位置を表現している ・基準点がどこかにある ・距離(基準点からどれくらいズレているか)で表現している の3つでしょうかね。 「オフセット」って単語が出てきたら「基準点からの距離で表した位置なんだな~」と、お考えください。 なるほど、なんとなくわかった気がする。 あ、わかったかも? そもそもの間違いとしてoffset_valueだけで解決できると思っていたのが間違いだった。 binding.pryでみていた時にoffset_valueが一定の値で固定されるが_counter変数はレンダリングされた回数を表示するので増えて行った この二つの要素を使えばいけるのでは?と思い上記の結論部分に辿り着き、無事実装完了しPR作成に至るのでした。 参考資料 「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsで架空のCafeのHPを作ってみよう!【11日目】『deviseのログイン画面でエラーメッセージを表示』編

概要 基本Railsの記法に則り書いていきます! 1から全ての説明ではなく その中であれ?どうやるの?と 疑問に思った点や実装に困った箇所を ピックアップして紹介していきます♩ 設定と準備 ・Rails ・HTML ・CSS ・Javascript(jQuery) ↑上記の言語とフレームワークを使い 架空(自分で考えたテキトーなもの)のCafeの HPを作っていこうと思います! 11日目の作業内容 ・ビューの作成(deviseのデフォルト画面をレイアウト) 11日目の気になった箇所 deviseのsign up画面ではエラーメッセージが表示されるのに sign in画面ではエラーメッセージが表示されない。 仮説 deviseにはデフォルトでエラーメッセージを表示させるための shared/_error_messages.html.erbというファイルが存在するため、 renderでそのファイルを呼び出せば表示できるのではないか。 sign up画面の場合 registrations/new.html.erb <%= render "/shared/error_messages" %> と記述することでエラーメッセージの表示に成功できたので 同じようにようにsign in画面でも sessions/new.html.erb <%= render "/shared/error_messages" %> とすれば表示されるのではないかと思ったが表示されなかった。 結論 sign in画面では違う記述の仕方でなければ エラーメッセージの表示ができないことがわかった。 sessions/new.html.erb <%= flash[:notice] %> <%= flash[:alert] %> 上記の記述をエラーメッセージを表示させたい箇所に追加することで 可能となる。 flashとはRailsでようされているオブジェクトのことらしい。 一時的にメッセージを表示してくれる機能のようなものらしいです。 これでエラーメッセージの表示には成功! 補足になりますがdevise.ja.ymlを導入し 日本語でエラーメッセージを表示させる方法をとられている方は devise.ja.yml ja: devise: failure: not_found_in_database: "%{authentication_keys}またはパスワードが違います。" こちらの箇所を変更するとsign in画面でのエラーメッセージ内容を変更できるので お試しください!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Linux MintでRails開発(ローカル)の環境を作る

なんでMac使わないの? 以前までMacBookで開発していたけどMintに変えました。 環境構築の解説をする前に、Macと比較して個人的に感じたメリットとデメリットを書いておきます。 Mintで開発環境作るメリット ・好きなデザインのPC選べる -> Macのデザインも好きですが、みんなと同じなのがなんか嫌 ・Macのシステムや仕様を覚えなくても良い -> OSアップデートするとライブラリ更新されてビルドできなくなったりっていうのはよくあった ・本番環境に近い環境で開発できる -> CentOSだとけっこう勝手が違うけどUbuntsならほぼ同じ Mintで開発環境作るデメリット ・デバイスドライバが少ない(自己責任) -> 最近はスピーカーとかそのまま刺してもわりと動くけど、動かないものもある。仕方ない(自分で書く) ・Macで使えるソフトが使えない -> Photoshopとか使いたかったらWine経由で頑張るしかなさそう(知らない) ・linuxに関する専門知識が必要 -> APサーバのプログラムだけやりたいって人はこれがネックになる。サービスについて開発からリリースまで全部できるようになりたいって人は避けて通れないので勉強したほうがいい OSをインストールする ・ディスクイメージをダウンロードする 公式から自分のマシンに合ったisoファイルをダウンロードしてください。 https://linuxmint.com/download.php わたしが今使っているのは[Linux Mint 20 "Ulyssa" Cinnamon(64bit)]です。 ミラーから直接ダウンロードするよりTorrentの方が圧倒的に早くダウンロードできます。 ・ディスクイメージをDVD-Rに焼く Windows10かMacなら空のDVD-Rを挿入すれば特に困ることなく書き込めるはずです。 PC持っていない方でMintからスタートする方はネカフェとかにあるPCを使うか、友達や先輩にお願いしてください。 ・PCにインストールする インストール先の光学ドライブにディスクを入れればインストーラが起動するはずです。 BIOSの設定によっては光学ドライブからブートできない場合もあるので、お使いのPCメーカーかMBの製造元のサイトを参照してください。 細かいインストールの設定は「linux mint インストール」で検索すればたくさん出てきます。 日本語入力を使えるようにする MintにはデフォルトでWindowsで言うところのIMEがインストールされていないので、日本語入力には別途ソフトウェアをインストールします。 わたしは下記のサイト様を参考に設定しました。 LinuxMint 19: 日本語入力の設定をする rbenvとruby-buildのインストール 複数のrubyバージョンをインストールして、プロジェクトごとに切り替えられるソフトです。 わたしは下記の記事を参考にして進めましたが、パッケージ管理はyumがaptになるので注意です。 VSCodeのインストール VimやEmacsだと大変なので、ハイライトや入力補助が最初から入っているVSCodeのlinux版をインストールします。 公式サイトからVSCodeの.debパッケージをダウンロードしてインストール。 https://code.visualstudio.com/download VSCodeにerb拡張機能のインストール htmlやrubyのファイルはそのままでもシンタックスハイライトされますが、rails標準のhtmlテンプレートの.erbファイルはハイライトされません。 拡張機能を作っている方がいるのでインストールします。 "Ctrl + X"で拡張機能画面を開いて、"erb"で検索します。 これでerbファイルがきれいにハイライトされるようになります。 画面キャプチャ+編集ソフトをインストール macだとわりとよく"Skitch"というソフトが使われているようです。 https://evernote.com/intl/jp/products/skitch ubuntuだと"Shutter"が人気のようだったので使ってみました。 下記のサイトに導入方法が書いてあります。 https://www.linuxuprising.com/2018/10/shutter-removed-from-ubuntu-1810-and.html aptにリポジトリを追加してapt-getコマンドでインストールできます。 $ sudo add-apt-repository ppa:linuxuprising/shutter $ sudo apt-get update $ sudo apt install shutter $ sudo apt install gnome-web-photo
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

EC2にRailsアプリをデプロイ② ~EC2に必要なツールをインストール~

はじめに 前回続きの記事です。 今回は前回作成したEC2インスタンスへターミナルからログインしてデプロイに必要なツールを導入していきます。 EC2インスタンスへログイン ターミナルから以下のコマンドを実行して、EC2インスタンスへログインします。 ターミナル % cd ~ ターミナル % mkdir ~/.ssh # .sshというディレクトリを作成 # File existsとエラーが表示されたとしても、.sshディレクトリは存在しているのということなので、そのまま進みましょう。 ターミナル % mv Downloads/ダウンロードした鍵の名前.pem .ssh/ # mvコマンドで、ダウンロードしたpemファイルを、ダウンロードディレクトリから、.sshディレクトリに移動します。 # 「ダウンロードした鍵の名前」の部分は、Finderでダウンロードフォルダから「〜.pem」というファイルを探し、「〜」の部分の名前に置き換えてください。 ターミナル % cd .ssh/ ターミナル % ls # pemファイルが存在するか確認しましょう ターミナル % chmod 600 ダウンロードした鍵の名前.pem # 「ダウンロードした鍵の名前」はFinderのダウンロードフォルダから探しましょう(見つからない場合は他のフォルダを探しましょう) ターミナル % ssh -i ダウンロードした鍵の名前.pem ec2-user@作成したEC2インスタンスに紐付けたElastic IP 下記のコマンドを実行するとyesかnoかの意思表示を求められますが、yesを入力して実行しましょう。 ターミナル % ssh -i aws_key.pem ec2-user@52.68.~~~~~~ The authenticity of host '52.68.~~~~~~ (52.68.~~~~~~)' can't be established. RSA key fingerprint is eb:7a:bd:e6:aa:da:~~~~~~~~~~~~~~~~~~~~~~~~. Are you sure you want to continue connecting (yes/no)? (ここで「yes」を入力し、実行する) このssh接続は時間が経つとタイムアウトするので、その場合はもう一度上記のコマンドで繋ぎ直せます。 必要なツールをインストール yumコマンド まずはLinuxにおけるソフトウェア管理の仕組みのyumコマンドでパッケージをアップデートします。 ターミナル [ec2-user@ip-172-31-25-189 ~]$ sudo yum -y update そのほかの環境構築に必要なパッケージを諸々インストール。 (めちゃ長いので注意)↓ ターミナル [ec2-user@ip-172-31-25-189 ~]$ sudo yum -y install git make gcc-c++ patch libyaml-devel libffi-devel libicu-devel zlib-devel readline-devel libxml2-devel libxslt-devel ImageMagick ImageMagick-devel openssl-devel libcurl libcurl-devel curl これらのコマンドに「-y」とありますがこれは-yオプションと言って、yesかnoの質問に対して自動的に全ての問いにyesと答えるコマンドです。 コマンドをつけ忘れたら「Y」を入力してエンターキーを押してインストールの完了をしてください。 EC2にNode.jsをインストール Node.jsはサーバーサイドで動くJavaScriptのパッケージです。今後のデプロイの作業の中で、CSSや画像を圧縮するのに使われます。 下記をターミナルを実行 ターミナル [ec2-user@ip-172-31-25-189 ~]$ sudo curl -sL https://rpm.nodesource.com/setup_10.x | sudo bash - [ec2-user@ip-172-31-25-189 ~]$ sudo yum -y install nodejs これでNode.jsのインストールは完了です。 Yarnのインストール YarnはRailsに搭載されているJavaScriptのパッケージ管理するものです。 下記のコマンドでインストール ターミナル [ec2-user@ip-172-31-25-189 ~]$ sudo yum -y install wget [ec2-user@ip-172-31-25-189 ~]$ sudo wget https://dl.yarnpkg.com/rpm/yarn.repo -O /etc/yum.repos.d/yarn.repo [ec2-user@ip-172-31-25-189 ~]$ sudo yum -y install yarn これでYarnのインストールは完了です。 rbenvとruby-buildをインストール Rubyをインストールする前にこの二つをインストールしなければなりません。 下記のコマンドでインストール ターミナル # ①rbenvのインストール [ec2-user@ip-172-31-25-189 ~]$ git clone https://github.com/sstephenson/rbenv.git ~/.rbenv # ②パスを通す [ec2-user@ip-172-31-25-189 ~]$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile # ③rbenvを呼び出すための記述 [ec2-user@ip-172-31-25-189 ~]$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile # ④.bash_profileの読み込み [ec2-user@ip-172-31-25-189 ~]$ source .bash_profile # ⑤ruby-buildのインストール [ec2-user@ip-172-31-25-189 ~]$ git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build # ⑥rehashを行う [ec2-user@ip-172-31-25-189 ~]$ rbenv rehash ①でgitからrbenvをクローンしています。 ②、③のパスを通すとはどのディレクトリからでもアプリケーションを呼び出せる状態にすることです。 ④で設定したパスを読み込みます。 ⑤でgitから「ruby-build」をクローンしています。 ⑥使用しているRubyのバージョンで、gemのコマンドを使えるようにします。 Rubyをインストール Rubyをインストールしますが下記のコマンドのインストールするRubyのバージョンや自分のアプリで使っているバージョンは適宜変更します。 ターミナル # Ruby 2.6.5のバージョンをインストール [ec2-user@ip-172-31-25-189 ~]$ rbenv install 2.6.5 # EC2インスタンス内で使用するRubyのバージョンを決める [ec2-user@ip-172-31-25-189 ~]$ rbenv global 2.6.5 # rehashを行う [ec2-user@ip-172-31-25-189 ~]$ rbenv rehash # Rubyのバージョンを確認 [ec2-user@ip-172-31-25-189 ~]$ ruby -v Rubyのインストールは時間がかかりますが、固まっているように見えても変にいじらず待ちましょう。 今回はここまでです。次回はデータベースを用意していきます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails API + Reactで作る俺流アニメデータベース

概要 自分はアニメを見るのが趣味です。昨今のコロナ事情によってリモートワークが基本となった事もあり、以前よりもアニメに没頭する機会が増えました。 毎日のように「今期のアニメで面白そうな作品は無いかなぁ」なんて探しているわけですが、どうもアニメの情報って効率的に取得しづらい気がしています。 もちろん、世の中にはたくさんのアニメ情報サイトが存在しているものの、自分にとっては必要無い情報がたくさん羅列されていたりしてしっくり来ない事もしばしば。 たとえば、私が視聴するアニメを選ぶ基準としては、 どんなスタッフが携わっているか どんな声優さんが出演されているか キャラデザインは自分好みか 世間的な注目度は高そうか といったものが主な判断材料となっています。 要するに、製作陣やキャスト陣、キービジュアルやSNSのフォロワー数などが一目でわかれば情報としてはそれなりに十分というわけですね。 そこで今回は、↑の要件を満たすアプリを自分で作ってみる事にしました。 完成イメージ 年代・季節ごとに作品を絞り込み 作品のタイトルで個別に検索 作品のイメージ画像 製作陣やキャスト陣の情報一覧 公式サイトやTwitterアカウントへのリンク 必要最低限な機能・情報がコンパクトにまとまっていると思います。 主な使用技術・サービス バックエンド Ruby Rails API MySQL フロントエンド React TypeScript 外部サービス Annict 見たアニメを記録したり、見た感想を友達にシェアすることができるWebサービス。APIを公開しており、各作品の情報を取得する事ができる。 しょぼいカレンダー アニメの番組表などが確認できるWebサービス。こちらもAPIを公開しており、各作品の情報を取得する事ができる。 ※ 再現性を考慮してバックエンドのみDockerで環境構築を行います。 Annict、しょぼいカレンダーともにアニメ好きであれば一度は利用した事があるのではないでしょうか。簡単な情報からマニアックな情報まで網羅的に掲載してくれている素晴らしいWebサービスです。 それぞれAPIを公開しているため、素直にそれらを使えば良いんじゃねって思われるかもしれませんが、どちらも個人的には痒いところにあと一歩届かない感があったので、色々こねくり回して扱いやすい形に整形するためバックエンドを準備しました。 実装 前置きはほどほどに実装を開始しましょう。 バックエンド 先にバックエンド側から。 環境構築 何はともあれ環境構築を行います。 各種ディレクトリ・ファイルを作成 $ mkdir aninfo-backend && cd aninfo-backend $ touch Dockerfile docker-compose.yml entrypoint.sh Gemfile Gemfile.lock ./Dockerfile FROM ruby:2.6.3 RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs mariadb-client ENV APP_PATH /myapp RUN mkdir $APP_PATH WORKDIR $APP_PATH COPY Gemfile $APP_PATH/Gemfile COPY Gemfile.lock $APP_PATH/Gemfile.lock RUN bundle install COPY . $APP_PATH COPY entrypoint.sh /usr/bin/ RUN chmod +x /usr/bin/entrypoint.sh ENTRYPOINT ["entrypoint.sh"] EXPOSE 3000 CMD ["rails", "server", "-b", "0.0.0.0"] ./docker-compose.yml version: "3" services: db: image: mysql:5.7 environment: MYSQL_ROOT_PASSWORD: password volumes: - mysql-data:/var/lib/mysql - /tmp/dockerdir:/etc/mysql/conf.d/ ports: - 4306:3306 api: build: context: . dockerfile: Dockerfile command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'" volumes: - .:/myapp - ./vendor/bundle:/myapp/vendor/bundle environment: TZ: Asia/Tokyo RAILS_ENV: development ports: - "3001:3000" depends_on: - db volumes: mysql-data: ./entrypoint.sh #!/bin/bash set -e # Remove a potentially pre-existing server.pid for Rails. rm -f /myapp/tmp/pids/server.pid # Then exec the container's main process (what's set as CMD in the Dockerfile). exec "$@" ./Gemfile # frozen_string_literal: true source "https://rubygems.org" git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } gem "rails", "~> 6" ./Gemfile.lock # 空欄でOK rails new APIモードで作成します。 $ docker-compose run api rails new . --force --no-deps -d mysql --api database.ymlを編集 デフォルトの状態だとデータベースとの接続ができないので「database.yml」の一部を書き換えます。 ./config/database.yml default: &default adapter: mysql2 encoding: utf8mb4 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: password # デフォルトだと空欄になっているはずなので変更 host: db # デフォルトだとlocalhostになっているはずなので変更 development: <<: *default database: myapp_development test: <<: *default database: myapp_test production: <<: *default database: <%= ENV["DATABASE_NAME"] %> username: <%= ENV["DATABASE_USERNAME"] %> password: <%= ENV["DATABASE_PASSWORD"] %> コンテナを起動 & データベースを作成 $ docker-compose build $ docker-compose up -d $ docker-compose run api bundle exec rails db:create localhost:3001 にアクセス localhost:3001 にアクセスして初期状態の画面が表示されればOKです。 gemをインストール 後々の処理で必要になるgemをインストールしておきます。 ./Gemfile gem 'faraday' gem 'syobocal' gem 'dotenv-rails' fadaday HTTPクライアント用のgem syobocal しょぼいカレンダーから情報を取得しやすくしてくれるgem dotenv-rails 環境変数を管理するためのgem Gemfileを更新したので再度ビルド。 $ docker-compose build 各種モデルを作成 $ docker-compose run api rails g model Work title:string year:integer season:integer image:string twitter_username:string official_site_url:string media_text:string season_name_text:string syobocal_tid:integer $ docker-compose run api rails g model WorkDetail work_id:integer staffs:text casts:text syobocal_tid:integer $ docker-compose run api rails db:migrate Work(作品) ※ Annictから取得する情報 title 作品タイトル year 放送年 season 季節 image 作品イメージ twitter_username Twitterアカウント名 official_site_url 公式サイトURL media_text どのメディアで放送か(TV、映画、OVAなど) syobocal_tid しょぼいカレンダーのTID WorkDetail(作品の詳細)※ しょぼいカレンダーから取得する情報 work_id Workモデルとの関連付け用 staffs 製作陣 casts キャスト陣 syobocal_tid しょぼいカレンダーのTID ./app/models/work.rb class Work < ApplicationRecord enum season: { spring: 1, summer: 2, autumn: 3, winter: 4 } has_one :work_detail # Annictから情報を取得 def import_from_annict base_url = "https://api.annict.com/v1" access_token = ENV["ANNICT_ACCESS_TOKEN"] start_year = 1970 # どの年からデータを取得したいかを指定 end_year = Date.today.year seasons = ["spring", "summer", "autumn", "winter"] (start_year..end_year).each do |year| seasons.each.with_index(1) do |season, index| # 初回リクエストはデータの総数を調べるために実行 data = JSON.parse(Faraday.get("#{base_url}/works?fields=id&per_page=50&filter_season=#{year}-#{season}&sort_watchers_count=desc&access_token=#{access_token}").body) data_count = data["total_count"] # データの数 page_count = (data_count / 50.to_f).ceil # ページの数 current_page = 1 # 現在のページ <= ページの数になるまで繰り返し処理を実行 while current_page <= page_count do data = JSON.parse(Faraday.get("#{base_url}/works?fields=title,images,twitter_username,official_site_url,media_text,syobocal_tid,season_name_text&page=#{current_page}&per_page=50&filter_season=#{year}-#{season}&sort_watchers_count=desc&access_token=#{access_token}").body) works = data["works"] works.each do |work| # すでにレコードが存在する場合は更新、無ければ新規作成 Work.find_or_initialize_by(title: work["title"]).update( year: year, season: index, image: work["images"]["recommended_url"], twitter_username: work["twitter_username"], official_site_url: work["official_site_url"], media_text: work["media_text"], syobocal_tid: work["syobocal_tid"], season_name_text: work["season_name_text"] ) end current_page += 1 end end end end end ./app/models/work_detail.rb class WorkDetail < ApplicationRecord serialize :staffs, Array serialize :casts, Array belongs_to :work # しょぼいカレンダーから情報を取得 def import_from_syobocal titles = Syobocal::DB::TitleLookup.get({ "TID" => "*" }) titles.each do |title| comment = title[:comment] parser = Syobocal::Comment::Parser.new(comment) staffs = [] # 製作陣 casts = [] # キャスト陣 parser.staffs.each do |staff| staffs << { "role": staff.instance_variable_get("@role"), "name": staff.instance_variable_get("@people")[0].instance_variable_get("@name") } end parser.casts.each do |cast| casts << { "character": cast.instance_variable_get("@character"), "name": cast.instance_variable_get("@people")[0].instance_variable_get("@name") } end tid = title[:tid] work = Work.find_by(syobocal_tid: tid) # すでにレコードが存在する場合は更新、無ければ新規作成 WorkDetail.find_or_initialize_by(syobocal_tid: tid).update( work_id: work ? work.id : nil, staffs: staffs, casts: casts ) end end end 各情報をデータベースにインポート Annict、しょぼいカレンダーから各情報をデータベースにインポートします。ただし、Annictに関してはAPIを利用するためのアクセストークンが必要になるので、公式ドキュメントの手順に従い事前に取得しておいてください。 Annict API 公式ドキュメント アクセストークンが取得できたら、ルートディレクトリに「.env」ファイルを作成してそこに環境変数としてセットします。 $ touch .env .env ANNICT_ACCESS_TOKEN=*********************** その後、Railsコンソールを立ち上げてそれぞれインポートを開始してください。 $ docker-compose run api rails c irb(main):001:0> Work.new.import_from_annict Work Load (0.6ms) SELECT `works`.* FROM `works` WHERE `works`.`title` = 'あしたのジョー' LIMIT 1 TRANSACTION (0.4ms) BEGIN Work Create (0.6ms) INSERT INTO `works` (`title`, `year`, `season`, `image`, `twitter_username`, `official_site_url`, `media_text`, `season_name_text`, `created_at`, `updated_at`) VALUES ('あしたのジョー', 1970, 1, '', '', '', 'TV', '1970年春', '2021-07-23 16:04:36.882115', '2021-07-23 16:04:36.882115') TRANSACTION (2.2ms) COMMIT ... irb(main):002:0> WorkDetail.new.import_from_syobocal Work Load (4.5ms) SELECT `works`.* FROM `works` WHERE `works`.`syobocal_tid` = 1 LIMIT 1 WorkDetail Load (0.5ms) SELECT `work_details`.* FROM `work_details` WHERE `work_details`.`syobocal_tid` = 1 LIMIT 1 TRANSACTION (0.3ms) BEGIN Work Load (2.0ms) SELECT `works`.* FROM `works` WHERE `works`.`id` = 2194 LIMIT 1 WorkDetail Create (0.8ms) INSERT INTO `work_details` (`work_id`, `staffs`, `casts`, `syobocal_tid`, `created_at`, `updated_at`) VALUES (2194, '---\n- :role: 監督\n :name: 下田正美\n- :role: 原作・脚本\n :name: 山田典枝\n- :role: 掲載\n :name: 月刊コミックドラゴン\n- :role: キャラクター原案\n :name: よしづきくみち\n- :role: キャラクターデザイン\n :name: 千葉道徳\n- :role: 総作画監督\n :name: 川崎恵子\n- :role: コンセプト・ワークス\n :name: 横田耕三\n- :role: 美術監督\n :name: 西川淳一郎\n- :role: 色彩設定\n :name: 石田美由紀\n- :role: 撮影監督\n :name: 秋元央\n- :role: 編集\n :name: 西山茂\n- :role: 音響監督\n :name: 田中英行\n- :role: 音楽\n :name: 羽毛田丈史\n- :role: 音楽プロデューサー\n :name: 廣井紀彦\n- :role: 音楽ディレクター\n :name: 和田亨\n- :role: 音楽協力\n :name: テレビ朝日ミュージック\n- :role: 録音調整\n :name: 小原吉男\n- :role: 音響効果\n :name: 今野康之\n- :role: 選曲\n :name: 神保直史\n- :role: 録音助手\n :name: 国分政嗣\n- :role: 録音スタジオ\n :name: タバック\n- :role: 音響制作\n :name: オーディオ・タナカ\n- :role: キャスティング協力\n :name: 好永伸恵\n- :role: ポストプロダクション\n :name: 東京現像所\n- :role: 広報\n :name: 小出わかな\n- :role: 宣伝プロデュース\n :name: 小林 剛\n- :role: アシスタントプロデューサー\n :name: 佐々木美和\n- :role: プロデューサー\n :name: 清水俊\n- :role: アニメーションプロデューサー\n :name: 新崎力也\n- :role: 企画\n :name: 角川大映\n- :role: アニメーション制作\n :name: ヴューワークス\n- :role: 制作\n :name: 魔法局\n', '---\n- :character: 菊池ユメ\n :name: 宮﨑あおい\n- :character: 小山田雅美\n :name: 諏訪部順一\n- :character: ケラ(加藤剛)\n :name: 飯田浩志\n- :character: アンジェラ\n :name: 渡辺明乃\n- :character: 遠藤耕三\n :name: 中博史\n- :character: 古崎力哉\n :name: 清川元夢\n- :character: 森川瑠奈\n :name: 石毛佐和\n- :character: ギンプン\n :name: 辻谷耕史\n- :character: ミリンダ\n :name: 平松晶子\n', 1, '2021-07-23 16:12:43.543292', '2021-07-23 16:12:43.543292') TRANSACTION (2.2ms) COMMIT ... 最終的にこんな感じでそれぞれの情報が格納されていれば成功です。(※ 古い作品などは空欄になってしまう箇所多し) APIを作成 データベースに格納した情報をJSON形式で返すAPIを作成します。 コントローラー $ docker-compose run api rails g controller api/v1/works ./app/controllers/api/v1/works_controller.rb class Api::V1::WorksController < ApplicationController def index return if !params[:year] && !params[:season] && !params[:title] works = [] # paramsによって絞り込みの条件を変更 queried_works = params[:year] && params[:season] ? Work.where(year: params[:year], season: params[:season]) : Work.where("title like ?", "%#{params[:title]}%") queried_works.each do |work| works << { id: work.id, # ID title: work.title, # 作品のタイトル year: work.year, # 年 season: work.season, # 季節 image: work.image, # 画像 staffs: work.work_detail ? work.work_detail.staffs : [], # 製作陣 casts: work.work_detail ? work.work_detail.casts : [], # キャスト陣 twitter_username: work.twitter_username, # Twitterのユーザー名 official_site_url: work.official_site_url, # 公式サイトのURL media_text: work.media_text, # ex. TV、映画、OVA、Web season_name_text: work.season_name_text # ex. 2021年春 } end render json: { status: 200, works: works } end end クエリパラメータに「year」と「season」が含まれていた場合は放送タイミングで作品を絞り込み、「title」が含まれていた場合は合致するタイトルの作品を絞り込むようにしました。 なお、Annictから取得した情報(Work)には製作陣(staffs)やキャスト陣(staffs)が含まれていなかったため、しょぼいカレンダーから取得した情報(WorkDetail)をプラスして情報の網羅性を高めています。 ルーティング ./backend/config/routes.rb Rails.application.routes.draw do namespace :api do namespace :v1 do resources :works, only: %i[index] end end end 動作確認 $ curl -X GET http://localhost:3001/api/v1/works?year=2021&season=3 $ curl -X GET http://localhost:3001/api/v1/works?title=小林さんちのメイドラゴンS curlコマンドを叩くなり、直接URLを打ち込んでアクセスするなりしてJSONが返ってくればOK。 CORS設定 今回の構成ではバックエンドとフロントエンドを完全に分けているため、RailsとReactがそれぞれ別のドメインで立ち上がっています。(localhost:3001とlocalhost:3000) この場合、デフォルトの状態だとセキュリティの問題でReactからRailsのAPIを使用できない点に注意が必要です。 これを解決するためには「CORS(クロス・オリジン・リソース・シェアリング)」の設定を行わなければなりません。 参照記事: オリジン間リソース共有 (CORS) rack-corsをインストール RailsにはCORSの設定を簡単に行えるgemがあるのでそちらをインストールしましょう。 ./Gemfile gem 'rack-cors' APIモードで作成している場合、すでにGemfile内に記載されているのでコメントアウトを外すだけでOKです。 $ docker-compose build Gemfileを更新したので再度ビルド。 cors.rbを編集 「config/initializers/」に設定ファイルが存在するはずなので、外部からアクセス可能なように編集しておきます。 ./backend/config/initializers/cors.rb Rails.application.config.middleware.insert_before 0, Rack::Cors do allow do origins "*" resource "*", headers: :any, methods: [:get, :post, :put, :patch, :delete, :options, :head] end end 設定の変更を反映させるためにコンテナを再起動。 $ docker-compose down $ docker-compose up -d これでバックエンド側の準備は完了です。 フロントエンド 次にフロントエンド側の実装に入ります。 環境構築 何はともあれ環境構築を行います。 各種ディレクトリ・ファイルを作成 おなじみの「create-react-app」でアプリの雛形を作ります。 $ mkdir aninfo-frontend && cd aninfo-frontend $ yarn create react-app . --template typescript tsconfig.jsonを修正 「./tsconfig.json」内に「"baseUrl": "src"」という1行を追記してください。 ./tsconfig.json { "compilerOptions": { "target": "es5", "lib": [ "dom", "dom.iterable", "esnext" ], "allowJs": true, ...省略... "baseUrl": "src" ← 追記 }, "include": [ "src" ] } これにより、インポート先を「./tsconfig.json」が配置されているディレクトリから相対パスで指定できるようになるので非常に楽です。 baseUrlを指定しない場合 import Hoge from "../../components/Hoge" // 呼び出すファイルからの相対パス baseUrlを指定した場合 import Hoge from "components/Hoge" // baseUrlからの相対パス いちいち「../../」みたいな記述をしなくて済むというわけですね。 不要なファイルを整理 この先使う事の無いファイルは邪魔なので今のうちに消しておきましょう。 $ rm src/App.css src/App.test.tsx src/logo.svg src/reportWebVitals.ts src/setupTests.ts 「./src/index.tsx」と「./src/App.tsx」を次のように変更します。 ./src/index.tsx import React from "react" import ReactDOM from "react-dom" import "./index.css" import App from "./App" ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById("root") ) ./src/App.tsx import React from "react" const App: React.FC = () => { return ( <h1>Hello World!</h1> ) } export default App 一旦、動作確認してみましょう。 $ yarn start localhost:3000 にアクセスして「Hello World!」と返ってくればOK。 型定義 プロジェクト全体で使い回す事になるであろう型(今回であればWork)を「./src/interfaces/index.ts」の中に記述しておきます。 $ mkdir src/interfaces $ touch src/interfaces/index.ts ./src/interfaces/index.ts export interface Work { id: number title: string year: number season: number image?: string staffs?: Array<{ role: string name: string }> casts?: Array<{ character: string name: string }> twitterUsername?: string officialSiteUrl?: string mediaText: string seasonNameText: string } APIを呼び出すための関数を作成 Railsで作成したAPIを呼び出すための関数を作成します。 $ mkdir src/lib $ mkdir src/lib/api $ touch src/lib/api/client.ts $ touch src/lib/api/works.ts $ touch .env.local $ yarn add axios axios-case-converter $ yarn add -D @types/axios axios HTTPクライアント用ライブラリ @types/axios 型定義用ライブラリ axios-case-converter axios経由で受け取るレスポンスの値をスネークケース→キャメルケースに変換、または送信するリクエストの値をキャメルケース→スネークケースに変換してくれるライブラリ ./src/lib/api/client.ts import applyCaseMiddleware from "axios-case-converter" import axios from "axios" /* applyCaseMiddleware axiosで受け取ったレスポンスの値をスネークケース→キャメルケースに変換 または送信するリクエストの値をキャメルケース→スネークケースに変換 */ const railsApiBaseUrl = process.env.REACT_APP_RAILS_API_BASE_URL const client = applyCaseMiddleware(axios.create({ baseURL: `${railsApiBaseUrl}/api/v1` })) export default client 慣習的にRubyなどの言語がスネークケースが基本であるのに対し、JavaScriptはキャメルケースが基本なので、足並みを揃える(スネークケース→キャメルケースへの変換もしくはその逆)ために「applyCaseMiddleware」というライブラリを使わせてもらっています。 .env.local REACT_APP_RAILS_API_BASE_URL=http://localhost:3001 ※ Reactで環境変数を使用する場合、環境変数名の先頭にREACT_APP_を付ける必要があるので注意。 動作確認 ./src/App.tsx import React, { useEffect, useState } from "react" import { getWorks } from "lib/api/works" import { Work } from "interfaces/index" const App: React.FC = () => { const [works, setWorks] = useState<Work[]>() const handleGetWorks = async (year?: number, season?: number, title?: string): Promise<void> => { const res = await getWorks(year, season, title) if (res.status === 200) { setWorks(res.data.works) } } useEffect(() => { handleGetWorks(2021, 3) // 20201年夏季のアニメ情報を取得 }, []) return ( <React.Fragment> { works?.map((work: Work) => ( <p>{work.title}</p> )) } </React.Fragment> ) } export default App localhost:3000 にアクセスして作品タイトルがズラーっと表示されていればOK。ちゃんと通信ができています。 各種ライブラリをインストール 後に必要となるライブラリをインストールしておきます。 $ yarn add @material-ui/core @material-ui/icons @material-ui/lab react-select $ yarn add -D @types/react-select material-ui UIを整える用のライブラリ react-select セレクトボックスが簡単に作れるライブラリ @types/react-select 型定義用ライブラリ 各種ビューを作成 各種ビューを作成します。 $ mkdir src/components $ mkdir src/components/layouts $ mkdir src/components/utils $ mkdir src/components/work $ touch src/components/layouts/Header.tsx $ touch src/components/utils/theme.ts $ touch src/components/work/SelectBox.tsx $ touch src/components/work/WorkDetail.tsx $ touch src/components/work/Works.tsx ./src/components/layouts/Header.tsx import React, { useState } from "react" import AppBar from "@material-ui/core/AppBar" import Toolbar from "@material-ui/core/Toolbar" import IconButton from "@material-ui/core/IconButton" import Typography from "@material-ui/core/Typography" import InputBase from "@material-ui/core/InputBase" import { alpha, makeStyles } from "@material-ui/core/styles" import MenuIcon from "@material-ui/icons/Menu" import SearchIcon from "@material-ui/icons/Search" const useStyles = makeStyles((theme) => ({ root: { flexGrow: 1 }, menuButton: { marginRight: theme.spacing(2) }, title: { flexGrow: 1, display: "none", [theme.breakpoints.up("sm")]: { display: "block" } }, search: { position: "relative", borderRadius: theme.shape.borderRadius, backgroundColor: alpha(theme.palette.common.white, 0.15), "&:hover": { backgroundColor: alpha(theme.palette.common.white, 0.25), }, marginLeft: 0, width: "100%", [theme.breakpoints.up("sm")]: { marginLeft: theme.spacing(1), width: "auto", } }, searchIcon: { padding: theme.spacing(0, 2), height: "100%", position: "absolute", pointerEvents: "none", display: "flex", alignItems: "center", justifyContent: "center" }, inputRoot: { color: "inherit" }, inputInput: { padding: theme.spacing(1, 1, 1, 0), paddingLeft: `calc(1em + ${theme.spacing(4)}px)`, transition: theme.transitions.create("width"), width: "100%", [theme.breakpoints.up("sm")]: { width: "12ch", "&:focus": { width: "20ch", } } } })) interface HeaderProps { handleGetWorks: Function setLoading: Function } const Header: React.FC<HeaderProps> = ({ handleGetWorks, setLoading }) => { const classes = useStyles() const [title, setTitle] = useState<string>("") return ( <div className={classes.root}> <AppBar position="static"> <Toolbar> <IconButton edge="start" className={classes.menuButton} color="inherit" aria-label="open drawer" > <MenuIcon /> </IconButton> <Typography className={classes.title} variant="h6" noWrap> AnInfo </Typography> <div className={classes.search}> <div className={classes.searchIcon}> <SearchIcon /> </div> <InputBase placeholder="作品名で検索" classes={{ root: classes.inputRoot, input: classes.inputInput, }} inputProps={{ "aria-label": "search" }} onChange={(e: React.ChangeEvent<HTMLInputElement>) => { setTitle(e.target.value) console.log(title) }} onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => { if (e.key === "Enter") { setLoading(true) handleGetWorks(null, null, title) } }} /> </div> </Toolbar> </AppBar> </div> ) } export default Header ./src/components/utils/theme.ts import { createTheme } from "@material-ui/core/styles" import blue from "@material-ui/core/colors/blue" import green from '@material-ui/core/colors/green' const theme = createTheme({ palette: { primary: { main: blue[500] }, secondary: { main: green[500] } }, typography: { h1: { fontSize: "3rem", fontWeight: 500 }, h2: { fontSize: "2rem", fontWeight: 500 }, h3: { fontSize: "1.25rem", fontWeight: 500 }, h4: { fontSize: "1rem", fontWeight: 500 } } }) export default theme ./src/components/work/SelectBox.tsx import React, { useState } from "react" import Select, { OptionTypeBase } from "react-select" import { makeStyles } from "@material-ui/core/styles" import Grid from "@material-ui/core/Grid" import IconButton from "@material-ui/core/IconButton" import SearchIcon from "@material-ui/icons/Search" import FormControl from "@material-ui/core/FormControl" const useStyles = makeStyles(() => ({ gridContainer: { marginBottom: "2rem" }, iconButton: { padding: 10 }, formControl: { margin: "3px", minWidth: 130 } })) interface SelectBoxProps { years: Array<{ value: number label: string }> seasons: Array<{ value: number label: string }> handleGetWorks: Function setLoading: Function } const SelectBox: React.FC<SelectBoxProps> = ({ years, seasons, handleGetWorks, setLoading }) => { const classes = useStyles() const [year, setYear] = useState<number>() const [season, setSeason] = useState<number>() return ( <Grid className={classes.gridContainer} container justifyContent="center"> <FormControl className={classes.formControl}> <Select instanceId="year-select" placeholder="年" options={years} onChange={(e) => { setYear(e?.value) }} /> </FormControl> <FormControl className={classes.formControl}> <Select instanceId="season-select" placeholder="シーズン" options={seasons} onChange={(e) => { setSeason(e?.value) }} /> </FormControl> <IconButton type="submit" className={classes.iconButton} size="medium" color="default" disabled={!year || !season} onClick={() => { setLoading(true) handleGetWorks(year, season) }} > <SearchIcon /> </IconButton> </Grid> ) } export default SelectBox ./src/components/work/WorkDetail.tsx import { createStyles, Theme, withStyles, WithStyles } from "@material-ui/core/styles" import Button from "@material-ui/core/Button" import Dialog from "@material-ui/core/Dialog" import MuiDialogTitle from "@material-ui/core/DialogTitle" import MuiDialogContent from "@material-ui/core/DialogContent" import MuiDialogActions from "@material-ui/core/DialogActions" import Typography from "@material-ui/core/Typography" import IconButton from "@material-ui/core/IconButton" import CloseIcon from "@material-ui/icons/Close" import { Work } from "interfaces/index" import React from "react" const styles = (theme: Theme) => createStyles({ root: { margin: 0, padding: theme.spacing(2), }, closeButton: { position: "absolute", right: theme.spacing(1), top: theme.spacing(1), color: theme.palette.grey[500] } }) export interface DialogTitleProps extends WithStyles<typeof styles> { id: string children: React.ReactNode onClose: () => void } const DialogTitle = withStyles(styles)((props: DialogTitleProps) => { const { children, classes, onClose, ...other } = props return ( <MuiDialogTitle disableTypography className={classes.root} {...other}> <Typography variant="h6">{children}</Typography> { onClose ? ( <IconButton aria-label="close" className={classes.closeButton} onClick={onClose}> <CloseIcon /> </IconButton> ) : null } </MuiDialogTitle> ) }) const DialogContent = withStyles((theme) => ({ root: { padding: theme.spacing(2), } }))(MuiDialogContent) const DialogActions = withStyles((theme) => ({ root: { margin: 0, padding: theme.spacing(1), } }))(MuiDialogActions) interface WorkDetailsProps { work: Work open: boolean handleClose: () => void } const WorkDetail: React.FC<WorkDetailsProps> = ({ work, open, handleClose }) => { return ( work.staffs != undefined && work.casts != undefined ? ( <Dialog onClose={handleClose} open={open} fullWidth> <DialogTitle id="customized-dialog-title" onClose={handleClose}> {work.title} </DialogTitle> <DialogContent dividers> <Typography variant="h4" gutterBottom> Staffs </Typography> { work.staffs.length > 1 ? work.staffs.map((staff, index: number) => ( <Typography key={index} variant="body2" gutterBottom> {staff.role}: {staff.name} </Typography> )) : <Typography variant="body2" gutterBottom>情報を取得できませんでした。</Typography> } <Typography variant="h4" gutterBottom style={{ marginTop: "1rem" }}> Casts </Typography> { work.casts.length > 1 ? work.casts.map((cast, index: number) => ( <Typography key={index} variant="body2" gutterBottom> {cast.character}: {cast.name} </Typography> )) : <Typography variant="body2" gutterBottom>情報を取得できませんでした。</Typography> } </DialogContent> <DialogActions> <Button autoFocus onClick={handleClose} color="primary"> 閉じる </Button> </DialogActions> </Dialog> ) : null ) } export default WorkDetail ./src/components/work/Works.tsx import React, { useState } from "react" import { makeStyles } from "@material-ui/core/styles" import Grid from "@material-ui/core/Grid" import Card from "@material-ui/core/Card" import CardMedia from "@material-ui/core/CardMedia" import CardContent from "@material-ui/core/CardContent" import CardActions from "@material-ui/core/CardActions" import Chip from "@material-ui/core/Chip" import CircularProgress from "@material-ui/core/CircularProgress" import Typography from "@material-ui/core/Typography" import WorkDetail from "components/work/WorkDetail" import { Work } from "interfaces/index" const useStyles = makeStyles(() => ({ circularProgress: { position: "absolute", top: "50%", left: "50%" }, card: { height: "100%", width: "100%", marginBottom: "0.5rem", transition: "all 0.3s", "&:hover": { boxShadow: "1px 0px 20px -1px rgba(0,0,0,0.2), 0px 0px 20px 5px rgba(0,0,0,0.14), 0px 1px 10px 0px rgba(0,0,0,0.12)", transform: "translateY(-3px)", } }, cardMedia: { aspectRatio: "16/9", cursor: "pointer" }, cardActions: { marginTop: "0.5rem" } })) interface WorksProps { loading: boolean works: Work[] } const initialWorkState: Work = { id: 0, title: "", year: 0, season: 0, image: "string", staffs: [], casts: [], twitterUsername: "", officialSiteUrl: "", mediaText: "", seasonNameText: "", } const Works: React.FC<WorksProps> = ({ loading, works}) => { const classes = useStyles() const [open, setOpen] = useState(false) const [work, setWork] = useState<Work>(initialWorkState) const handleOpen = () => { setOpen(true) } const handleClose = () => { setOpen(false) } return ( <React.Fragment> <Grid container spacing={4}> <WorkDetail work={work} open={open} handleClose={handleClose} /> { loading ? <CircularProgress className={classes.circularProgress}/> : works != null && works.length >= 1 && works.map((work) => ( <Grid item key={work.id} xs={12} sm={6} md={4}> <Card className={classes.card}> <CardMedia component="img" className={classes.cardMedia} // 画像がなかった場合は「NO IMAGE」を表示(各自用意してpublicディレクトリ以下に配置) src={work.image ? work.image : "/no_image.png"} onError={(e: any) => { e.target.src = "/no_image.png" }} onClick={() => { handleOpen() setWork(work) }} /> <CardActions className={classes.cardActions}> { work.seasonNameText != null && ( <Chip label={work.seasonNameText} variant="outlined" /> ) } { work.mediaText != null && ( <Chip label={work.mediaText} variant="outlined" /> ) } { work.officialSiteUrl != null && ( <Chip label="公式サイト" component="a" rel="noopener noreferrer" href={work.officialSiteUrl} target="_blank" clickable color="secondary" variant="outlined" /> ) } { work.twitterUsername != null && ( <Chip label="Twitter" component="a" rel="noopener noreferrer" href={`https://twitter.com/${work.twitterUsername}`} target="_blank" clickable color="primary" variant="outlined" /> ) } </CardActions> <CardContent> <Typography variant="h3" gutterBottom> {work.title} </Typography> </CardContent> </Card> </Grid> )) } </Grid> </React.Fragment> ) } export default Works ./src/App.tsx import React, { useEffect, useState } from "react" import { makeStyles, ThemeProvider } from "@material-ui/core/styles" import Container from "@material-ui/core/Container" import Header from "components/layouts/Header" import Works from "components/work/Works" import SelectBox from "components/work/SelectBox" import theme from "components/utils/theme" import { getWorks } from "lib/api/works" import { Work } from "interfaces/index" const useStyles = makeStyles(() => ({ container: { marginTop: "2rem" } })) const years: Array<{ value: number label: string }> = [] // 現在の年を取得 const currentYear: number = new Date().getFullYear() for (var y = currentYear; y >= 1970; y--) { years.push({ value: y, label: `${y}` }) } const seasons: Array<{ value: number label: string }> = [ { value: 1, label: "春" }, { value: 2, label: "夏" }, { value: 3, label: "秋" }, { value: 4, label: "冬" } ] // 現在の季節を取得 const currentSeason: number = seasons[(Math.ceil((new Date().getMonth() +1 ) / 3)) - 2].value const App: React.FC = () => { const classes = useStyles() const [loading, setLoading] = useState<boolean>(true) const [works, setWorks] = useState<Work[]>([]) const handleGetWorks = async (year?: number, season?: number, title?: string): Promise<void> => { const res = await getWorks(year, season, title) if (res.status === 200) { setWorks(res.data.works) } setLoading(false) } // デフォルトでは現在の年・季節の作品を取得 useEffect(() => { handleGetWorks(currentYear, currentSeason) }, []) return ( <React.Fragment> <ThemeProvider theme={theme}> <Header handleGetWorks={handleGetWorks} setLoading={setLoading}/> <Container className={classes.container} maxWidth="lg"> <SelectBox years={years} seasons={seasons} handleGetWorks={handleGetWorks} setLoading={setLoading} /> <Works works={works} loading={loading}/> </Container> </ThemeProvider> </React.Fragment> ) } export default App 動作確認 最終的にこんな感じになっていれば完成です。 番外編(データベースの定期更新) ここから先は番外編なので興味の無い人は読み飛ばしてOKです。 もし今回作成したアプリを本格的に使い続けたい場合、定期的に情報を更新するバッチ処理などを実装する必要があるでしょう。(アニメ作品はこれからも続々と追加されていくため) そこで一応、データベースの定期更新について自分なりの手順を記しておきます。 sidekiqをインストール 今回はRailsアプリに定期実行を組み込む際に定番の sidekiq というgemを使っていきたいと思います。 ./Gemfile gem 'sidekiq' gem 'sidekiq-cron' Gemfileを更新したので再度ビルド。 $ docker-compose build Workerクラスを作成 定期実行用のWorkerクラスを作成します。 $ docker-compose run api rails g sidekiq:worker Test $ docker-compose run api rails g sidekiq:worker WorkImport $ docker-compose run api rails g sidekiq:worker WorkDetailImport ./app/workers/test_worker.rb class TestWorker include Sidekiq::Worker # 動作確認用 def perform puts "Hello World!" end end ./app/workers/work_import_worker.rb class WorkImportWorker include Sidekiq::Worker # Annictから情報を取得 def perform Work.new.import_from_annict end end ./app/workers/work_detail_import_worker.rb class WorkDetailImportWorker include Sidekiq::Worker # しょぼいカレンダーから情報を取得 def perform WorkDetail.new.import_from_syobocal end end 各種設定 $ touch config/initializers/sidekiq.rb config/sidekiq.yml config/schedule.yml ./config/initializers/sidekiq.rb # Redisの設定 Sidekiq.configure_server do |config| config.redis = { url: ENV.fetch("REDIS_URL", "redis://localhost:6379") } end Sidekiq.configure_client do |config| config.redis = { url: ENV.fetch("REDIS_URL", "redis://localhost:6379")} end # どのタイミングで定期実行を行うかを記述したファイルを読み込む schedule_file = "config/schedule.yml" if File.exist?(schedule_file) && Sidekiq.server? Sidekiq::Cron::Job.load_from_hash YAML.load_file(schedule_file) end ./config/sidekiq.yml :verbose: false :pidfile: ./tmp/pids/sidekiq.pid :concurrency: 25 :queues: - default ./config/schedule.yml test: cron: "*/5 * * * *" # 5分おきに実行 class: "TestWorker" queue: default work_import: cron: "0 0 * * 1" # 毎週月曜日の午前0時に実行 class: "WorkImportWorker" queue: default work_detail_import: cron: "0 0 * * 1" # 毎週月曜日の午前0時に実行 class: "WorkDetailImportWorker" queue: default ./config/application.rb # 以下3行を適当な場所に追記(sidekiqのダッシュボードを見るために必要) # https://edgeguides.rubyonrails.org/api_app.html#using-session-middlewares config.session_store :cookie_store, key: '_interslice_session' config.middleware.use ActionDispatch::Cookies config.middleware.use config.session_store, config.session_options ./config/routes.rb require "sidekiq/web" require "sidekiq/cron/web" Rails.application.routes.draw do mount Sidekiq::Web, at: "/sidekiq" # ダッシュボードへのルーティング namespace :api do namespace :v1 do resources :test, only: %i[index] resources :works, only: %i[index] end end end ./docker-compose.yml version: "3" services: db: image: mysql:5.7 environment: MYSQL_ROOT_PASSWORD: password volumes: - mysql-data:/var/lib/mysql - /tmp/dockerdir:/etc/mysql/conf.d/ ports: - 4306:3306 api: build: context: . dockerfile: Dockerfile command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'" volumes: - .:/myapp - ./vendor/bundle:/myapp/vendor/bundle environment: TZ: Asia/Tokyo RAILS_ENV: development REDIS_URL: redis://redis:6379 # 追記 ports: - "3001:3000" depends_on: - db redis: # 追記 image: redis:6.0-alpine volumes: - redis:/data command: redis-server --appendonly yes worker: # 追記 build: . environment: RAILS_ENV: development REDIS_URL: redis://redis:6379 volumes: - .:/myapp depends_on: - redis command: bundle exec sidekiq -C config/sidekiq.yml volumes: mysql-data: redis: # 追記 動作確認 設定の変更を反映させるためにコンテナを再起動させます。 $ docker-compose down $ docker-compose up -d http://localhost:3001/sidekiq にアクセスして良い感じのダッシュボードが表示されればOK。 「cron」タブを開いてみると、先ほど作成した定期実行がスケジューリングされています。 $ docker-compose logs -f worker worker_1 | 2021-07-23T20:37:18.682Z pid=1 tid=go7s74505 INFO: Cron Jobs - add job with name: work_detail_import worker_1 | 2021-07-23T20:37:20.715Z pid=1 tid=go7s74505 INFO: Booted Rails 6.1.4 application in development environment worker_1 | 2021-07-23T20:37:20.715Z pid=1 tid=go7s74505 INFO: Running in ruby 2.6.3p62 (2019-04-16 revision 67580) [x86_64-linux] worker_1 | 2021-07-23T20:37:20.715Z pid=1 tid=go7s74505 INFO: See LICENSE and the LGPL-3.0 for licensing details. worker_1 | 2021-07-23T20:37:20.715Z pid=1 tid=go7s74505 INFO: Upgrade to Sidekiq Pro for more features and support: https://sidekiq.org worker_1 | 2021-07-23T20:40:07.738Z pid=1 tid=go7ssy92d class=TestWorker jid=be181a712bacc7ece7d0d7c7 INFO: start worker_1 | Hello World! worker_1 | 2021-07-23T20:40:10.093Z pid=1 tid=go7ssy92d class=TestWorker jid=be181a712bacc7ece7d0d7c7 elapsed=2.353 INFO: done worker_1 | 2021-07-23T20:45:18.099Z pid=1 tid=go7sopr55 class=TestWorker jid=82dccac35bf6b3b56cd2d701 INFO: start worker_1 | Hello World! worker_1 | 2021-07-23T20:45:18.103Z pid=1 tid=go7sopr55 class=TestWorker jid=82dccac35bf6b3b56cd2d701 elapsed=0.004 INFO: done 「docker-compose logs」コマンドでログを確認し、5分おきに「Hello World!」と出力されれば無事動いていると考えて大丈夫です。その他も時が来ればしっかりと実行されるはず。 あとがき 以上、Annict様としょぼいカレンダー様の力を借りて俺流アニメデータベースを作ってみました。 やはり自分で作ったアプリというのは愛着が湧くものなので、今後視聴するアニメを選ぶ際などに利用したいと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む