Rails4 + AngularJS + paperclip で動的画像アップロード
やりたいこと
画像の横のあるファイル選択ダイアログからファイルを選んだら、すぐにファイルをサーバーにアップロードして、アップロードに成功したら現在の画像をすり替える、ということをやりたいです。
図にするとこんな感じです。
環境
- Rails4 をベースにしたアプリケーション
- AngularJS 1系を JSフレームワークとして使用
- paperclip を使用して、画像格納用のモデルを作成
- angular-file-upload を使用して、クライアント <=> サーバー間通信を行う
概要
ざっくり言うと、上であげた「やりたいこと」は以下の 2つの要素に分解できます。
- 画像を格納するモデルを作成する
- 画像をアップロードする機能を追加する
混ぜて書くとわかりにくくなるので、それぞれ説明します。
画像を格納するモデルを作成する
まず、アップロードされた画像をサーバーに格納するためのモデルを作成します。 一から作るのは面倒なので、paperclip という gem を使用します。
インストール
- Gemfile に以下の 1行を追加します。
gem 'paperclip'
- bundle install します。
$ bundle install
モデル作成
- 画像を格納するためのモデルとして、Pictureモデルを作成します。
$ rails g model Picture name:string
- Pictureモデルに avater という名前で ファイル名やパスを格納するためのアタッチメントを追加します。
$ rails g paperclip picture avater # 作成された migrationファイルを表示 $ cat db/migrate/yyyymmddHHMMSS_add_attachment_avater_to_pictures.rb class AddAttachmentPhotoToPictures < ActiveRecord::Migration def self.up change_table :pictures do |t| t.attachment :avater end end def self.down remove_attachment :pictures, :avater end end
- db:migrate します。
$ bundle exec rake db:migrate
これで、Pictureモデルに avater が追加されました。 例えば、
picture = Picture.take picture.avater.url
などとすると、画像の url を取得することができます。
画像をアップロードする機能を追加する
これで画像を格納するためのモデルが構築できたので、次は実際に View から angular 経由でサーバーにファイルをアップロードするところを実装します。
インストール
- Gemfile に以下の行を追加します。
gem 'angularjs-file-upload-rails'
- bundle install します。
$ bundle install
- application.js に以下の行を追加します。ただし、 angularjs よりも後に ロードされなければなりません。
//= require angularjs-file-upload
実装
とりあえずコードを列挙した上で説明します。 ただし、コードは分かりやすさを重視しており、そのまま本番に埋め込むのは適切ではありません。
avater.html.erb
<div ng-contoller="avaterCtrl" ng-init="setAvater(#{@picture.id})"> <img alt="avater" src="{{avater_url}}" /> <input type="file" nv-file-select uploader="uploader"/> </div>
avaterCtrl.js
var myApp = angular.module('avaterApp', []); myApp.controller('avaterCtrl', ['$scope', function($scope) { // uploaderインスタンスを作成 $scope.uploader = new FileUploader({ // /foo/bar のところには ajax_api_controller.rb で後述する post_new_avaterメソッドへのパスを入れてください url: '/foo/bar', headers : { // Rails用 'X-CSRF-TOKEN': $('meta[name=csrf-token]').attr('content') } }); // uploader にファイルが追加された際に呼び出されるコールバック関数 $scope.uploader.onAfterAddingFile = function(item) { item.upload(); }; // upload が成功したときに呼び出されるコールバック関数 $scope.uploader.onSuccessItem = function(item, response, status, headers) { // Queue を空にする $scope.uploader.clearQueue(); // アバターの url を更新 $scope.avater_url = response.avater_url; }; $scope.setAvater = function(picture_id) { // /hoge/fuga のところには get_avaterメソッドへのパスを入れてください $http.get('/hoge/fuga', { picture_id: picture_id }).success( (data) -> $scope.avater_url = data.avater_url; ) }; }]);
def get_avater picture = Picture.find(params[:picture_id]) render json: { avater_url: picture.avater.url } end def post_new_avater picture = Picture.new() picture.avater = params[:file] if picture.save render json: { avater_url: picture.avater.url } else render text: 'failed to upload new avater' end end
解説
いくつか重要な点を説明します。
クライアント側の処理
まずは、uploaderインスタンスを宣言します。
avaterCtrl.js
// uploaderインスタンスを作成 $scope.uploader = new FileUploader({ // /foo/bar にはアップロード処理に関するコントローラーのパスを指定します url: '/foo/bar', });
uploader にはいくつかの コールバック関数 が定義されており、それらをオーバーライドすることでイベントに応じた処理を行います。
まず、ファイル選択ダイアログにて画像が選択されると、その画像は Queue(キュー)に追加されます。 そのタイミングで、自動的に画像のアップロードを行いたいので、onAfterAddingFile関数をオーバーライドしています。
avaterCtrl.js
// uploader にファイルが追加された際に呼び出されるコールバック関数 $scope.uploader.onAfterAddingFile = function(item) { item.upload(); };
次に、アップロードが成功したら既存の画像を差し替えます。
avaterCtrl.js
// upload が成功したときに呼び出されるコールバック関数 $scope.uploader.onSuccessItem = function(item, response, status, headers) { // Queue を空にする $scope.uploader.clearQueue(); // アバターの url を更新 $scope.avater_url = response.avater_url; };
この際、Queue をクリアしないと一度アップロードしたファイルがずっと Queue に残り続け、ファイルを選択する度に複数ファイルをアップロードしてしまいます。 removeAfterUpload というプロパティが定義されているため、それを true にしても同じ効果が得られるはずです(未検証)。 今回は 1画像しか保持しない構成なので clearQueue しても問題ないですが、複数ファイルをアップロードする際などにはタイミング等々もう少し考える必要があります。
基本的にクライアント側の処理はこれだけなのですが、Railsアプリケーションの場合には X-CSRF-TOKEN をサーバーに送信する必要があります。 関連issue: https://github.com/nervgh/angular-file-upload/issues/40
なので、実際には uploaderインスタンスの宣言は以下のようになります。
avaterCtrl.js
// uploaderインスタンスを作成 $scope.uploader = new FileUploader({ url: '/foo/bar', headers : { // Rails用 'X-CSRF-TOKEN': $('meta[name=csrf-token]').attr('content') } });
サーバー側の処理
まず、クライアント側ではページを読み込んだ際に avater への url を取得する必要があるため、その url を返却する api を定義します。
def get_avater picture = Picture.find(params[:picture_id]) render json: { avater_url: picture.avater.url } end
angular-file-upload を使用してファイルをアップロードした場合、params[:file] にそのファイルに関するパラメータが代入されて渡されます。 なので、サーバー側ではそれを使用して新規に Pictureインスタンスを作成します。
def post_new_avater picture = Picture.new() picture.avater = params[:file] if picture.save render json: { avater_url: picture.avater.url } else render text: 'failed to upload new avater' end end
最後に、これらの apiメソッドに対して適宜 routing を行えば完了です。 routing に関しては他に素晴らしいドキュメントがいくつもあるので、ここでは割愛します。
まとめ
これで動的にアバター画像を変更することができるはずです。 普段は個人的な趣味嗜好で、Rails のテンプレートエンジンには slimテンプレートを使っており、 また、js は coffeescript で書いているので、本記事のコードには文法エラーがたぶんに組み込まれている可能性があります。 何か気になる点等あればご連絡いただけると幸いです。