猫Rails

ねこー🐈

mofmofさんの会社体験会に参加したら、みんな虜になったよ🐑

f:id:nekorails:20190920172909j:plain

先日mofmof inc.さんでmofmofさんのアジャイル開発を体験できるイベントが開催されたので、愛知県から新幹線で参加させていただきましたー!

www.wantedly.com

このイベントはmofmofさんに興味があるエンジニア見習いさんに、mofmofさんがどんなところかを知ってもらうために開かれたもので、これ以外にも定期的に開催されているそうです。

イベントは以下の流れで進みましたー。

  1. mofmofさんの会社紹介
  2. 社員さん、参加者さんの自己紹介
  3. KPTワークショップ
  4. 社員さんに質問する会

mofmofさんの会社紹介

(おそらく)こちらのスライドで紹介してくださいました。

speakerdeck.com

社員さん、参加者さんの自己紹介

社員さんは4名。
参加者は9名で、プログラミングスクールの生徒さん、独学でプログラミングを学習されている方、現在プログラマーで転職を考えている方などが参加されていました。

KPTワークショップ

KPT(Keep/Problem/Try)とはシンプルで強力なふりかえりの手法です。

(社員さんがすごくわかりやすいスライドでKPTの説明してくださったのですが、そのスライドを見つけられず😹・・・うろ覚えですがこんな感じだった気がします💦)

下記のような3つの要素に分けて現状分析を行います。

  • Keep: 良かったこと(今後も続けること)
  • Problem: 悪かったこと(今後はやめること)
  • Try: 次に挑戦すること(Problemの改善策、Keepでさらに改善すること)

ふりかえりを円滑にすすめるために、以下のグラウンドルールが用意されています。

  • 積極的に話し、参加する
    • 当事者意識を持つ
  • 1人で話しすぎない
    • 発言をさえぎらない
    • 話してない人にも思いあり
  • 原因追求をする。個人の責任追求をしない
    • 罪を憎んで人を憎まず
    • 「人 vs 人」ではなく、「チーム vs 問題」の構図を意識する
    • だから自己弁護も不要

このワークショップでは、最初に「砂漠で遭難したときにどうするか」というコンセンサスゲームを行い、それをKPTで振り返るということをしました。

答えを知っているとゲームがつまらなくなってしまうそうなので、詳細は割愛します🙇

最後は「ふりかえりのふりかえり」としてKPT自体をふりかえります。
自分達のチームはTryが抽象的だったので、具体的な行動に落とし込むといいよと社員さんにアドバイスいただきました。(「時間配分を意識する」だと抽象的なので、「xxさんが、タイムキーパーをやる」まで落とし込む)

社員さんも参加者の方も皆さん優しくて、楽しい上にめちゃ勉強になるという最高の体験でした。

社員さんに質問する会

結構際どい質問にも答えていただきました。書ける範囲で。
(結構失礼なことも聞いてしまった気がします・・・。申し訳ありませんでした🙇)

案件はどれくらい走ってますか?

20程度。一人で2案件掛け持ちすることも。その場合コンテキストスイッチが大変だけど、時間配分は個人の裁量に任されていて柔軟に対応できるようにしている。

デザイナーさんはいますか?

いない。お客さんにデザイナーさんを用意していただくとか、簡単なものならBootstrapとかのCSSフレームワークを使ってプログラマが作ることもある。デザインが得意なプログラマもいる。

社員さん、フリーランスさんの割合はどれくらいですか?

10名ずつ程度。フリーランスの場合は基本2.5日/週から。社員でも2.5日/週を選べる。

参考: 新しい働き方 | 月額制受託開発の株式会社mofmof

コードレビューはしていただけますか?

基本全てコードレビューする。想像以上にやる。研修時には50とかコメント付くこともある。

参考

フロントエンドの技術はなんですか?

Vue.jsがメイン。フロントはSPAでRailsAPIサーバーに徹する案件も。Reactも得意な人いる。jQueryは使ってない。

モダンフロントエンドの知識ない場合、入社してからの勉強でも大丈夫ですか?

ES6の知識なかったが入社してから勉強した。Railsも(事前に勉強したが、)実務未経験で入社した。ただ手取り足取り教えてもらえるわけではないので、自分で学んでいく姿勢が大事。

採用で重視するポイントはなんですか?

技術力/コミュニケーション力/人間性。技術力が高くてもmofmofの価値観と合わないと難しいかも。

参考: 採用面談で失敗してしまいがちな3パターンをまとめた - 毎日がもふもふ

3日でプロダクトを作るなんて可能なのですか?

フル機能を作るわけでなくMVPを作るので可能。MVP(Minimum Viable Product)と呼ばれる実用最小限の製品を作り、ユーザーに価値を提供できているか仮説検証を行う。DropboxのMVPはデモ動画だった。

参考: イキナリ!MVP

(結構思い込みで補完してしまっている気がします・・・💦ミートアップが定期的に開かれているそうなので、正確な情報はその時に聞いていただけると幸いです🙇)

感想

めちゃ楽しかったです!

mofmofさんは企業ブログや社長さんのブログ等、アクセスできる情報が充実しているのでだいたいどんな会社なのかイメージできていたのですが、イメージそのままでした。
技術が好きで、ものづくりが好きで、優しい会社さんです。
社員さん皆さんから優秀さと優しさが滲み出てました。

帰り道で他の参加者の方達と本音トークさせていただきました。
「めっちゃ良い」、「絶対行きたい」、「うちの会社とぜんぜん違う」等、自分も含めてみんなmofmofさんの虜になってる感じでした。

mofmofさん、参加者の皆さん、楽しいイベントをありがとうございました!

プログラミングスクールで、「リアルバーチャルYoutuber」というWebサービスを作りました

f:id:nekorails:20190910041229p:plain

はじめに

(まだまだRails勉強中の身で、至らない点も多いと思います💦よろしくお願いします🙇)

FJORD BOOT CAMP(フィヨルドブートキャンプ) というプログラミングスクールの最終課題で、自作のWebサービスを作りました。
本記事はWebサービスの紹介と開発日誌のまとめです。

作ったWebサービスの紹介

リアルバーチャルYoutuber」は、AIが生み出した実在しない人物の顔を、AIによる顔合成技術でユーザーの顔に合成することで、顔を匿名化するWebサービスです。

f:id:nekorails:20190910040516g:plain

f:id:nekorails:20190910040519g:plain

ユーザーは、ベースとなる動画をアップロードして、用意された実在しない人物の顔モデルを選ぶことで、その顔になることができます。

f:id:nekorails:20190910192547g:plain

f:id:nekorails:20190910192553g:plain

実写Youtubeをしたいけれど顔出しはしたくない、といった方達に使って頂けると嬉しいです。
Rails + Herokuで作られていて、開発期間は2週間ほどです。
利用は無料で、ソースコードOSSとして公開しています。

サービス: https://realvtuber.shita1112.com
ソースコード: https://github.com/shita1112/realvtuber

開発日誌

以下、開発時にやったことのまとめです。
フィヨルドブートキャンプ生のような、初めてWebサービスを作る方達に読んでいただけると嬉しいです。
次回Webサービスを作る時用の備忘録も兼ねているので、リンク多めです。ご了承ください🙇

Getting Realを読む

まず最初にフィヨルドブートキャンプでサービス開発の教科書としておすすめされた Getting Real を読みました。(たしか翻訳もあったはずですが、なぜか検索しても見つかりませんでした💦すみません🙇)
Rails作者のDHHさんのいる会社「BaseCamp」の社長が書いた文章です。
サービス開発する際に大切なことがたくさん書かれています。
サービス開発で悩んだ際は、この本に立ち返りつつサービス開発を進めていきました。(とはいえサービス開発の哲学が凝縮された本なので、まだ消化しきれていないことが多いのですが・・・。)

今後も何度か読むことになりそうです。

エレベーターピッチを作る

エレベーターピッチとは、投資家さんとエレベータで30秒だけ一緒にいられる場合に、短い時間でサービスの本質を伝えるための文章です。
趣味の個人開発の場合でも、エレベーターピッチを作っておくとプロダクトの「解決する課題」や「ターゲット」等を自分の中で整理でき、「本当に必要な機能」と「必要ない機能」を実装前に知ることができます。
あと、それをメンターさんたちと共有できます。

エレベーターピッチのテンプレートはいくつかパターンがあるようですが、フィヨルドブートキャンプで使っているのはこんな感じです。

