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
公式さんがいくつもお手本となるアプリを用意してくれてるようです。写経してると、なんか自分でもできる気がしてきます。(気がするだけです)
- Vuexのexamples: Vuexを利用したアプリ5つ
- vuejs/vue-hackernews-2.0: vue-router + vuex + SSRのHackerNewsクローン
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
をいい感じに解釈して、@commentable
がreportオブジェクト
の場合は/reports/1/comments/1
というURLを作り、@commentable
がbookオブジェクト
の場合は/books/1/comments/1
というURLを作ってくれます。
このコード1つで2つのURLに対応してくれます。
ここでreport_comment_path(@report, @comment)
と書いてしまうと、後に分岐処理を書くことになり、ポリモーフィック関連の強みを活かせません。
そのため[@report, @comment]
という書き方を利用しています。
ちなみに[@report, @comment]
のような書き方は、redirect_to
だけでなくlink_to
やform_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
することで、book
とreport
を同じ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
ちなみにこちらの方法を使ってコントローラーを分けると、より綺麗な実装になります。
以上でポリモーフィック関連のコントローラーの完成です。
あとはBook
をReport
と同じように実装して、ビューを修正すれば課題クリアになります。
お付き合いいただき、ありがとうございました🙇
Wardenの使い方 まとめ
- Wardenとは?
- 導入
- strategy
- scope
- callback
- callbackとは?
- 使い方
- 種類
- Warden::Manager.after_set_user: userをset後
- Warden::Manager.after_authentication: 認証後
- Warden::Manager.after_fetch: user取得後
- Warden::Manager.before_failure: failure_app呼び出し前
- Warden::Manager.after_failed_fetch: ?
- Warden::Manager.before_logout: ログアウト前
- Warden::Manager.on_request: リクエスト毎(wardenインスタンス生成時)
- Warden::Manager.prepend_xxxxx: prepend
- failure_app
- userをsessionに保存する
- 未ログイン時のルーティングを指定する
- テスト
- 使いそうなメソッドまとめ
- ざっくりコードリーディング
- 参考URL
- 自分用のメモを公開したものです。ドキュメント/ソースコード等をまとめただけで試していないコードも多いので、信頼性は低いです。
Wardenとは?
- 認証のフレームワーク。実際の認証は自分で実装する
- 認証はRackミドルウェアで行う
- Wardenをアプリで直接使うことはなさそう。deviseのような認証ライブラリのためのライブラリと考えたほうが良さそう
- Deviseで使われてる。Wardenのstrategy/scope/callback等の知識がないとDeviseのコード読むのつらそう
導入
- Railsアプリ(APIモード)にwardenを使い認証機能を追加する
- deviseなし。生warden
- 参考
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_as
とlogout
を追加してくれる
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では問題ないらしい
- さっきのコードはWebrickとThinで動作が変わるらしい。Webrickの場合は毎回0にリセットされて、期待通りの動作になるらしい。
- https://github.com/steveklabnik/request_store
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
- 内部では
Thrad.current
使っていて、1リクエスト1スレッドを前提としてる。別スレッドを作ってもそっちのスレッドでは共有されない - request_store_rails(gem)を使えばOKっぽい
- https://stackoverflow.com/questions/4097510/does-rails-have-a-per-request-store-that-i-can-add-objects-to-and-retrieve-from
テスト
- ミドルウェアを追加する必要あり
# 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 = 1
やgon.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.watch
、gon.unwatch
、gon.unwatchAll
が定義- jQuery依存っぽい
js/watch.js
- coffee/watch.coffeeをコンパイルしたファイル
参考URL
Awesome Printの使い方 まとめ
- 自分用のメモを公開したものです。ドキュメント/ソースコード等をまとめただけで試していないコードも多いので、信頼性は低いです。
Awesome Printとは
- コンソールやログ等の出力が見やすくするgem。いい感じにインデントを追加してくれたり、カラーリングしてくれたりする。
導入
インストール
group :development do gem "awesome_print" end
$ bundle install
使ってみる
$ rails console
# 色付きで、いい感じのインデントで出力 ap Cat.first
メソッド
ap(object): オブジェクトを見やすく出力
ap Cat.first
object.ai: オブジェクトを見やすく加工
- apの戻り値版
使い方
obj.ai obj.ai(plain: false) # 色付けしない obj.ai(html: false) # htmlに加工しない
オプション
indent: 4, # Number of spaces for indenting.
index: true, # Display array indices.
html: false, # Use ANSI color codes rather than HTML.
multiline: true, # Display in multiple lines.
plain: false, # Use colors.
raw: false, # Do not recursively format instance variables.
sort_keys: false, # Do not sort hash keys.
sort_vars: true, # Sort instance variables.
limit: false, # Limit arrays & hashes. Accepts bool or int.
ruby19_syntax: false, # Use Ruby 1.9 hash syntax in output.
color: {
args: :pale,
array: :white,
bigdecimal: :blue,
class: :yellow,
date: :greenish,
falseclass: :red,
fixnum: :blue,
float: :blue,
hash: :pale,
keyword: :cyan,
method: :purpleish,
nilclass: :red,
rational: :blue,
string: :yellowish,
struct: :pale,
symbol: :cyanish,
time: :greenish,
trueclass: :green,
variable: :cyanish
}
string.red: 文字を赤色にする
使い方
- 文字列のみ
- purpleとか他の色もok
"cat is kawaii".red #=> "\e[1;31mcat is kawaii\e[0m" puts "cat is kawaii".red # -> cat is kawaii
colorの種類
- gray
- red
- green
- yellow
- blue
- purple
- cyan
- white
- black
- redish
- greenish
- yellowish
- blueish
- purpleish
- cyanish
- pale
logger.ap(message): Awesome Printを使い、ログ吐き
使い方
- LoggerとActiveSupport::BufferedLoggerにapメソッドを加える
- そのためRailsデフォルトloggerでこんな感じで利用できる
- デフォルトは:debugレベル。変更可能
logger.ap @users # 出力 [ [0] #<User:0x00007fcce9d38d18> { :id => 1, :first_name => nil, :last_name => nil, :email => nil, :created_at => Fri, 31 Aug 2018 07:16:07 UTC +00:00, :updated_at => Fri, 31 Aug 2018 07:16:07 UTC +00:00 } ]
ログレベル指定(全体)
AwesomePrint.defaults[:log_level]
ログレベル指定(個別)
logger.ap @users, :warn
ap_debug(object): Awesome Printを使い、debug
- ActionView::Baseにapメソッドを追加するので、ビューでapが利用できる
- 色付けして、
<pre debug_dump>
で囲む <pre>
タグで囲むので、<%== %>
でHTMLエスケープしないようにする- debugに近い(こっちはyaml)
<%== ap @users %> <%== ap_debug @users %> <%== debug @users %>
オプション
グローバル
# ~/.apr AwesomePrint.defaults = { :indent => -2, :color => { :hash => :pale, :class => :white } }
個別
obj.ai(plain: false) # 色付けしない
REPLとの統合
irb
# ~/.irbrc require "awesome_print" AwesomePrint.irb!
pry
# ~/.pryrc require "awesome_print" AwesomePrint.pry!
# 通常時 pry(main)> [1, 2] => [1, 2]
# AwesomePrint.pry!時 pry(main)> [1, 2] [ [0] 1, [1] 2 ]
参考リンク
Kaminariの使い方 まとめ
- 導入
- scopeメソッド
- page: nページ目のレコードを取得
- limit_value: 取得するレコード数
- total_pages: 総ページ数
- current_page: 現在のページ番号
- next_page: 次のページ番号
- prev_page: 前のページ番号
- first_page?: 1ページ目なら、true
- last_page?: 最終ページなら、true
- out_of_range?: ページが範囲外なら、true
- per: ページ毎のレコード数を設定
- padding: 最初のn件は取得しない
- total_count: 全体のレコード数
- without_count: COUNTクエリを発行しない
- except: page/per取り消し
- ビューヘルパーメソッド
- paginate: ページネーションのテンプレートをrender
- link_to_next_page: 次ページへのリンクをrender
- link_to_previous_page: 前ページへのリンクをrender
- link_to_prev_page: alias
- page_entries_info: ページ情報をrender
- rel_next_prev_link_tags:
- path_to_next_page: 次ページのpath
- path_to_prev_page: 前ページのpath
- next_page_url: 次ページのURL
- prev_page_url: 前ページのURL
- ヘルパーメソッドをコントローラーで使う
- 設定
- I18n
- テンプレートを変更する
- 配列をページネートする
- テーマ
- コンポーネント
- その他メモ
- ざっくりコードリーディング
- lib/
- kaminari-core/
- コア機能
- kaminari-core/lib/kaminari/
- kaminari-core/lib/kaminari/models/array_extension.rb
- kaminari-core/lib/kaminari/models/configuration_methods.rb
- kaminari-core/lib/kaminari/models/page_scope_methods.rb
- kaminari-core/lib/kaminari/helpers/helper_methods.rb
- kaminari-core/lib/kaminari/helpers/tags.rb
- kaminari-core/lib/kaminari/helpers/paginator.rb
- パーシャル
- kaminari-core/app/views/kaminari/
- kaminari-core/app/views/kaminari/_first_page.html.erb
- kaminari-core/app/views/kaminari/_last_page.html.erb
- kaminari-core/app/views/kaminari/_next_page.html.erb
- kaminari-core/app/views/kaminari/_prev_page.html.erb
- kaminari-core/app/views/kaminari/_page.html.erb
- kaminari-core/app/views/kaminari/_gap.html.erb
- kaminari-core/app/views/kaminari/_paginator.html.erb
- kaminari-core/config/locales/kaminari.yml
- ジェネレータ
- コア機能
- kaminari-actionview/
- kaminari-activerecord/
- 参考URL
- 自分用のメモを公開したものです。ドキュメント/ソースコード等をまとめただけで試していないコードも多いので、信頼性は低いです。
- 正確な情報はドキュメントを参照してください。ドキュメントのできが良すぎてほとんどコピペみたいになってしまったので、ドキュメントを見たほうが良いです。 -> https://github.com/kaminari/kaminari
導入
インストール
# Gemfile gem 'kaminari'
$ bundle install
使ってみる
コントローラー
- ページ番号は
params[:page]
に格納される。これをpage
メソッドに食わせる
# app/controllers/items_controller.rb def index @items = Item.page(params[:page]) end
ビュー
paginate
メソッドでページネーションのテンプレートをrenderする
# app/views/items/index.html.erb <%= paginate @items %>
scopeメソッド
page: nページ目のレコードを取得
- pageは1から始まるので注意。
page(0)
はpage(1)
と同じ結果になる - デフォルトでは1ページ25レコード
- 内部的にはActiveRecordのlimitとoffsetを利用している
User.page(1) # 1ページ目の分のレコード(LIMIT 25 OFFSET 0) User.page(2) # 2ページ目の分のレコード(LIMIT 25 OFFSET 25)
- 実際にはコントローラーに
params[:page]
が渡ってくるので、それを食わせる
User.page(params[:page])
- pageは内部的にlimitを利用しているので、両方使うと上書きする
User.limit(10).count #=> 10 User.limit(10).page(1).count #=> 25
limit_value: 取得するレコード数
User.page(1).limit_value #=> 25 User.page(1).per(10).limit_value #=> 10
total_pages: 総ページ数
User.page(1).total_pages #=> 50 User.page(1).per(50).total_pages #=> 25
current_page: 現在のページ番号
User.page(1).current_page #=> 1
next_page: 次のページ番号
User.page(1).next_page #=> 2
prev_page: 前のページ番号
User.page(2).prev_page #=> 1
first_page?: 1ページ目なら、true
User.page(1).first_page? #=> true
last_page?: 最終ページなら、true
User.page(50).last_page? #=> true
out_of_range?: ページが範囲外なら、true
User.page(100).out_of_range? #=> true
per: ページ毎のレコード数を設定
- デフォルト: 25
User.page(7).per(50) # モデルに対しては直接使えない(page指定なしに使うことはありえないから) User.per(50) # エラー
padding: 最初のn件は取得しない
- OFFSETを使用して最初のn件は取得しない
- モデルに対しては直接使えない
User.page(1).padding(3) # SELECT `users`.* FROM `users` LIMIT 25 OFFSET 3
total_count: 全体のレコード数
User.count #=> 1000 User.page(1).count #=> 25 User.page(1).total_count #=> 1000
without_count: COUNTクエリを発行しない
- 参考: https://qiita.com/yuki24/items/aab0d8e417d6fe546688
- 一般的に、ページネーションではリンクを表示するために総レコード数を知らないといけない。そのためクエリを発行する必要がある
- しかし、リンクが"next"と"prev"だけでいいなら総レコード数を知る必要はない。この場合
without_count
を使うとCOUNTクエリを発行しない - 次のページが存在するか確かめるために
limit + 1
する(デフォルトならLIMIT 26
) - レコード数が大量にある場合に役立つ
# コントローラー User.page(3).without_count
- ビューでは
paginate
を使う代わりに、link_to_prev_page
とlink_to_next_page
を使い自分でリンクを用意する
# ビュー <%#= paginate @users %> <%= link_to_prev_page @users, 'Previous Page' %> <%= link_to_next_page @users, 'Next Page' %>
except: page/per取り消し
page
とper
は内部的にはActiveRecordのlimitとoffsetを利用している。なのでpageとperを取り消したくなったら、except(:limit, :offset)
を使えばOKexcept
はActiveRecordのメソッド
User.page(3).per(10) # SELECT `users`.* FROM `users` LIMIT 10 OFFSET 20 User.page(3).per(10).except(:limit, :offset) # SELECT `users`.* FROM `users`
ビューヘルパーメソッド
paginate: ページネーションのテンプレートをrender
使い方
# « First ‹ Prev ... 2 3 4 5 6 7 8 9 10 ... Next › Last » <%= paginate @users %>
オプション
window: リンク数(内側)
- デフォルト: 4
# ... 5 6 7 8 9 ... <%= paginate @users, window: 2 %>
outer_window: リンク数(外側)
- デフォルト: 0
# 1 2 3 ...(snip)... 18 19 20 <%= paginate @users, outer_window: 3 %>
left: リンク数(外側 - 左)
- デフォルト: 0
# 1 ...(snip)... 18 19 20 <%= paginate @users, left: 1, right: 3 %>
right: リンク数(外側 - 右)
- デフォルト: 0
# 1 ...(snip)... 18 19 20 <%= paginate @users, left: 1, right: 3 %>
param_name: パラメータ名
params[:page]
の:page- デフォルト: :page
# コントローラーでは`params[:page]`の代わりに`params[:pagina]`を使う <%= paginate @users, param_name: :pagina %>
params: パラメータ操作
# 上書き <%= paginate @users, params: {controller: 'foo', action: 'bar'} %> # merge # コントローラーでは`params[:hoge] #=> "piyo"` <%= paginate @users, params: {hoge: "piyo"} %>
remote: リンクがAjaxになる
- 全てのリンクに
data-remote="true"
を追加
# これだけでリンクがAjaxになる <%= paginate @users, remote: true %>
views_prefix: ViewのDirectory
- デフォルト:
kaminari/
# app/views/templates/kaminariのpartialを探す <%= paginate @users, views_prefix: 'templates' %>
theme: テーマ指定
<%= paginate @users, theme: 'my_custom_theme' %>
link_to_next_page: 次ページへのリンクをrender
# 基本 <%= link_to_next_page @items, 'Next Page' %> # Ajax <%= link_to_previous_page @items, 'Previous Page', remote: true %> # ブロックで、1ページ目の処理を指定できる <%= link_to_previous_page @users, 'Previous Page' do %> <span>At the Beginning</span> <% end %>
link_to_previous_page: 前ページへのリンクをrender
link_to_prev_page: alias
page_entries_info: ページ情報をrender
使い方
# "Displaying posts 6 - 10 of 26 in total"のような表示 <%= page_entries_info @posts %>
オプション
entry_name: 表示名を変更
# "Displaying items 6 - 10 of 26 in total"のような表示 <%= page_entries_info @posts, entry_name: 'item' %>
rel_next_prev_link_tags:
# <link rel="next" href="/users?page=5"> # <link rel="prev" href="/users?page=3"> <%= rel_next_prev_link_tags @users %>
- 実際にはタグ内で使う
<head> <title>My Website</title> # <link rel="next" href="/users?page=5"> # <link rel="prev" href="/users?page=3"> <%= yield :head %> </head> <% content_for :head do %> <%= rel_next_prev_link_tags @items %> <% end %>
path_to_next_page: 次ページのpath
# /users?page=5 <%= path_to_next_page @users %>
path_to_prev_page: 前ページのpath
# /users?page=3 <%= path_to_prev_page @users %>
next_page_url: 次ページのURL
# http://www.example.org/items?page=2 <%= next_page_url @items %>
prev_page_url: 前ページのURL
# http://www.example.org/items <%= prev_page_url @items %>
ヘルパーメソッドをコントローラーで使う
Kaminari::Helpers::UrlHelper
モジュールにヘルパーメソッドが定義されているので、コントローラーでinclude
すればOK
class UsersController < ApplicationController include Kaminari::Helpers::UrlHelper def index @users = User.page(1) path_to_next_page(@items) #=> /items?page=2 end end
設定
- kaminariのデフォルト動作の変更は、ざっくり3箇所で指定可能。同一内容の設定の場合、下が優先させる
- グローバル設定
- モデル単位の設定
- 個別の設定
グローバル設定
設定ファイルを作る
$ rails g kaminari:config create config/initializers/kaminari_config.rb
# config/initializers/kaminari_config.rb # frozen_string_literal: true Kaminari.configure do |config| # config.default_per_page = 25 # config.max_per_page = nil # config.window = 4 # config.outer_window = 0 # config.left = 0 # config.right = 0 # config.page_method_name = :page # config.param_name = :page # config.params_on_first_page = false end
default_per_page: ページ毎のレコード数
- デフォルト: 25
max_per_page: ページ毎のレコード数の最大値
- デフォルト: nil
max_pages: ページ数の最大値
- デフォルト: nil
window: リンク数(内側)
- デフォルト: 4
outer_window: リンク数(外側)
- デフォルト: 0
left: リンク数(外側 - 左)
- デフォルト: 0
right: リンク数(外側 - 右)
- デフォルト: 0
page_method_name: pageメソッド(nページ目のレコードを取得)の名前変更
- デフォルト: :page
param_name: パラメータ名
params[:page]
の:page- デフォルト: :page
params_on_first_page: 最初のページでparamsを無視しない
- デフォルト: false
- デフォルト設定だと、最初のページでparamsを無視する。paramsを利用してフィルター機能などを実装していた場合に、それも消えてしまい問題になるので、その対策。(参考: https://qiita.com/yuki24/items/aab0d8e417d6fe546688#params_on_first_page-%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E8%BF%BD%E5%8A%A0)
モデル単位の設定
paginates_per: ページ毎のレコード数
class Article < ActiveRecord::Base paginates_per 10 end
max_paginates_per: ページ毎のレコード数の最大値
per
スコープでこの数値以上を指定した場合、この数値が利用される- デフォルト: nil(max制限はなし)
class User < ActiveRecord::Base max_paginates_per 50 end
max_pages: ページ数の最大値
- ここで指定した以上のページがあっても、リンクを作らない
class Article < ActiveRecord::Base max_pages 100 end
個別の設定
- ページ毎のレコード数指定には
per
を使う
User.page(7).per(50)
- 見た目の変更には
patinate
のオプションを指定する
# ... 5 6 7 8 9 ... <%= paginate @users, window: 2 %> # 1 2 3 ...(snip)... 18 19 20 <%= paginate @users, outer_window: 3 %>
I18n
ラベルはI18nに対応してる
- ページネーションのラベルを変更したい場合は、ロケールファイルを変更すればOK
- 以下がデフォルトのロケールファイル(参考: https://github.com/kaminari/kaminari/blob/master/kaminari-core/config/locales/kaminari.yml)
en: views: pagination: first: "« First" last: "Last »" previous: "‹ Prev" next: "Next ›" truncate: "…" helpers: page_entries_info: entry: zero: "entries" one: "entry" other: "entries" one_page: display_entries: zero: "No %{entry_name} found" one: "Displaying <b>1</b> %{entry_name}" other: "Displaying <b>all %{count}</b> %{entry_name}" more_pages: display_entries: "Displaying %{entry_name} <b>%{first} - %{last}</b> of <b>%{total}</b> in total"
ラベルを変更する
- 変更したい場合は、ロケールファイルを用意して変更したい部分を変えればOK
# config/locales/kaminari.en.yml en: views: pagination: previous: "<-"
ラベルに日本語を使う
1. デフォルトのロケールを日本語にする
# /config/application.rb config.i18n.default_locale = :ja
2. 日本語のロケールファイルを用意する
- 参考
# config/locales/kaminari.ja.yml ja: helpers: page_entries_info: more_pages: display_entries: "<b>%{total}</b>中の%{entry_name}を表示しています <b>%{first} - %{last}</b>" one_page: display_entries: one: "<b>%{count}</b>レコード表示中です %{entry_name}" other: "<b>%{count}</b>レコード表示中です %{entry_name}" zero: "レコードが見つかりませんでした %{entry_name}" views: pagination: first: "« 最初" last: "最後 »" next: "次 ›" previous: "‹ 前" truncate: "…"
kaminari-i18n(gem)
- https://github.com/tigrish/
- 各言語に対応したロケールファイルがある
テンプレートを変更する
1. パーシャルを作成する
# defaultテーマを利用する
$ rails g kaminari:views default
- 以下の7つのパーシャルが作成される
- app/views/kaminari/_first_page.html.erb: << First
- app/views/kaminari/_last_page.html.erb: Last >>
- app/views/kaminari/_next_page.html.erb: Next >
- app/views/kaminari/_prev_page.html.erb: < Prev
- app/views/kaminari/_page.html.erb: 4(各ページへのリンク)
- app/views/kaminari/_gap.html.erb: ...(ページ間の省略)
- app/views/kaminari/_paginator.html.erb: ページネーションHTML全体。各partialはここで使われる
2. haml/slimに変換する
- erbではなくhaml/slimを扱いたい場合は
html2haml
やhtml2slim
を使って、自分で変換する。 - 昔は
-e haml
オプションでhamlを生成できたが、現在はdeprecated
3. パーシャルを変更する
- 作成されたパーシャルはkaminari内部で使用されているものと同じ。しかしこちらのほうが優先されるので、こちらを自分で変更することで、テンプレートを変更できる。
配列をページネートする
- Relationだけでなく、配列もページネート可能
@paginatable_array = Kaminari.paginate_array(my_array_object).page(params[:page]).per(10)
total_countを指定
- 実際のcountとは異なった値を返すような、配列っぽいオブジェクトに対して使う(ドキュメントではRSolr)
@paginatable_array = Kaminari.paginate_array([], total_count: 145).page(params[:page]).per(10) @paginatable_array.total_count #=> 145 @paginatable_array.count #=> 0
テーマ
- テーマを変更することで、ページネーション部分のHTMLを変更できる
- 各CSSフレームワークに対応したテーマが多いっぽい
- デフォルトのテーマは
default
使い方
# defaultテーマを利用 $ rails g kaminari:views default # bootstrap4用のテーマを利用 $ rails g kaminari:views bootstrap4
テーマ一覧
- default
- bootstrap2
- bootstrap3
- bootstrap4
- bourbon: Bourbon
- bulma
- foundation
- foundation5
- github
- materialize
- purecss
- semantic
kaminari_themes(gem)
- https://github.com/amatsuda/kaminari_themes
- テーマはこのリポジトリで管理されている
$ rails g kaminari:views bootstrap4
時にはこのリポジトリから取ってくる
復数のテーマを使う
1. カスタムテーマ用のディレクトリを用意する
# 1. defaultでテンプレート作成 % rails g kaminari:views default # 2. cd % cd app/views/kaminari # 3. カスタムテーマ用のディレクトリを作る % mkdir my_custom_theme # 4. テンプレートをすべてコピー % cp _*.html.* my_custom_theme/
2. paginateでカスタムテーマ用のディレクトリを参照する
<%= paginate @users, theme: 'my_custom_theme' %>
コンポーネント
- kaminariはコンポーネントベースで構成されている
- 主要コンポーネントは以下の3つ。これらは単独のgemだがkaminariにバンドルされてる
- kaminari-core: コアロジック
- kaminari-activerecord: Active Recordアダプタ
- kaminari-actionview: Action Viewアダプタ
- コンポーネントは他にも色々ある。コンポーネントベースな設計のおかげで、自分の環境にあったコンポーネントを選んで使うことができる。
使い方
# ActiveRecord + Rails(ActionView)の場合 # 普通にkaminariを使う場合はこうなる gem 'kaminari' # ActiveRecord + Rails(ActionView)の場合 # kaminari gemは以下の3つのgemからなるので、個別にinstallしてもOK # こんな感じでコンポーネントを選ぶことが可能 gem 'kaminari-activerecord' gem 'kaminari-actionview' gem 'kaminari-core' # これは2gemと依存関係にあるので省略可能 # Mongoid + Rails(ActionView)の場合 gem 'kaminari-mongoid' gem 'kaminari-actionview' gem 'kaminari-core' # これは2gemと依存関係にあるので省略可能 # ActiveRecord + Sinatraの場合 gem 'kaminari-activerecord' gem 'kaminari-sinatra' gem 'kaminari-core' # これは2gemと依存関係にあるので省略可能
コンポーネント一覧
ORM
kaminari-activerecord: Active Record
kaminari-mongoid: Mongoid
kaminari-mongo_mapper: MongoMapper
kaminari-data_mapper: DataMapper
フレームワーク
kaminari-actionview: Rails(Action View)
- https://github.com/kaminari/kaminari/tree/master/kaminari-actionview
- Kaminari gem内にある
kaminari-sinatra: Sinatra
kaminari-grape: Grape
その他メモ
ちゃんとorderを使う
- 内部的にはActiveRecordのlimitとoffsetを利用している。なのでちゃんとorderを使う必要がある
# bad User.page(params[:page]) # good User.order('name').page(params[:page])
ユーザーフレンドリー + ページキャッシュ
/users?page=33
を/users/page/33
でアクセスできるようにすると良いらしい- ユーザーフレンドリーになる
- ページキャッシュが効く
# routes.rb # concern使わない場合 resources :users do get 'page/:page', action: :index, on: :collection end # concernを使う場合 concern :paginatable do get '(page/:page)', action: :index, on: :collection, as: '' end resources :users, concerns: :paginatable
1.0.0で大きく変わってるので注意する
- 変更点
- コンポーネント化
- generatorのhaml/slimテンプレート作成機能がDeprecated(
-e haml
、-e slim
) - without_count追加
- 他にも色々
管理画面のテンプレートを用意する
- 管理画面等のテンプレートが必要になった場合には、ジェネレータで
--views-prefix
オプションを使う
$ rails g kaminari:views default --views-prefix admin
create app/views/admin/kaminari/_next_page.html.erb
create app/views/admin/kaminari/_last_page.html.erb
create app/views/admin/kaminari/_first_page.html.erb
create app/views/admin/kaminari/_page.html.erb
create app/views/admin/kaminari/_paginator.html.erb
create app/views/admin/kaminari/_prev_page.html.erb
create app/views/admin/kaminari/_gap.html.erb
サポート
- Ruby 2.0.0, 2.1.x, 2.2.x, 2.3.x, 2.4.x, 2.5.x, 2.6
- Rails 4.1, 4.2, 5.0, 5.1, 5.2
- Sinatra 1.4
- Haml 3+
- Mongoid 3+
- MongoMapper 0.9+
- DataMapper 1.1.0+
ざっくりコードリーディング
- v1.1.1(現在の最新)が対象
- どこに何があるか?程度
lib/
- lib/配下はkaminari.rbとバージョンファイルしかない。実際のコードは各コンポーネントにある
lib/kaminari.rb
- 3つの主要コンポーネントをrequire
- これで
gem "kaminari"
で一通りの機能が使えるようになる
require 'kaminari/core' require 'kaminari/actionview' require 'kaminari/activerecord'
kaminari-core/
- kaminariのコアロジック
- コレ自体がgem
コア機能
kaminari-core/lib/kaminari/
- コア機能置き場
kaminari-core/lib/kaminari/models/array_extension.rb
Kaminari.paginate_array
のロジック
kaminari-core/lib/kaminari/models/configuration_methods.rb
paginates_per
等のモデルの設定用のクラスメソッド群
kaminari-core/lib/kaminari/models/page_scope_methods.rb
per
等のscopeメソッド群
kaminari-core/lib/kaminari/helpers/helper_methods.rb
paginate
等のヘルパーメソッド群
kaminari-core/lib/kaminari/helpers/tags.rb
- kaminariのページネーションで使われるHTMLを表現するTagクラス
- TagのサブクラスとしてPrevPage等があり、それらは
app/views/kaminari/_prev_link.html.erb
等に対応する
kaminari-core/lib/kaminari/helpers/paginator.rb
- TagのサブクラスとしてPaginatorがあり、
app/views/kaminari/_paginator.html.erb
等に対応する(?)
パーシャル
kaminari-core/app/views/kaminari/
- パーシャル置き場
$ rails g kaminari:views default
で作成するパーシャルでもある- erb以外にもhaml/slimもある。Deprecatedなだけでまだ使えるっぽい
kaminari-core/app/views/kaminari/_first_page.html.erb
- "<< First"となる、1ページ目へのリンクのパーシャル
t('views.pagination.first')
でロケールファイルを利用
<span class="first"> <%= link_to_unless current_page.first?, t('views.pagination.first').html_safe, url, remote: remote %> </span>
kaminari-core/app/views/kaminari/_last_page.html.erb
- "Last >>"
kaminari-core/app/views/kaminari/_next_page.html.erb
- "Next >"
kaminari-core/app/views/kaminari/_prev_page.html.erb
- "< Prev"
kaminari-core/app/views/kaminari/_page.html.erb
- "4"となる、各ページへのリンクのパーシャル
kaminari-core/app/views/kaminari/_gap.html.erb
- "..."となる、ページ間の省略を表すパーシャル
kaminari-core/app/views/kaminari/_paginator.html.erb
paginate
ヘルパーでrenderするやつ。ページネーションHTML全体。_first_page.html.erb
等のパーシャルはここで利用される- kaminari-core/lib/kaminari/helpers/paginator.rbのKaminari::Helpers::Paginatorインスタンスを利用して、renderしてる。
first_page_tag
などもpaginator.rbで定義
<%= paginator.render do -%> <nav class="pagination" role="navigation" aria-label="pager"> <%= first_page_tag unless current_page.first? %> <%= prev_page_tag unless current_page.first? %> <% each_page do |page| -%> <% if page.display_tag? -%> <%= page_tag page %> <% elsif !page.was_truncated? -%> <%= gap_tag %> <% end -%> <% end -%> <% unless current_page.out_of_range? %> <%= next_page_tag unless current_page.last? %> <%= last_page_tag unless current_page.last? %> <% end %> </nav> <% end -%>
kaminari-core/config/locales/kaminari.yml
- デフォルトのロケールファイル
- パーシャルはここを参照してる
ジェネレータ
kaminari-core/lib/generators/kaminari/config_generator.rb
$ rails g kaminari:config
に対応
kaminari-core/lib/generators/kaminari/views_generator.rb
$ rails g kaminari:views
に対応
kaminari-actionview/
- ActionViewモジュール
- ActionView::Baseにヘルパー機能をincludeして、ビューでヘルパー使えるようにしてる
- 他にもちょい
kaminari-activerecord/
- ActiveRecordモジュール
- ActiveRecord::Baseにscope/config機能をincludeしてる