LoginSignup
15
16

More than 5 years have passed since last update.

Rails4 + AngularJS + paperclip で動的画像アップロード

Posted at

やりたいこと

画像の横のあるファイル選択ダイアログからファイルを選んだら、すぐにファイルをサーバーにアップロードして、アップロードに成功したら現在の画像をすり替える、ということをやりたいです。

環境

  • 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 よりも後に ロードされなければなりません。
application.js
//= require angularjs-file-upload

実装

とりあえずコードを列挙した上で説明します。
ただし、コードは分かりやすさを重視しており、そのまま本番に埋め込むのは適切ではありません。

avater.html.erb

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

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;
    )
  };
}]);

ajax_api_controller.rb

ajax_api_controller.rb
  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

avaterCtrl.js
// uploaderインスタンスを作成
$scope.uploader = new FileUploader({
  // /foo/bar にはアップロード処理に関するコントローラーのパスを指定します
  url: '/foo/bar',
});

uploader にはいくつかの コールバック関数 が定義されており、それらをオーバーライドすることでイベントに応じた処理を行います。

まず、ファイル選択ダイアログにて画像が選択されると、その画像は Queue(キュー)に追加されます。
そのタイミングで、自動的に画像のアップロードを行いたいので、onAfterAddingFile関数をオーバーライドしています。

avaterCtrl.js

avaterCtrl.js
// uploader にファイルが追加された際に呼び出されるコールバック関数
$scope.uploader.onAfterAddingFile = function(item) {
  item.upload();
};

次に、アップロードが成功したら既存の画像を差し替えます。

avaterCtrl.js

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

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 を定義します。

ajax_api_controller.rb

ajax_api_controller.rb
def get_avater
  picture = Picture.find(params[:picture_id])
  render json: { avater_url: picture.avater.url }
end

angular-file-upload を使用してファイルをアップロードした場合、params[:file] にそのファイルに関するパラメータが代入されて渡されます。
なので、サーバー側ではそれを使用して新規に Pictureインスタンスを作成します。

ajax_api_controller.rb

ajax_api_controller.rb
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 で書いているので、本記事のコードには文法エラーがたぶんに組み込まれている可能性があります。
何か気になる点等あればご連絡いただけると幸いです。

15
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
16