サービス名 というサービスは、
解決する問題 を解決したい
このサービスを使うターゲット 向けの、
サービスのカテゴリーです。
ユーザーは このサービスでできること ができ、
競合サービス とは違って、
差別化要素 が備わっている事が特徴です。

自分の場合はいくつかぼんやりしたWebサービスのアイデアがあったのですが、自分で一人では考えをまとめられなかったので、一度エレベーターピッチにしてメンターである@komagataさんと@machidaさんのレビューを受けることにしました。

作ったエレベーターピッチは以下の4つです。

リアルバーチャルYoutuber というサービスは、
Youtubeで実写動画を作る際に、顔を出さなければいけないという問題 を解決したい
Youtubeで実写動画を公開したいけど、顔出しはしたくない人 向けの、
動画の顔の匿名化ツールです。
ユーザーはAIで生成された実在しない人物の顔を、AIによる顔転写技術を使ってユーザーの顔に転写することで、ユーザーの顔を実在しない人物の顔に置き換えることができ、
バーチャルYoutuberとは違って、
実写に使えます。

Readme翻訳 というサービスは、
Gemを利用する時に、英語が苦手なためReadmeを読まないという問題 を解決したい
英語が苦手なRailsプログラマ(=自分) 向けの、
英語技術文書のリーディング支援ツールです。
ユーザーはReadmeの隣に表示されるGoogle翻訳の結果を読み、日本語で大意を掴むことで、Readmeを読む負担を減らすことができ、
日本語に翻訳(chrome内蔵のGoogle翻訳機能)でReadmeを読む 場合とは違って、
Google翻訳を原文を読むための補助として利用します。

Tsukutte(ツクッテ) というサービスは、
技術力UPとポートフォリオ強化のためにWebサービスを作りたいが、作りたいものがないという問題 を解決したい
Webエンジニア 向けの、
マッチングサービスです。
依頼者は自分が作って欲しいWebサービスの内容をTsukutteに書き込み、開発者は書き込み内容から作りたいWebサービスを選んで開発することができ、
クラウドソーシングでWebサービスの開発を依頼/開発する場合 とは違って、
無料なので依頼者/開発者ともに気楽に使えます。

Readch(リードック) というサービスは、
英文のEPUBやPDFを読む際に、英文リーディング用のChrome拡張(Mouse Dictionary等)を使えないという問題 を解決したい
英語が苦手なRailsプログラマ(=自分) 向けの、
洋書の技術書のリーディング支援ツールです。
ユーザーが英文のEPUB/PDFをサイトにアップロードすると、HTMLに変換されてレンダリングされるので、Chrome拡張を使って読むことができ、
EPUP/PDFをHTMLに変換するWebサービス とは違って、
HTMLをダウンロードせずに、サイト上で読むことができます

フィヨルドブートキャンプからのアドバイスで、エレベーターピッチを作る際には以下の2点に気をつけました。

「自分の課題を解決するサービスにする」
自分の課題を解決するサービスだと、ターゲットが自分になり普段痛みを感じている具体的な課題から始めることが出来るので、課題の質を上げられます。
あと必ず自分は使うので、個人開発でありがちなユーザー数0という悲劇を避けられます。(このパターンは本当に多いそうです。)

CGM(Consumer Generated Media)型ではなくツール型にする」
CGMだとそもそもユーザーやユーザー投稿が集まらないと始まらないので、避けたほうがいいそうです。
ツール型なら過疎っていてもとりあえず自分は使えるので安心です。

@machidaさんや@komagataさんに相談しつつ、最終的にはツール型でポートフォリオとして面白がってもらえそうな「リアルバーチャルYoutuber」を作ることに決めました。

参考

技術検証

フェイススワップ(ディープフェイク)というDeep Learningを用いた顔転写技術とこの生成技術で今回のプロダクトを作れそうと思いましたが、自然な出来になるのか心配だったので先に検証しました。

最初の試作動画はこんな感じです。

f:id:nekorails:20190910020508g:plain

少し顔が崩れていますが、改良すればなんとかいけるかなーと判断して開発を始めました。

いくつか調整を重ねて、今はもう少し質が上がっています。

f:id:nekorails:20190910015744g:plain

ペーパープロトタイプを作る

作るもののざっくりとしたUIを書くことで、必要な機能やUIの検討ができます。

自分はこんな感じで作りました。

ログイン前の画面4つ

f:id:nekorails:20190910002337j:plain

ログイン後の画面4つ

f:id:nekorails:20190910002403j:plain

ここでもメンターさん達にレビューしていただき、そもそもログインはいるのかー?等をつめていきました。

参考

技術選定とシステム構成図の作成

技術スタックはこんな感じです。

  • バックエンド: Rails
  • フロントエンド: Vue.js + Webpack(Webpacker) + Material Design for Bootstrap 4
  • インフラ: Heroku + S3 + CloudFront + MySQL

バックエンドはちょうどRails6がリリースされたタイミングだったので、Rails6で作りました。
gemが対応できているか少し心配でしたが、特に問題なく開発できました。

フロントエンドはVue.jsの勉強をがっつりしたばかりだったので、経験を積むためにVue.jsを選びました。(でも結局JS側のコードはあんまり書きませんでした😹)
CSSフレームワークにはMaterial Design for Bootstrap 4を採用しました。

インフラはHerokuを採用しています。
CDNはHerokuのFastlyアドオンを使うと簡単にできるそうなのですが、価格面を考慮した結果CloudFrontを採用することにしました。

システム構成はこんな感じです。

f:id:nekorails:20190910002900j:plain

動画作成でDeep Learningを使うので、GPUサーバーが必要です。
しかしAWSGCPGPUサーバーを使おうと思うと、GPU1つで数万円/月もかかってしまい、とても運用できません💦
たまたま自宅にGTX 1080 Tiが4枚あったので、自宅でGPUサーバーを組んで運用することにしました。
GPUサーバーにもコードを置いて、そこでJobワーカーを動かします。

あとこの図だとassetsをS3に置いていますが、 @komagata さんにassetsはS3に置かずにHerokuから配信すればいいよーとアドバイスいただいたので、今はHerokuからassetsを配信して前段にCloudFrontを置くように変更しています。

開発の進め方を考える

タスク管理にはGithubプロジェクトを利用していました。
しかし途中から面倒になってしまい、 Emacsのorgモード で管理するようになりました。
(orgモードはMarkdown相当の機能にTODO管理とかの便利機能を色々詰め込んだ感じのものです。)

ワークフローはOSSなのでちゃんとしたほうがいいのかなと思い GitHub Flow と呼ばれるものを使っていました。
しかしこちらも途中から面倒になってしまい、結局masterに直接commitするようになりました。

$ rails new

ここまでで開発の準備ができたので、ここから実際の開発を進めていきます。
$ rails newRailsアプリを作成します。この際に--skip-xxxxオプションを使うと不要な機能を外してくれるので便利です。

$ rails new realvtuber --skip-test --skip-turbolinks --skip-active-storage --skip-action-cable --skip-action-text --skip-action-mailbox --webpack=vue --database=mysql

テストはRSpecを使い、ファイルアップローダーはActiveStrageの代わりにCarrierWaveを使うのでskipしました。
あとVue.jsやMySQLを使う場合はここで指定しておくと、自分で設定する手間を省けます。

Herokuをセットアップ

PaasにはHerokuを利用しました。

参考

独自ドメイン設定 + SSL

Herokuアプリを作ると「サービス名.herokuapp.com」ドメインが割り当てられます。
これを利用してもいいのですが、せっかくなので独自ドメインお名前.com で取得して設定します。

実はこのアプリを作る前に「Readme翻訳」というサービスを開発をするために独自ドメインを取得したのですが、結局諸事情で開発が中止になってしまいドメインだけが手元に残ってしまいました。
とても悲しかったので、その教訓から作ったWebサービスが誰からも使われなくてさみしい問題を解決する(しない)意識低い方法論 を参考にさせていただいて、サブドメインで運用することにしました。

Herokuで独自ドメインSSL化するのは若干面倒でしたが、以下の記事等を参考にしてなんとかいけました。

参考

ルートドメインを設定する場合にも、PointDNSというアドオンを使えば無料でいけます。

参考

SendGridをセットアップ

メール配信にはSendGridアドオンを利用します。
無料枠が大きいので、個人開発の場合は無料枠に収まることが多そうです。

参考

S3 + CloudFrontをセットアップ

Herokuにはファイルを置けないので、ファイル置き場としてS3を用意します。
あと配信速度を上げるためにCDNとしてCloudFrontも一緒に利用します。
S3の前段にCloudFrontを置くイメージです。
CloudFront + S3 によるCDN (Cache Distribution パターン) 構築手順 を参考に設定すればいけました。
特に難しい箇所もなく、基本的にはデフォルト設定でOKです。

assetsの配信でHerokuの前段にCloudFrontを置く場合は、Rails側の設定も必要になります。
特にWebフォントを使用するためにはCORS対応が必要になり、そこでハマりました。
以下のサイトが参考になります。

Googleアナリティクスをセットアップ

アクセス解析を行うために、Googleアナリティクスを導入しておきます。

参考

Googleサーチコンソールをセットアップ

サイトの検索キーワード等を調べるために、Googleサーチコンソールの設定を行っておきます。

参考

ロゴ・ファビコンを作る

ロゴはWebで使えるデザインツールのCanvaで作りました。

f:id:nekorails:20190910004423p:plain

こんな感じでフォントを最大サイズにして、フォントパワーに全てを託します。

あとは色を反転させたものをファビコンとして使います。
ファビコンはこちらのサイトで作れます。

参考

サイトマップを自動作成

サイトマップGoogle等の検索エンジンにサイトの情報を伝えることで、インデックスを促します。
sitemap_generatorというgemを使い、リリース時にサイトマップを自動作成するようにしておきます。

参考

metaタグを設定する

SEO対策としてtitle等のmetaタグを設定できるようにします。
meta-tags というgemを利用しました。
必要な部分だけ各ページで上書きできるので、メンテしやすくてよかったです。

参考

Rubocopを設定する

Rubocopを使い、コーディング規約を強制します。
Rubocopのデフォルト設定は(Ruby Style Guide通りなのですが)ルールが厳しめで守るのが難しいので、少し緩くしたいです。
@onkさんがいい感じの設定をgemにまとめてくださっているので、そちらのonkcopというgemを利用します。

# Gemfile

group :development do
  gem "onkcop", github: "shita1112/onkcop" # onkcopがrubocop最新版に対応していないようなので、一時的にforkを使う
  gem "rubocop"
  gem "rubocop-rails"
  gem "rubocop-rspec"
end
$ bundle install

FrozenStringLiteralの設定を追加しておきます。

# .rubocop.yml

inherit_gem:
  onkcop:
    - "config/rubocop.yml"
    - "config/rails.yml"
    - "config/rspec.yml"

AllCops:
  TargetRubyVersion: 2.6
  Exclude:
    - "bin/bundle"
    - "node_modules/**/*"

Style/FrozenStringLiteralComment:
  Enabled: true

rubocopコマンドの--auto-correctを使うとルールに合わせて機械的に変換してくれるので、使っておきます。

$ rubocop --auto-correct

参考

便利Gemを導入する

開発する際に便利なGemを先に導入しておきます。

bullet

N+1の検出ができます。

letter_opener

developmentモードで簡単に送信メールの確認できます。

letter_opener_web

letter_openerのwebインターフェースです。

xray-rails

どの部分がパーシャルか、画面上に表示してくれます。

pry-rails

binding.pryブレークポイント仕込めます。

pry-byebug

binding.pryでステップ実行できます。

pry-doc

Cで書かれたRubyソースコードを表示できます。

pry-alias

binding.pryの代わりにbpでいけます。

awesome_print

pppより出力が綺麗なapが使えるようになります。

tapp

レシーバをputsしてくれるtapです。デバッグで利用します。

[1, 2, 3].tapp(&:size)
3
=> [1, 2, 3]

rack-mini-profiler

簡易的なパフォーマンス測定として、ページ左上にレンダリング時間を表示してくれます。

newrelic_rpm

パフォーマンス測定できるNew Relicというサービスを利用できます。Herokuのアドオンとして基本機能は無料で使えます。

enum_help

Rails標準機能のenumi18n機能を追加してくれます。

config

環境毎に定数管理できます。

gon

Rails側からJS側に変数を渡せます。

browser

ブラウザ/デバイスの種類を判定できます。スマホかどうか調べるのに利用します。

devise

認証。カスタマイズが難しく避けられがちですが、今回はビューの変更だけで済みそうだったので利用しました。

pundit

認可。リソース毎にファイルを作るので、cancancanより好みです。

active_decorator

デコレータ。自動的にデコレートしてくれるので、draperより好みです。関連先もデコレートしてくれるようになっていました。

carrierwave

ファイルアップローダー。たしかActiveStrageにはキャッシュとバリデーション機能がまだなかったはずなのでこちらを利用します。

Railsを日本語化

設定を変更して、タイムゾーンとlocaleを日本向けにします。

# config/application.rb

+    config.time_zone = "Tokyo"
+
+    config.i18n.default_locale = :ja

日本語のロケールファイルはこれを利用しました。

利用規約・プライバシーポリシーを作る

フッターに置く利用規約とプライバシーポリシーを作成します。
何を書けばいいのか全くわからなかったのですが、フィヨルドブートキャンプの先輩方のサービスを参考にさせていただきました🙇

あと今読んでいる途中なのですが、【改訂新版】良いウェブサービスを支える 「利用規約」の作り方 が利用規約を置く意義等が丁寧に書かれていてとても参考になります。

PageSpeed Insightsで表示速度を測定する

Railsアプリが一通りできたらPageSpeed Insightsで表示速度を測定します。
PageSpeed Insightsはサイトの表示速度を測定した上で、遅い場合には改善策を提示してくれます。

測定してみるとリアルバーチャルYoutuberはパソコンが98点、モバイルが89点でした。 f:id:nekorails:20190910005453p:plain f:id:nekorails:20190910005528p:plain

「合格した監査」を見てみると、画像を適切なサイズで配信できていることや、CSS/JSをminifyできていること等を確認できます。 f:id:nekorails:20190910014533p:plain

「改善できる項目」を見てみると、「次世代フォーマットでの画像の配信」を改善すれば速くできることがわかります。
この場合はWebp対応ブラウザにWebpを配信すれば速くできそうです。 f:id:nekorails:20190910005622p:plain

@machida さんによるデザインレビューを受ける

メンターでデザイナーの@machida さんにデザインレビューをしていただきました。

こちらがもともとのデザインです。
作成した動画をリストとして並べています。 f:id:nekorails:20190910195520p:plain

次がレビューを元に修正したデザインです。
作成した動画をカードとして並べることで、前のデザインよりスッキリして見えます。
あとh1タグはただでかい文字が置かれているだけだったのですが、別ブロックを作ることで見やすくなりました。

f:id:nekorails:20190910195443p:plain

他のページもかなり丁寧にレビューしてくださいました。

普段デザインのことはあまり考えていなかったので、デザインはとても苦労しました。
開発のかなりの時間をデザインに使ってた気がします。

なのでプロのデザイナーである@machidaさんにアドバイス頂けたのはとても助かりましたし、勉強になりました。

@komagata さんによるコードレビューを受ける

リリースを優先させてテストコードがおざなりなので、これからテスト書いてレビューしていただきます。

さいごに

謝辞

メンターとして相談にのってくださった@machidaさん、@komagataさん、毎週の進捗報告会に付き合ってくださった@matt59649858さん、@tararicoさん、@wai_doiさん、フィードバックをくださったフィヨルドブートキャンプの皆さん、唐突なお願いにも関わらず動画提供や戦略面でのアドバイスなど親身になって協力してくださったYoutuberの@fujishuuさん、@Layer_Qさん、皆さんのおかげでなんとか完成させることができました。本当にありがとうござました🙇

あとうちのお猫様には開発中も癒やされました。ありがとうございました😺 f:id:nekorails:20190910011444j:plain

(完成祝にちゅーるあげました)

Railsのお仕事探しています🙇

これでプログラミングスクールが完了になるので、これからお仕事探しを本格的に始めます。
もしRailsプログラマ探してるよーという方がいましたら、自分の経歴をまとめたのでこちらを見ていただけると嬉しいです
よろしくおねがいします🙇

Vue.js入門用の資料 まとめ

Vue.jsに入門して1ヶ月ほど経ったので、良かった資料をまとめておきますー😺

入門用の資料のみ、日本語の資料のみで、易しい順です。

やわらかVue.js

実際にVue.jsの勉強を始める前に読むと良さそうです。

漫画とかも使っていて、やさしくVue.jsの全体像を理解できます。

Vue.jsはデータバインディングの機能だけなら小さくて学びやすいのですが、エコシステムを含めると勉強すべきことが大量にあり、先に全体像を把握しておかないと迷子になります。(なりました。)

Vue.jsの学び方と、書籍のページが特におすすめです。

Vue JS入門決定版!jQueryを使わないWeb開発 - 導入からアプリケーション開発まで体系的に動画で学ぶ

自分的にはイチオシです。

Udemyの有料のスクリーンキャストです。公式ドキュメントから特に大事な部分を選別して、初心者向けに丁寧に説明してくれる感じです。選別したと言っても8時間分のボリュームがあり、これだけで基礎を網羅的に学べます。演習も十分用意されています。フロントエンドに慣れていない人に配慮してかES2015を使わずにES5を使っているので、const=>がわからない人でも安心です。 

自分はいきなり公式ドキュメントやって辛かったのでこの動画を併用したのですが、かなりスムーズに進みました。演習も含めて公式ドキュメントに即した形になっているので、公式ドキュメントを読むための前学習としても最適です。

自分はセールで1,300円で買ったのですが、今見たら定価の7,200円になってました。素晴らしい動画なので7,200円の価値はあるとは思うのですが、Vue.jsは他にも良い資料がたくさんあるので買うべきかは悩ましいところです。

またセールするかもしれないので、その時は買いです。

基礎から学ぶ Vue.js

本を使って勉強したい方におすすめです。

通称猫本と呼ばれているそうです。こちらもVue JS入門決定版と同じくらいわかりやすいです。Vue JS入門決定版と比べると、少しだけレベルが高く、情報量が多く、より網羅的です。Vue JS入門決定版では扱っていないvuexまで扱っています。サポートページも充実しています。

個人的には動画が好きなのでVue JS入門決定版がおすすめですが、こちらも素晴らしいです。

ただ、epub版を買ったのですが固定レイアウトでした。コピペできない。悲しみです...😹

公式ドキュメント

当たり前ですが、一番おすすめです。

わかりやすいし、情報が充実してるし、翻訳されてるし、最高です。公式ドキュメント書いてる方達、翻訳してる方達に感謝です🙇

勉強する時は公式ドキュメントを中心に考えるのが良さそうです。

ただフロントエンドの知識がない場合、いきなり公式ドキュメントをやるとちょいツラかもです。というか、自分がそうでした。 なので、まずはここより上の資料で公式ドキュメントを読めるレベルになって、次に公式ドキュメントを読んで、そしてここより下の資料で実務レベルの知識を補強していくのがいいのかなーと思います。

Vue.js入門 基礎から実践アプリケーション開発まで

入門とありますが応用的な内容が多く、公式ドキュメントを一読した後でやるのがよさそうです。

jQueryからの移行や、コンポーネント設計の技法等、実際のプロダクト開発で必要になるであろう実践的な知識をまとめて獲得できます。

特に8章~10章の中規模・大規模向けのアプリケーション開発は知見の塊で、唯一無二な感じです。

公式ドキュメントと同じく、今後何度も読み返すことになりそうです。

すごくおすすめです。

Examples

公式さんがいくつもお手本となるアプリを用意してくれてるようです。写経してると、なんか自分でもできる気がしてきます。(気がするだけです)

web

Vue.jsは大人気みたいで、web上に大量の知見があります(感謝)。自分は休憩時にはてなブックマークの検索で新着順で遡って、気になった記事を読んでました。

実際の現場でどんな感じで使われているのか勉強になります。

結局どれをやればいいの?

プログラミングに慣れてる人は公式ドキュメントだけでいいと思いますー。
プログラミングに慣れていない人はVueJS入門決定版 OR 基礎から学ぶVue.js -> 公式ドキュメントの順でやるのがいいと思います。
自分みたいにフロントエンドの知識がなくて、かつ時間がある人は、全てやるのもありだと思います。重複は多いですが、いろんな視点で学べるので理解が深まる気がします。

ではー。

ポリモーフィック関連のコントローラー

まだまだRails勉強中の身なので、間違いもあるかと思います💦間違いを見つけた場合は、コメントいただけると嬉しいです🙇

お世話になっているFJORD BOOT CAMP(フィヨルドブートキャンプ)さんでこのような課題が出ました。

ポリモーフィック関連を使い、BookとReportにコメント機能をつけよ。(要約)

シンプルな課題ですが、難しいです。 ポリモーフィック関連の実装方法を知っているだけではだめで、以下のような知識も必要になります。

Commentのコントローラーを実装しながら、解説に挑戦してみます!

シンプルなコントローラー

まずはCommentのCRUDが欲しいので、CommentのScaffoldを作成します。

$ rails g scaffold Comment body

ルーティングとコントローラーはこんな感じです。

# config/routes.rb
resources :comments
# app/controllers/comments_controller.rb
class CommentsController < ApplicationController
  before_action :set_comment, only: [:show, :edit, :update, :destroy]

  # GET /comments
  def index
    @comments = Comment.all
  end

  # GET /comments/1
  def show
  end

  # GET /comments/new
  def new
    @comment = Comment.new
  end

  # GET /comments/1/edit
  def edit
  end

  # POST /comments
  def create
    @comment = Comment.new(comment_params)

    if @comment.save
      redirect_to @comment, notice: 'Comment was successfully created.'
    else
      render :new
    end
  end

  # PATCH/PUT /comments/1
  def update
    if @comment.update(comment_params)
      redirect_to @comment, notice: 'Comment was successfully updated.'
    else
      render :edit
    end
  end

  # DELETE /comments/1
  def destroy
    @comment.destroy
    redirect_to comments_url, notice: 'Comment was successfully destroyed.'
  end

  private
    def set_comment
      @comment = Comment.find(params[:id])
    end

    def comment_params
      params.require(:comment).permit(:body)
    end
end

よく見るコードですが、大事なコードです。 Scaffoldで作成されるコントローラーは、コントローラーの理想形だと思います。 できるだけこの形を壊さないように、修正していきます。

ネストしたリソースのコントローラー

少し遠回りになりますが、ポリモーフィック関連を実装する前にネストしたリソースを実装します。

ネストしたリソースというのはこれのことです。

resources :reports do
  resources :comments
end

通常のresources :commentsだとコメント一覧はGET /commentsです。

一方ネストしたリソースだとGET /reports/1/commentsになります。

URLでReportとCommentの親子関係を表現することで、id=1のreportに紐づくcomment一覧を取得できるようになります。

ポリモーフィック関連のリソースは必然的にネストしたリソースになります(たぶん)。 ポリモーフィック関連とネストしたリソースの2つを同時に実装すると、問題が起きた時に切り分けが難しくなるので、慣れないうちは分けて考えたほうがいいかと思います。

以下、ネストしたリソースのコードです。

モデル

# app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :report
end

# app/models/report.rb
class Report < ApplicationRecord
  has_many :comments
end

ルーティング

resources :reports do
  resources :comments
end

コントローラー

class CommentsController < ApplicationController
  before_action :set_report
  before_action :set_comment, only: [:show, :edit, :update, :destroy]

  # GET /reports/1/comments
  def index
    @comments = @report.comments
  end

  # GET /reports/1/comments/1
  def show
  end

  # GET /reports/1/comments/new
  def new
    @comment = @report.comments.build
  end

  # GET /reports/1/comments/1/edit
  def edit
  end

  # POST /reports/1/comments
  def create
    @comment = @report.comments.build(comment_params)

    if @comment.save
      redirect_to [@report, @comment], notice: 'Comment was successfully created.'
    else
      render :new
    end
  end

  # PATCH/PUT /reports/1/comments/1
  def update
    if @comment.update(comment_params)
      redirect_to [@report, @comment], notice: 'Comment was successfully updated.'
    else
      render :edit
    end
  end

  # DELETE /reports/1/comments/1
  def destroy
    @comment.destroy
    redirect_to [@report, :comments], notice: 'Comment was successfully destroyed.'
  end

  private
    def set_report
      @report = Report.find(params[:report_id])
    end

    def set_comment
      @comment = Comment.find(params[:id])
    end

    def comment_params
      params.require(:comment).permit(:body)
    end
end

参考: nested_scaffold/controller.rb at master · amatsuda/nested_scaffold · GitHub

ポリモーフィックなルーティング

上記のコードでは、createアクションのリダイレクト処理はこうなっています。

redirect_to [@report, @comment], notice: 'Comment was successfully created.'

[@report, @comment]の部分は、Rails内部でurl_for([@report, @comment])となり、/reports/1/comments/1というurlになります。

[@report, @comment]の部分はreport_comment_path(@report, @comment)という書き方もできます。 こっちの方がよく見る気がします。 しかしこっちの書き方だと、ポリモーフィック関連を使う際に問題が出ます。

後でポリモーフィック関連を実装する際に、reportだけでなくbookも扱えるようにするため、[@report, @comment]@reportの部分を@commentableに抽象化して[@commentable, @comment]にします。 この時@commentableの参照先はreportオブジェクトbookオブジェクトです。

Rails@commentableをいい感じに解釈して、@commentablereportオブジェクトの場合は/reports/1/comments/1というURLを作り、@commentablebookオブジェクトの場合は/books/1/comments/1というURLを作ってくれます。

このコード1つで2つのURLに対応してくれます。

ここでreport_comment_path(@report, @comment)と書いてしまうと、後に分岐処理を書くことになり、ポリモーフィック関連の強みを活かせません。 そのため[@report, @comment]という書き方を利用しています。

ちなみに[@report, @comment]のような書き方は、redirect_toだけでなくlink_toform_withでも利用できます。 この知識はビューを書く際に必要になります。

参考

ポリモーフィック関連のモデル

ポリモーフィック関連ポリモーフィック(polymorphic)とはオブジェクト指向の本などに出てくるポリモーフィズム(polymorphism)のことです。

Rubyの場合はダックタイピングの文脈で語られます。

そのためポリモーフィック関連を正しく使うためには、ポリモーフィズムダックタイピングの知識が必要になります。

自分はオブジェクト指向の知識が全くない状態でポリモーフィック関連を使い、悲惨な目にあいました😂

以下の2つの記事が素晴らしいのでおすすめです。

それではポリモーフィック関連を実装していきます。

素朴に実装するとこうなります。

# app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :commentable, polymorphic: true
end

# app/models/report.rb
class Report < ApplicationRecord
  has_many :comments, as: :commentable
end

参考: Active Record の関連付け - Railsガイド

これでも良いのですが、Commentableとしての責務をmoduleに切り出すとさらに良いです。 moduleに切り出すことで名前と境界が明確になり、ReportがCommentableとして振る舞えることを意識しやすくなります。

# app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :commentable, polymorphic: true
end

# app/models/report.rb
class Report < ApplicationRecord
  include Commentable
end

# app/models/concerns/commentable.rb
module Commentable
  extend ActiveSupport::Concern

  included do
    has_many :comments, as: :commentable
  end
end

moduleに切り出す際にActiveSupportのconcernという機能を使っています。

参考: [Rails] ActiveSupport::Concern の存在理由 - Qiita

これでreport - commentsだった関連がcommentable - commentsとなり、抽象化できました。 Bookでinclude Commentableすることで、bookreportを同じcommentableとして扱えるようになります。

ポリモーフィック関連のコントローラー

ネストしたリソースのコントローラーを修正してポリモーフィック関連のコントローラーにします。

ルーティングはそのままです。

resources :reports do
  resources :comments
end

コントローラーはこうなります。

class CommentsController < ApplicationController
  before_action :set_commentable
  before_action :set_comment, only: [:show, :edit, :update, :destroy]

  # GET /reports/1/comments
  # reportsと同じようにbooksをルーティングに追加すれば、
  # GET /books/1/commentsも可能になります。
  def index
    @comments = @commentable.comments
  end

  # GET /reports/1/comments/1
  def show
  end

  # GET /reports/1/comments/new
  def new
    @comment = @commentable.comments.build
  end

  # GET /reports/1/comments/1/edit
  def edit
  end

  # POST /reports/1/comments
  def create
    @comment = @commentable.comments.build(comment_params)

    if @comment.save
      redirect_to [@commentable, @comment], notice: 'Comment was successfully created.'
    else
      render :new
    end
  end

  # PATCH/PUT /reports/1/comments/1
  def update
    if @comment.update(comment_params)
      redirect_to [@commentable, @comment], notice: 'Comment was successfully updated.'
    else
      render :edit
    end
  end

  # DELETE /reports/1/comments/1
  def destroy
    @comment.destroy
    redirect_to [@commentable, :comments], notice: 'Comment was successfully destroyed.'
  end

  private
    def set_commentable
      resource, id = request.path.split('/')[1,2]
      @commentable = resource.singularize.classify.constantize.find(id)
    end

    def set_comment
      @comment = Comment.find(params[:id])
    end

    def comment_params
      params.require(:comment).permit(:body)
    end
end

基本的には@report@commentableに変えるだけです。

ただ、1つ難しい点があります。@commentableの取得方法です。

調てみるといくつか方法があるようですが、今回は実装が簡単なPolymorphic Association in Rails 5 の方法を使います。

request情報から強引に取得します。

    def set_commentable
      resource, id = request.path.split('/')[1,2]
      @commentable = resource.singularize.classify.constantize.find(id)
    end

ちなみにこちらの方法を使ってコントローラーを分けると、より綺麗な実装になります。

以上でポリモーフィック関連のコントローラーの完成です。

あとはBookReportと同じように実装して、ビューを修正すれば課題クリアになります。

お付き合いいただき、ありがとうございました🙇

Wardenの使い方 まとめ

  • 自分用のメモを公開したものです。ドキュメント/ソースコード等をまとめただけで試していないコードも多いので、信頼性は低いです。

Wardenとは?

  • 認証のフレームワーク。実際の認証は自分で実装する
  • 認証はRackミドルウェアで行う
  • Wardenをアプリで直接使うことはなさそう。deviseのような認証ライブラリのためのライブラリと考えたほうが良さそう
  • Deviseで使われてる。Wardenのstrategy/scope/callback等の知識がないとDeviseのコード読むのつらそう

導入

1. インストール

# Gemfile
gem 'warden'
gem 'bcrypt' # has_secure_passwordのため
$ bundle

2. ベースとなるRailsアプリ

  • routes
# config/routes.rb
Rails.application.routes.draw do
  get 'welcome/index'.
  root 'welcome#index'
end
  • コントローラー
# app/controllers/welcome_controller.rb
class WelcomeController < ApplicationController
  def index
    render text: "Welcome guest, it's #{Time.now}"
  end
end
  • Userのmigration
class CreateUsers < ActiveRecord::Migration
  def change
    create_table :users do |t|
      t.string :username
      t.string :password_digest
      t.string :authentication_token # 認証にはこれを使う

      t.timestamps null: false

      t.index :authentication_token, unique: true
    end
  end
end
  • Userモデル
# app/models/user.rb
class User < ActiveRecord::Base
  after_create :generate_authentication_token!

  has_secure_password

  private

  # user生成後に一意となるauthentication_tokenを作成する
  def generate_authentication_token!
    self.authentication_token = Digest::SHA1.hexdigest("#{Time.now}-#{self.id}-#{self.updated_at}")
    self.save
  end
end

3. Strategyを定義する

  • strategyは認証の実装。これらを切り替えることで、認証方法を切り替えられる(Strategyパターン)
  • ここで定義するのは認証トークンを利用した認証
  • Railsの場合はlib/strategiesを用意してそこに置くのが良さげ
# lib/strategies/authentication_token_strategy.rb
class AuthenticationTokenStrategy < ::Warden::Strategies::Base
  # ガード
  # params['authentication_token']がある場合のみ、authenticate!を実行する
  def valid?
    params['authentication_token']
  end

  # params['authentication_token']を使い認証
  # userが存在すれば、認証成功
  # コントローラーでenv["warden"].authenticate!の形で利用する
  def authenticate!
    user = User.find_by_authentication_token(params['authentication_token'])

    if user
      # 認証成功
      # ここで渡したuserは、コントローラーからenv['warden'].userで取り出せる
      success!(user)
    else
      # 認証失敗
      # ここで渡したメッセージは、コントローラーからenv['warden'].messageで取り出せる
      fail!('strategies.authentication_token.failed')
    end
  end
end

4. Strategyをwardenに追加する

# config/initializers/warden.rb

require Rails.root.join('lib/strategies/authentication_token_strategy')

# `AuthenticationTokenStrategy`を`:authentication_token`で参照できるようになる
Warden::Strategies.add(:authentication_token, AuthenticationTokenStrategy)

5. wardenをRackミドルウェアスタックに追加

  • session(cookie)ミドルウェアの後に追加すべき。ここではflashの後に追加しておく。
# config/application.rb
config.middleware.insert_after ActionDispatch::Flash, Warden::Manager do |manager|
  # 先程定義したstrategyをデフォルトのstrategyとする
  manager.default_strategies :authentication_token
end

6. 認証情報をコントローラーから利用する

  • current_user等のメソッドをenv['warden']を使い定義する
  • authenticate!before_filterでリクエストのたびに実行されるようにする
# app/controllers/concerns/warden_helper.rb
module WardenHelper
  extend ActiveSupport::Concern

  included do
    # ビューでも使えるようにする
    helper_method :warden, :signed_in?, :current_user

    # before_filterで認証チェック
    prepend_before_filter :authenticate!
  end

  def signed_in?
    !current_user.nil?
  end

  def current_user
    warden.user
  end

  def warden
    request.env['warden']
  end

  def authenticate!
    warden.authenticate!
  end
end
# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
  include WardenHelper
end

strategy

strategyとは?

  • strategyは認証の実装。これらを切り替えることで、認証方法を切り替えられる(Strategyパターン)

Warden::Strategies::Baseとは?

  • 各strategyの親クラス
  • これのサブクラスを定義することで、独自のstrategyを定義できる

使い方1. Warden::Strategies::Baseを継承する場合

  • authentiate!は必須
  • valid?はなくてもOK。ない場合はtrueで、必ず実行される
# lib/strategies/authentication_token_strategy.rb
class AuthenticationTokenStrategy < ::Warden::Strategies::Base
  # ガード
  # params['authentication_token']がある場合のみ、authenticate!を実行する
  def valid?
    params['authentication_token']
  end

  # params['authentication_token']を使い認証
  # userが存在すれば、認証成功
  # コントローラーでenv["warden"].authenticate!の形で利用する
  def authenticate!
    user = User.find_by_authentication_token(params['authentication_token'])

    if user
      # 認証成功
      # ここで渡したuserは、コントローラーからenv['warden'].userで取り出せる
      success!(user)
    else
      # 認証失敗
      # ここで渡したメッセージは、コントローラーからenv['warden'].messageで取り出せる
      fail!('strategies.authentication_token.failed')
    end
  end
end

# 追加
Warden::Strategies.add(:authentication_token, AuthenticationTokenStrategy)

使い方2. 定義 + 追加

  • addする際に、同時に定義することも可能
  • 内部でWarden::Strategies::Baseのサブクラスを定義してる
Warden::Strategies.add(:authentication_token) do
  def valid?
    ...
  end

  def authenticate!
    ...
  end
end

使い方3. 復数strategyを使う場合

  • sample1のトークン認証strategyに加えて、Basic認証strategyも使う
# lib/strategies/basic_auth_strategy.rb
class BasicAuthStrategy < ::Warden::Strategies::Base
  def auth
    @auth ||= Rack::Auth::Basic::Request.new(env)
  end

  def valid?
    auth.provided? && auth.basic? && auth.credentials
  end

  def authenticate!
    user = User.find_by_username(auth.credentials[0])
    if user && user.authenticate(auth.credentials[1])
      success!(user)
    else
      fail!('strategies.basic_auth.failed')
    end
  end
end
  • WardenのStrategyに追加
# config/initializers/warden.rb
require Rails.root.join('lib/strategies/authentication_token_strategy')
require Rails.root.join('lib/strategies/basic_auth_strategy')

Warden::Strategies.add(:authentication_token, AuthenticationTokenStrategy)
Warden::Strategies.add(:basic_auth, BasicAuthStrategy)
  • デフォルトのstrategyに指定。デフォルトstrategyは復数指定可能
# config/application.rb
config.middleware.insert_after ActionDispatch::Flash, Warden::Manager do |manager|
  manager.default_strategies :authentication_token, :basic_auth
end
  • 順番に実行されて、halt!されるまでcascadeされる
env['warden'].authenticate

scope

scopeとは?

  • :user以外にも、:adminのようなユーザーを作ることができる。それぞれにstrategyを適用することができる
  • デフォルトのscopeは:default。scope指定がない場合は常に:default
  • deviseでも使われてる。current_userとかをcurrent_adminとして扱えるやつ

使い方

scopeを設定する

use Warden::Manager do |config|
  # default_stragetiesの代わりに、default_scopeを指定する
  config.default_scope = :user

  # 各scopeに、strategiesを指定する
  config.scope_defaults :user,        :strategies => [:password]
  config.scope_defaults :account,     :store => false,  :strategies => [:account_by_subdomain]
  config.scope_defaults :membership,  :store => false,  :strategies => [:membership, :account_owner]
  config.scope_defaults :api,         :store => false  ,:strategies => [:api_token], :action => "unauthenticated_api"
end

scopeを使用する

# 認証
env['warden'].authenticate(:scope => :admin)

# 認証済みか?
env['warden'].authenticated?(:admin)

# userへのアクセス
env['warden'].user(:admin)

# ログアウト
env['warden'].logout(:admin)

callback

callbackとは?

  • env['warden'].user等にcallbackを仕掛けられる

使い方

  • callbackは宣言順に実行される
Warden::Manager.after_set_user do |user, warden, opts|
  unless user.active?
    warden.logout
    throw(:warden, :message => "User not active")
  end
end

種類

Warden::Manager.after_set_user: userをset後

基本

  • trigger
    • env['warden'].userを初めて呼ぶ時
    • env['warden'].authenticate
    • env['warden'].set_uesr(user)
Warden::Manager.after_set_user do |user, warden, opts|
  unless user.active?
    warden.logout
    throw(:warden, :message => "User not active")
  end
end

only: 指定のtriggerだけ実行

# triggerは以下の3つ
env['warden'].user           # fetch
env['warden'].authenticate   # authentication
env['warden'].set_uesr(user) # set_user

# fetch時だけ実行
Warden::Manager.after_set_user only: :fetch

except: 指定のtriggerでは実行しない

Warden::Manager.after_authentication: 認証後

  • Warden::Manager.after_set_user only: :authentication のalias
Warden::Manager.after_authentication do |user,warden,opts|
  user.last_login = Time.now
end

Warden::Manager.after_fetch: user取得後

  • Warden::Manager.after_set_user only: :fetchのalias

Warden::Manager.before_failure: failure_app呼び出し前

Warden::Manager.before_failure do |env, opts|
  request = Rack::Request.new(env)
  env['SCRIPT_INFO'] =~ /\/(.*)/
  request.params[:action] = $1
end

Warden::Manager.after_failed_fetch: ?

  • trigger
    • env['warden'].user

Warden::Manager.before_logout: ログアウト前

  • trigger
    • env['warden'].logout
Warden::Manager.before_logout do |user,warden,opts|
  user.forget_me!
  warden.response.delete_cookie "remember_token"
end

Warden::Manager.on_request: リクエスト毎(wardenインスタンス生成時)

Warden::Manager.on_request do |warden|
  # do_something
end

Warden::Manager.prepend_xxxxx: prepend

  • prependしたい場合は、prepend_xxxxとする
  • prepend_before_failureとか

failure_app

  • 認証失敗時の処理をRackアプリとして実装できる
  • failure_appとしてセットされたRackアプリは、user.authenticate! で全ての認証が失敗した時に起動される

使い方1. lambdaを使う場合

  • RackアプリならなんでもOK。lambdaでもOK
config.middleware.use Warden::Manager do |manager|
  manager.default_strategies :authentication_token
  manager.failure_app = ->(env) { ['401', {'Content-Type' => 'application/json'}, { error: 'Unauthorized', code: 401 }] }
end

使い方2. クラスを使う場合

# app/controllers/unauthorized_controller.rb
class FailureApp
  def call(env)
    # `fail!`のメッセージを取得できる
    error_message = env["warden"].message

    status = 401
    headers = { "Content-Type" => "application/json"}
    body = [error_message]

    [stauts, headers, body]
  end
end
# config/application.rb
config.middleware.insert_after ActionDispatch::Flash, Warden::Manager do |manager|
  manager.default_strategies :authentication_token
  manager.failure_app = FailureApp
end

使い方3. Railsのコントローラーを使う場合

# config/initializers/warden.rb
Rails.application.config.middleware.use Warden::Manager do |manager|
  manager.default_strategies :authentication_token

  # アクションを直接指定してもよいが開発環境でキャッシュされてしまうようなので、lambdaにしておくのが良さそう
  manager.failure_app = ->(env) { SessionsController.action(:new).call(env) }
end

# app/controllers/sessions_controller.rb
def new
  flash.now.alert = warden.message if warden.message.present?
end

userをsessionに保存する

  • userインスタンスをそのままsessionに突っ込むのではなく、user.idだけ突っ込む
# /config/initializers/warden.rb
# sessionにはuser.idを保存する
Warden::Manager.serialize_into_session do |user|
  user.id
end

# sessionからuser.idを取り出して、userを取得する
Warden::Manager.serialize_from_session do |id|
  User.find(id)
end

未ログイン時のルーティングを指定する

  • constraintとwardenを利用することで、未ログイン時のルーティングを指定できる
# config/routes.rb
# 未ログインの場合は、このルーティングが適用される
scope constraints: ->(request) { request.env['warden'].user.nil? } do
  get "signup", to: "users#new", as: "signup"
  get "login", to: "sessions#new", as: "login"
end

resources :users
resources :sessions

テスト

ヘルパーを追加

  • login_aslogoutを追加してくれる
include Warden::Test::Helpers

login_as: ログイン

# "A User"として、ログインする
login_as "A User"

# "An Admin"として、ログインする
login_as "An Admin", :scope => :admin

# 両方で、ログインする
login_as "A User"

ログアウト

# ログアウトする
logout

# adminとして、ログアウトする
logout :admin

使いそうなメソッドまとめ

Warden::Strategies::Base

request: Rack::Requestオブジェクト

session: sessionオブジェクト

params: パラメータオブジェクト

env: Rackのenv

succes!: 認証成功 + halt!

  • halt!でstrategyのcascadingを止める
# このuserはenv['warden'].userでアプリから取り出せる
success! user

fail: 認証失敗 + halt!

  • halt!でstrategyのcascadingを止める
  • !なしだとhalt!しないっぽい。次のstrategyに移り実行する
# このメッセージはenv['warden'].messageでアプリから取り出せる
fail "Invalid email or password"

redirect!: リダイレクト + halt!

custom!: カスタムのRackアプリ配列 + halt!

halt!: strategyのcascadingを止める

  • 後続のstrategyは実行されない

pass: このstrategyを飛ばす

Warden::Manager

manager.default_strategies: デフォルトのstrategy

# passwordとbasicの2つのstrategyをデフォルトとしてセット
manager.default_strategies :password, :basicp

manager.failure_app=: failure_app設定

manager.failure_app = FailureApp

manager.default_scope: デフォルトのscope

use Warden::Manager do |manager|
  manager.failure_app = Public::SessionsController.action(:new)

  # default_stragetiesの代わりに、default_scopeを指定する
  manager.default_scope = :user

  # 各scope*4に、strategiesを指定する
  manager.scope_defaults :user,        :strategies => [:password]
  manager.scope_defaults :account,     :store => false,  :strategies => [:account_by_subdomain]
  manager.scope_defaults :membership,  :store => false,  :strategies => [:membership, :account_owner]
  manager.scope_defaults :api,         :store => false  ,:strategies => [:api_token], :action => "unauthenticated_api"

end

manager.scope_defaults: デフォルトのstrategy(scope)

Warden::Manager.serialize_into_session: sessionに保存する

Warden::Manager.serialize_into_session do |user|
  user.id
end

Warden::Manager.serialize_from_session: sessionから取り出す

Warden::Manager.serialize_from_session do |id|
  User.find(id)
end

Railsコントローラー

env['warden']: wardenを取得

env['warden'].user: 認証成功時にsetしたuser

env['warden'].user # デフォルトscope
env['warden'].user(:api) # scope指定

env['warden'].message: 認証失敗時にsetしたエラーメッセージ

env['warden'].authenticated?: 認証済みなら、true

env['warden'].authenticated? # デフォルトscope
env['warden'].authenticated?(:foo) # scope指定

env['warden'].authenticate!: 認証する

env['warden'].authenticate # 認証(失敗時に、処理を継続する)
env['warden'].authenticate! # 認証(失敗時に、例外を投げる)
env['warden'].authenticate(:password) # passwordストラテジーを利用する
env['warden'].authenticate(:password, :basic) # strategyは復数指定可能
env['warden'].authenticate!(:scope => :api) # scopeを指定する

env['warden'].valid?: ガード

env['warden'].set_user(@user): userを自分でセットする

env['warden'].set_user(@user) # デフォルトscope
env['warden'].set_user(@user, :scope => :admin) # scope指定
env['warden'].set_user(@user, :store => false) # 今回のリクエストだけで、sessionには保存しない

env['warden'].logout: ログアウト

env['warden'].logout # デフォルトscope
env['warden'].logout(:sudo) # scope指定

ざっくりコードリーディング

  • v1.2.8
  • 難しい...あんまりわからんかった

warden.gemspec

  • 依存gemはrackのみ

lib/warden/proxy.rb

  • env['warden']Warden::Proxy.new
  • env['warden']のメソッド群はここに定義されてる

lib/warden/strategies

  • Warden::Strategies.addが定義されてる

lib/warden/strategies/base.rb

  • Warden::Strageties::Baseは全てのStrategyの親クラス
  • Warden::Strategies.addを使えば、WArden::Strageties::Baseを継承した自前のstrategyを簡単に定義できる
  • succsess!等のメソッドはここに定義されてる

参考URL

RequestStoreの使い方 まとめ

  • 自分用のメモを公開したものです。ドキュメント/ソースコード等をまとめただけで試していないコードも多いので、信頼性は低いです。

request_storeとは?

  • リクエスト毎にグローバルな変数を使える
  • ☆975

導入

1. インストール

gem 'request_store'
$ bundle

2. 使ってみる

# ApplicationController
before_action :set_current_user
def set_current_user
  RequestStore.store[:current_user] = current_user
end

# モデル
RequestStore.store[:current_user] #=> current_user

類似機能との比較

Thread.current

  • スレッドローカル変数。つまりスレッド単位でグローバルな変数。(ドキュメントにはnot thread-local but fiber-localと書いてあったが違いがわからんかった。fiber使わんし難しい...)
  • request_storeは内部でコレ使ってる
# Thread.current: 現在のスレッド
# Thread.current[:foo] 現在のスレッドにおいて、グローバルな変数
Thread.current[:foo] = 0
Thread.current[:foo] #=> 0

問題点

  • アプリサーバはリクエスト1回分でスレッドが終了せず、次のリクエストも同じスレッドで処理する。その際に値がリセットされていないので、前回の値を持ち越してしまう
# 毎回0にリセットされず、1,2,3,...と増えていく
def index
  Thread.current[:counter] ||= 0
  Thread.current[:counter] += 1

  render :text => Thread.current[:counter]
end

Webrickでは問題ないらしい

ActiveSupport::CurrentAttributes

  • リクエスト毎にリセットされるスレッドローカルな属性を定義できる
  • こちらも内部でThread.currentを利用してる
  • ユースケースとしてはrequest_id等を想定してるっぽい
  • Rails5.2で追加
  • 参考: https://github.com/rails/rails/pull/29180

使い方

  • たぶんこんな感じ?自信なし
# app/models/current.rb
class Current < ActiveSupport::CurrentAttributes
  # 属性定義。この属性はリクエスト事にリセットされる
  attribute :user
end


# before_actionで`Current.user`をセット。これでどこからでも`Current.user`でcurrent_userにアクセスできるようになる
class ApplicationController < ActionController::Base
  before_action :set_current_user

  def set_current_user
    Current.user = currrent_user
  end
end

# モデル
Current.user #=> currrent_user

使う際の注意点

グルーバルにアクセスできちゃう

  • グルーバルにアクセスできちゃうので、
    • MVCが壊れてカオスになる
    • テストが難しくなる
  • コレが必要になる場合は設計が間違っている可能性があるので、まずは設計を見直す
  • CurrentAttributesの記事だけど、同じことが当てはまりそう: https://techracho.bpsinc.jp/hachi8833/2017_08_01/43810

リクエスト毎にマルチスレッド使うのはNG

テスト

  • ミドルウェアを追加する必要あり
# spec_helper.rb
def app
  Rack::Builder.new do
    use RequestStore::Middleware
    run MyApp
  end
end

Rails以外で使う

  • ミドルウェアを追加すればOK
use RequestStore::Middleware

ざっくりコードリーディング

  • コードベースはかなり小さい

request_store.gemspec

  • 依存gem
    • rack: Rackミドルウェアを使うため

lib/request_store.rb

  • RequestStore
  • Thread.current[:request_store]を便利に扱えるようにしたラッパーのようなクラス
module RequestStore
  # `RequestStore.store[:foo]`は`Thread.current[:request_store][:foo]`に相当
  def self.store
    Thread.current[:request_store] ||= {}
  end

  ...
end

lib/request_store/middleware.rb

  • RequestStore::Middleware
  • レスポンス時にThread.current[:request_store]をクリアするRackミドルウェア
    # callはRackミドルウェアの規約
    def call(env)
      # リクエストの処理
      # フラグ立てる
      RequestStore.begin!

      # Rackアプリ本体の処理
      response = @app.call(env)

      # リクエスト時の処理
      # フラグ折る + データをクリア
      returned = response << Rack::BodyProxy.new(response.pop) do
        RequestStore.end!
        RequestStore.clear!
      end
    ensure
      unless returned
        RequestStore.end!
        RequestStore.clear!
      end
    end

lib/request_store/railtie.rb

  • ミドルウェアを追加したり

参考URL

Gonの使い方 まとめ

  • 自分用のメモを公開したものです。ドキュメント/ソースコード等をまとめただけで試していないコードも多いので、信頼性は低いです。

Gonとは

  • Railsの変数をJSで使えるようになる

導入

1. インストール

# Gemfile
gem 'gon'
$ bundle

2. セットアップ

# application.html.erb

<head>
  ...
  # application.jsの前に読み込めばロード前にgon変数にアクセスできる
  #
  # 以下のようなHTMLを生成してくれる
  # <script>
  # //<![CDATA[
  # window.gon={};gon.hoge=1;
  # //]]>
  # </script>
  <%= Gon::Base.render_data %>
  <%= javascript_include_tag "application" %>
  ...
</head>

3. 使ってみる

class ProductsController < ApplicationController
  def index
    # Rails側で変数をセット
    gon.hoge = 1

    ...
  end
end
# coffee

# JS側で使用できる
# 型キャストやエスケープもいい感じにやってくれる
gon.hoge // => 1

設定

使い方

  • Gon::Base.render_dataのオプションとして指定する
Gon::Base.render_data(watch: true)

watch: watch機能を有効化する

  • watch機能 を参照
  • 実際にはfalseでもwatch機能は使える。明示的に宣言するだけ
  • デフォルト: false

camel_case: キャメルケースに変換する

  • デフォルト: false
# 設定
Gon::Base.render_data(camel_case: true)

# ruby
gon.int_cased = 1

# js
gon.intCased #=> 1

camel_depth: キャメルケースを適用する深さ

  • デフォルト: 1
# 設定
Gon::Base.render_data(:camel_case => true, :camel_depth => 2)

# js
gon.testHash.testDepthOne.test_depth_two

namespace: gonという名前空間を変える

  • デフォルト: 'gon'
# 設定
Gon::Base.render_data(:namespace => 'serverExports')

# js
serverExports.your_int

init: window.gon = {}で初期化する

  • デフォルト: true
# js
# 初期化されてるので、データがない場合でもエラーにならない
window.gon // => {}

type: <script>type="text/javascript"を追加する

  • false
Gon::Base.render_data(type: true) #=> <script type="text/javascript">window.gon=...</script>

nonce: <script>nonce=...を追加する

  • CSP対応
  • デフォルト: nil
Gon::Base.render_data(nonce: 'test') #=> "<script nonce=\"test">...

need_tag: <script>タグあり

  • デフォルト: true
Gon::Base.render_data(need_tag: true)
#=> <script>
#=> //<![CDATA[
#=> window.gon={};gon.hoge="piyo";
#=> //]]>
#=> </script>
Gon::Base.render_data(need_tag: false)
#=> window.gon={};gon.hoge="piyo";

cdata: CDATAあり

  • デフォルト: true

global_root: globalという名前空間を変える

  • デフォルト: 'global'

amd: AMD対応

  • デフォルト: false
  • include_gon_amdと同じ

<script>タグのrenderメソッド*3

Gon::Base.render_data

  • 基本コレを使えばok

include_gon

  • Rails3の場合はこっちらしい
  • 内部でGon::Base.render_dataを利用してる
<%= include_gon %>

include_gon_amd

  • AMDの場合はこっち
  • 内部でGon::Base.render_dataを利用してる
<%= include_gon_amd %>

メソッド

gon.hoge=: set

gon.hoge = 1

gon.push: set

gon.push(hoge: 1, piyo: 2)

gon.hoge: get

gon.hoge #=> 1

gon.all_variables: setした全ての値

gon.hoge = 1
gon.piyo = 2
gon.all_variables #=> {"hoge"=>1, "piyo"=>2}

gon.clear: setした値を消す

gon.hoge = 1
gon.piyo = 2
gon.all_variables #=> {"hoge"=>1, "piyo"=>2}
gon.clear
gon.all_variables #=> {}

watch機能

  • Ajaxでポーリングして、gonのデータをリアルタイムで取得する

導入

1. watchオプションをtrueにする

  • どうもwatchオプションは明示的にする意味しかなく、指定しなくてもOKっぽい
# app/views/layouts/application.html.erb
<%= include_gon(watch: true) %>

2. アプリ作成

  • コントローラーでgon.watchを利用するのがポイント
# app/controllers/home_controller.rb
# ここにAjaxリクエストが送られる。そのたびにusers_countの値がレスポンスして更新される
def index
  @users_count = User.count
  gon.watch.users_count = @users_count
end
# app/views/home/index.html.erb
# Ajax成功時に、コールバックでここの表示が変わる
<div id='users-counter'></div>

3. JS

  • gon.watch()は一定間隔でリクエストを送り、コントローラーでgonにセットした変数をコールバックで利用する
# app/assets/javascripts/home.js.coffee
# コールバック
#   uesrs_count: Ajaxのレスポンス値
renewUsers = (uesrs_count) ->
  $('#users-counter').text(users_count)

# gon.watchの使い方
# gon.watchを使うとポーリングできる
# ここでは1秒ごとに`/home`にAjaxリクエストを送って、`#users-counter`の表示を変更している
#
# gon.watch(name_of_variable, options, callback)
#   name_of_variable: 変数名(コントローラーで`gon.watch.users_count = @users_count`とすれば、'users_count'となる)
#   options
#     interval: Ajaxリクエストする間隔。ms
#     method: HTTPメソッド。デフォルトはGET
#     url: 陸ストを送るURL
#   callback: Ajaxのコールバック
gon.watch('users_count', interval: 1000, renewUsers)

止める

  • gon.unwatchで止める
# ビュー
<a href='#' id='stop-renewing'>
  Stop renewing
</a>
# coffee
$('#stop-renewing').click ->
  # 止める
  gon.unwatch('users_count', renewUsers)
  return false

グローバルに使う

  • Gon.globalを使う
# config/initializers/some_initializer.rb
Gon.global.variable = 1
# coffee
gon.global.variable // => 1

RSpecでコントローラーテスト

# spec/support/shared_contexts/gon.rb
shared_context :gon do

  # Gonは内部でRequestStore(リクエストグローバルな変数。gem)を利用している
  # RequestStoreはRackミドルウェアで変数を削除する
  # しかしテストではRackミドルウェアを経由しない
  # そのためテストケース毎にGonの変数を手動でクリアする必要がある
  # 参考: https://tech.misoca.jp/entry/2015/06/15/151419
  let(:gon) { RequestStore.store[:gon].gon }
  before { Gon.clear }
end

# spec/controllers/thingies_controller_spec.rb
RSpec.describe ThingiesController do
  include_context :gon

  describe 'GET #new' do
    it 'gonifies as expected' do
      get :new, {}, valid_session

      # Gonに変数がsetされていることをテスト
      expect(gon['key']).to eq :value
    end
  end
end

ざっくりコードリーディング

gon.gemspec

  • 依存gem
    • actionpack: Railsのcontroller/routing
    • request_store: リクエストグローバルな変数
    • multi_json: 主要なJSON Engineに対応したJSONパーサ

lib/gon.rb

  • メインとなるGonクラス
  • コントローラーのgonの正体はGon
  • gon.hoge = 1gon.hogeにはmethod_missingを利用している。

lib/gon/base.rb

  • Gon::Base.render_data

-Gon::Base.render_dataのオプションのデフォルト値

    VALID_OPTION_DEFAULTS = {
        namespace: 'gon',
        camel_case: false,
        camel_depth: 1,
        watch: false,
        need_tag: true,
        type: false,
        cdata: true,
        global_root: 'global',
        namespace_check: false,
        amd: false,
        nonce: nil
    }

lib/gon/global.rb

  • Gon::GlobalはGonのサブクラス
  • Gon::Globalはgon.globalに対応
  • Gonとの違いはあんまりないっぽい

lib/gon/watch.rb

  • Gon::WatchはGonのサブクラス
  • Gon::Watchはgon.watchに対応

coffee/watch.coffee

  • gon.watchgon.unwatchgon.unwatchAllが定義
  • jQuery依存っぽい

js/watch.js

  • coffee/watch.coffeeをコンパイルしたファイル

参考URL