LoginSignup
37
41

More than 5 years have passed since last update.

Rails5でAPIモードでファイルアップロード機能を作成した際のサンプル解説とポイントまとめ

Last updated at Posted at 2016-09-29

はじめに

以前にRailsでの開発現場を経験したことや最近Rails5で新しくAPIモードが追加されたこともあり、自分でも気になっていた部分や実装の際のポイントやそれを取り巻く周辺知識や概念を自分なりにまとめたものを「Ruby / Rails ビギナーズ勉強会 第16回 #coedorb」での登壇の際に発表してきました。
このような機会を本当にありがとうございました!

マイクロサービスアーキテクチャ等の概念に関する部分については、こちらの記事では割愛していますがこの記事では登壇の際にデモを行った際のサンプルに関して深堀りしてまとめたものになりますので、上記の資料と併せて活用して頂ければ幸いに思います。

1. サンプル概要

Rails5のAPIモードで作成したAPIサーバーにおいて、静的なHTMLページからファイルのアップロードを行うサンプルになります。
現状では画像アップロードと新規追加部分だけではありますが、今後もWebアプリケーションやネイティブアプリのAPIサーバーを作成する際に活用して頂ければ幸いに思います。

  • フロントエンド側:gulp(Node.js)
  • バックエンドAPI側:Rails5(APIモード)

フロントエンド側の画面

upload_form.png

バックエンド側の画面

json_response.png

今回のサンプルではあくまでRails側をAPIの機能を提供するためのものとして使用する形にして、他のフロントエンドの部分からこのAPIの機能にアクセスして処理を行うというものになります。

2. このサンプルを動かすための環境設定手順

各々のローカルサーバーを立ち上げてフロンドエンドのindex.htmlに書かれたJavaScript(jQuery)のコードを経由してAPI側のエンドポイント(Rails5側のcreateメソッド)にアクセスしてファイルの投稿を行うサンプルになります。

まずは本サンプルを下記コマンドでcloneする or Zipファイルをダウンロードしてください。

$ git clone git@github.com:fumiyasac/rails-api-mode-sample.git

サンプルを展開できた後の設定手順から動きを確かめるところまでの手順を下記にまとめておきます。

★2-1. このサンプルを動かすための準備(node.js)

frontendフォルダ内にpackage.jsonファイルがあるので、node.jsがすでにインストールされている場合には下記のコマンドを実行してインストールをして下さい。

$ cd frontend/
$ npm install

インストールが完了したらgulpを実行してサーバーを起動して下さい。

$ gulp

コンソール内の[BS] Access URLs:のExternalのURLにアクセスしてエラー等が起こらなければOKです。

local_server.png

  • デフォルトではlocalhost:3000でのアクセスになりますが、今回はコンソールに表示される[BS] Access URLs:のExternalのURLにアクセスして下さい。
  • 今回はHTMLの文法チェックが入っているのでご活用下さい。
  • node.jsの環境構築とgulpの導入に関しては4. node.jsをインストールからgulpの環境を構築する手順まとめを参考にしてみて下さい。
★2-2. このサンプルを動かすための準備(Rails5)

API部分は本サンプルではすでに出来上がっている状態ですので、Gemの導入とDB(developmentではsqlite3を使用)のマイグレーションを行います。

$ cd api/file_upload_api/
$ bundle install
$ rails db:migrate

上記が完了して特にエラー等が発生しなければサーバー起動を実行すればOKです。

$ rails s -b 127.0.0.1

127.0.0.1:3000にアクセスしてエラー等が起こらなければOKです。

Rails5を動かす環境がない場合 or Rails4からアップグレードを行う場合の参考資料:

サンプルを動かす際にはRails5で開発できる環境が必要になります。下記にて環境構築を行う際の参考になりそうな記事等をピックアップしましたので、ご参考になれば幸いに思います。
特にRails5からはRubyのバージョンが2.2.2以上が必要になりますのでRubyのバージョンを上げる作業をインストール前に済ませておく必要があります。

お使いのPCがMacの場合の参考例:

お使いのPCがWindowsの場合の参考例:

私が使用している開発環境のmacではRails4.1のプロジェクトがあるため、rbenvを利用してrubyのバージョンを切り替えるような形にしています。

3. 仕様とRails5側の実装についてのポイントまとめ

※現状の仕様に関しては下記のような感じになっています。こちらの機能に関しては今後追加していく予定です。

今回のRails5で作成したAPI側はScaffoldを使用しています。

ざっくりと構築までの手順を説明すると

  1. プロジェクトをAPIモードで作成
  2. 必要なGemの有効化や追記
  3. ScaffoldでAPIの雛形を作成
  4. jbuilderファイルで出力するjsonを整形

という流れになります。下記が構築までのコマンドをまとめたものになります。

$ rails _5.0.0_ new file_upload_api --api
---(paperclipとaws-sdkを追加 & jbuilderとrack-corsのコメントアウトを外す)---
$ bundle install
$ rails g scaffold Item title:string description:string
$ rails g paperclip item picture
$ rails db:migrate

今回はファイルアップロード機能を追加したのでファイルアップロード用の関連Gem(paperclipとaws-sdk)も一緒に導入しています。
下記のGemfile(デフォルトに少し追記したもの)を見てみると、APIモードの際には通常のインストールの際に記載されているフロントエンド関連のGem例:jquery-railsやturbolinks等がない状態になっており、ある意味ではサーバーサイドのAPIを作成することにより特化したものになっています。

Gemfile
source 'https://rubygems.org'


# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 5.0.0'
# Use sqlite3 as the database for Active Record
gem 'sqlite3'
# Use Puma as the app server
gem 'puma', '~> 3.0'

# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
# (有効化)JSON出力用のテンプレートを作成するGem
gem 'jbuilder', '~> 2.5'

# Use Redis adapter to run Action Cable in production
# gem 'redis', '~> 3.0'
# Use ActiveModel has_secure_password
# gem 'bcrypt', '~> 3.1.7'

# Use Capistrano for deployment
# gem 'capistrano-rails', group: :development

# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible
# (有効化)クロスドメインでのアクセスを有効にするGem
gem 'rack-cors'

# (追加)画像アップロードをしやすくするGem
gem 'paperclip', '~> 5.0.0.beta1'
gem 'aws-sdk', '~> 2.0'

group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug', platform: :mri
end

group :development do
  gem 'listen', '~> 3.0.5'
  # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
  gem 'spring'
  gem 'spring-watcher-listen', '~> 2.0.0'
end

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

また、作成したRails5プロジェクト内のapp配下のディレクトリにも変化があります。フロントエンド関連のGemがないので、当然helperディレクトリがなくviewsディレクトリもJSONファイルの整形を行うjbuilderファイルやメール関連のもの等API構築に必要なものしかない状態になっています。

参考:RailsによるAPI専用アプリ

★3-1. Rails5をAPIモードで動かす際にカギとなるGemやこのサンプルで追加したGem等に関して

Rails5でAPIモードの状態でAPI開発を行う際で是非とも押さえておきたいGemや今回のサンプルで導入したGemに関しての部分をざっくりと解説します。
Rails5でAPIモードの状態プロジェクトを作成した際には下記の2つのGemがコメントアウトされた状態になっていますので下記のGemを有効化します。

ポイント1. コメントアウトを行っている部分を有効化する

  • (有効化)JSON出力用のテンプレートを作成するGem:gem 'jbuilder', '~> 2.5'
  • (有効化)クロスドメインでのアクセスを有効にするGem:gem 'rack-cors'

☆jbuilderについて

jbuilderはRails4からデフォルトで導入されたものです。ざっくりと言ってしまうとjsonの整形をこれまでの.erbファイルと似たような感じで行うために必要なGemになります。

今回は[GET] http://127.0.0.1:3000/items.jsonでアクセスした際のレスポンスで返されるJSONファイルを整形するために使用しています。

app/views/items/index.json.jbuilder
# jbuilderを用いてJSONを整形する
json.item do
  json.contents do
    json.array!(@items) do |t|
      json.id t.id
      json.title do
        json.label t.title
      end
      json.description do
        json.label t.description
      end
      json.thumbnail_medium do
        json.url ("http://#{request.host}:#{request.port.to_s}" + t.picture.url(:medium))
      end
    end
  end
end

上記のようにレスポンスで出力するJSONをクライアント側で受け取りたい形に合わせて柔軟に整形することができるようになります。下記が今回のサンプルからデータを入力した際のJSONの一例になります。

{
    "item": {
        "contents": [
            {
                "id": 1,
                "title": {
                    "label": "おでんを食べました。"
                },
                "description": {
                    "label": "まだ夏の終わりで暑い日が続きますが、コンビニでもおでんが店頭に並んでいることが増えました。"
                },
                "thumbnail_medium": {
                    "url": "http://127.0.0.1:3000/system/items/pictures/000/000/001/medium/oden.jpg?1475182069"
                }
            },
            {
                "id": 2,
                "title": {
                    "label": "ホットドッグを食べました。"
                },
                "description": {
                    "label": "休日の朝に散歩していると、偶然おしゃれなカフェを見つけたのでつい立ち寄ったので買ってみました。"
                },
                "thumbnail_medium": {
                    "url": "http://127.0.0.1:3000/system/items/pictures/000/000/002/medium/hotdog.jpg?1475182144"
                }
            },
            {
                "id": 3,
                "title": {
                    "label": "サラダを食べました。"
                },
                "description": {
                    "label": "一人暮らしだとついつい野菜が不足しがちなので、最近は健康のためにも進んで食べるようにしています。"
                },
                "thumbnail_medium": {
                    "url": "http://127.0.0.1:3000/system/items/pictures/000/000/003/medium/salad.jpg?1475182225"
                }
            }
        ]
    }
}

☆rack-corsについて

rack-corsはCORS(Cross-Origin Resource Sharing)をコントロールするためのものです。
各ブラウザではXSS(クロスサイトスクリプティング)を防止のためにクロスドメイン通信を拒否する仕組みが実装されています(簡単に言うとドメインをまたいだ通信ができないようにしてある)。
今回は特定のリクエスト(GET or POST)でアクセスがあった際には、クロスドメイン通信の許可をしたいのでこのGemを有効化して設定を行います。

rack-corsを有効にした際は、config/application.rbへ下記を追記しています。

config/application.rb
module FileUploadApi
  class Application < Rails::Application
    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration should go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded.

    # Only loads a smaller set of middleware suitable for API only apps.
    # Middleware like session, flash, cookies can be added back manually.
    # Skip views, helpers and assets when generating a new resource.

    # Corsに関する設定を追記する
    config.middleware.insert_before 0, "Rack::Cors" do
      allow do
        origins '*'
        resource '*', :headers => :any, :methods => [:get, :post, :options]
      end
    end
    config.api_only = true
  end
end

Scaffoldで作成する際には一覧・詳細・追加・変更・削除の処理を一気に作成してくれますが、今回使用するのは新規追加の部分とJSONの出力だけなので:methods => [:get, :post, :options]としています。
※今回はサンプルなのであくまで簡単なセキュリティの考慮しかしていませんが、実際の開発の際にはセキュリティ対策をしっかりと行うようにしてください。

ポイント2. 新たに追加したファイルのアップロードに関するGem

  • (新規追加)画像等のファイルアップロードをしやすくするGem:gem 'paperclip', '~> 5.0.0.beta1'
  • (新規追加)AWSへのアクセスや処理をするためのGem:gem 'aws-sdk', '~> 2.0'

※ローカル内でのみ本ファイルを動かす際はaws-sdkは特に不要です。(heroku等のサーバーを使用する場合に画像などのアップロードファイルを保存する際に必要)

実装例はAPI側のitem.rb(Model側)items_controller.rb(Controller側)を参考に実装して頂ければと思います。この部分の実装に関しては特に従来のRailsでの実装方法と同じ形で問題ありません。

またAmazonS3を外部ストレージとして使用する場合には下記の記事等を参考に設定を行って頂ければと思います。
参考:PaperclipとAWS S3を用いた画像アップロード機能作成手順まとめ

またPaperclipを導入した際には、ファイルのアップロードに関連する部分の追記をScaffoldで出力されたControllertファイルとModelファイルに行う形になります。

Modelファイル(item.rb)

こちらはScaffoldで生成されたコードにPaperclipの設定部分を追加したものになります。Modelファイルの書き方がRails4の場合の書き方と若干異なる部分もありますので、その点は注意してください。

app/models/item.rb
class Item < ApplicationRecord
  # 画像アップロード時に切り出すファイルのサイズを指定する
  has_attached_file :picture, styles: { large: "640x480>", medium: "300x300>", thumb: "100x100>" }
  # 画像のバリデーションを設定する(画像は必須 & 画像のMIME-TYPEが正しい形式であること)
  validates_attachment :picture, presence: true, content_type: { content_type: ["image/jpeg", "image/gif", "image/png"] }
  do_not_validate_attachment_file_type :picture
end

Controllerファイル(items_controller.rb)

こちらはScaffoldで生成されたコードにitem_paramsメソッド内へ:pictureの追記と日本語コメントが入っている部分が追記または若干の修正を行った部分になります。

app/controllers/items_controller.rb
class ItemsController < ApplicationController
  before_action :set_item, only: [:show, :update, :destroy]

  # GET /items
  # GET /items.json
  def index
    @items = Item.all
    # index.json.jbuilderで出力するjsonを整形する(出力するJSONの形式を変えています)
  end

  # GET /items/1
  # GET /items/1.json
  def show
    # show.json.jbuilderで出力するjsonを整形する(今回は省略)
  end

  # POST /items
  def create
    @item = Item.new(item_params)

    if @item.save
      # 今回はコールバックをフロント側で受け取るのでステータスを返すだけのものにする
      render json: @item, status: :created, location: @item
    else
      render json: @item.errors, status: :unprocessable_entity
    end
  end

  # ※ここから先のupdate/destoryアクションは使用しない
  # PATCH/PUT /items/1
  def update
    if @item.update(item_params)
      render json: @item, status: :ok, location: @item
    else
      render json: @item.errors, status: :unprocessable_entity
    end
  end

  # DELETE /items/1
  def destroy
    @item.destroy
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_item
      @item = Item.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def item_params
      params.require(:item).permit(:title, :description, :picture)
    end
end

また今回の実装に関しては下記の記事を参考にして、ところどころ簡略化をして実装を行ったものになります。

見た目関連の部分の実装が少なくなる分、幾分シンプルにはなっていますがこれまでの慣れ親しんだRailsの書き方からは大きく外れることはないので、個人的には今までAPI開発の際には少しRailsだとちょっと大げさかもと感じる部分がありましたが、これからはどんどん活用してみようと思いました(^^)

★3-2. 実際にファイルのアップロード処理を行っている部分(フロントエンド)

フロントエンド側の実装に関しては、htmlファイルになるのでAjax通信を利用してAPIに対してPOSTでデータを送信し、API側での処理のコールバックに応じてエラーハンドリングを行うような形にしています。
※こちらの部分に関しても実際の運用で活用する場合はbasic認証等のセキュリティに関する部分の考慮をするようにして下さい。

formタグ部分の実装は下記のようになります。

index.html
<form id="upload-form" method="POST" action="#" accept-charset="UTF-8" enctype="multipart/form-data">
    <div class="form-group">
        <label for="title">Title:</label>
        <input class="form-control" placeholder="タイトルを入力して下さい。" name="item[title]" type="text" id="title">
    </div>
    <div class="form-group">
        <label for="description">Description:</label>
        <textarea class="form-control" placeholder="本文を入力して下さい。" name="item[description]" cols="50" rows="10" id="description"></textarea>
    </div>
    <div class="form-group">
        <label for="picture">Picture:</label>
        <input name="item[picture]" type="file" id="picture">
    </div>
    <div class="form-group">
        <input class="btn btn-primary form-control" type="submit" id="submit-btn-upload" value="新規投稿をする">
    </div>
</form>

inputタグのname属性はrails側での処理に合わせて書いている感じにしています。
またフォーム部分にて画像ファイルをAjaxを用いてアップロードする部分に関しては下記のようになります。(jQuery2系を使用)

index.html
(function(win, doc) {
  $('form').submit(function(e) {
    $(this).find(':submit').prop('disabled', true);
    $(this).find(':submit').val('送信中...');
    e.preventDefault();
    var fd = new FormData($(this)[0]);
    $.ajax('http://127.0.0.1:3000/items', {
      method: 'POST',
      processData: false,
      contentType: false,
      data: fd,
      dataType: 'json',
      success: function(json) {
        $('form').find(':submit').val('画像の投稿が完了しました');
        alert('ファイルが投稿されました');
      },
      error: function(json) {
        $('form').find(':submit').prop('disabled', false);
        $('form').find(':submit').val('新規投稿を再度チャレンジする');
        alert('エラーが発生しました');
      }
    });
  });
})(this, document);

必要項目を入れてデータを送信するとAPI側のコンソールでは下記のように処理がなされます。

post_success.png

また、こちらのHTMLのデザインに関してはTwitterBootstrapを使用しています。

4. node.jsをインストールからgulpの環境を構築する手順まとめ

今回のサンプルはRails5のAPIモードと活用方法に解説がメインになりますが、gulpの環境を整えておくとhtml/javascript/css等のフロントエンドの開発でも役に立つ局面があるので、node.jsが導入されていない環境から「gulpを導入する & ローカルサーバー立ち上げとhtmlの文法チェックの導入」までの手順を下記にまとめました。
(自分の環境で構築しているので、もしかしたらバージョンが異なってしまう場合等があるかと思いますのでその部分はご容赦下さいm(_ _)m)

(自分の環境)
ご参考になるかはちょっとわかりませんが、私の開発環境は下記の通りになります。

  • node.js 4.5.0
  • MacOS X El Capitan (Ver10.11.6)
★4-1. node.jsをローカルPCで導入する

node.jsのインストール方法に関してはnodebrewを用いたインストール方法もありますが、今回はNode.jsはローカルマシンの中で使用するので、Node.jsの公式サイトよりダウンロードしてインストーラー経由でのインストールする形にします。

node.jsの公式サイトよりインストールする

nodejs.png

まずはnode.jsの公式サイトよりnode.jsのインストーラをダウンロードして自分のお使いのPCへインストールをして下さい。
(node.jsのインストーラーを立ち上げて進んでいけばOKです。)

★4-2. gulpを導入してHTMLの文法チェック&ローカルサーバー起動をするためのプラグインをインストールする

下記のコマンドを実行して自分の作業を行うフォルダ内にpackage.jsonファイルを作成し、Gulpをインストールします。

//package.jsonの作成
$ npm init

//Gulpをグローバルインストール
$ npm install gulp -g

//Gulpをローカルインストール
$ npm install gulp --save-dev

//Gulpのバージョン確認
$ gulp -v

※1) package.jsonの作成時に色々聞かれると思いますがはじめのうちはEnter連打でOKです。(package.jsonの内容は後からでも変更が可能です)
※2) package.jsonにgulpのバージョンが記載されるので次回からは$ npm installコマンドを実行するだけでOKです。(上記のサンプルリポジトリをクローンした際も同様でお願いします)

他にもGulp導入のメリットについて述べられている記事を下記にピックアップをしておきますので、参考にして頂ければ幸いに思います。

ここからはHTMLの文法チェック&ローカルサーバー起動をするためのプラグインをインストールする手順になります。

今回インストールを行うプラグインは下記になります。プラグインに関する詳細な情報に関しては下記の表内のリンクページ等を参考にしてみてください。

プラグイン名 プラグインの機能概要 リンク
gulp-htmlhint HTMLの文法チェックを行う gulp-htmlhintへのリンク
browser-sync ローカルでページ検証用のブラウザを立ち上げる browser-syncへのリンク

下記のコマンドを実行して上記のプラグインを実行します。

//gulp-htmlhintのインストール
$ npm install gulp-htmlhint --save-dev

//browser-syncのインストール
$ npm install browser-sync --save-dev

//新しくGulpfileを作成
$ touch gulpfile.js

ここまで完了したら次は、新しく作成したgulpfile.jsの中にnode.jsでgulpコマンドを実行した際に行うタスク処理を記述していきます。

gulpfile.js
//必要なモジュールを読み込む
var gulp     = require('gulp');
var htmlhint = require('gulp-htmlhint');
var browser  = require('browser-sync');

//ローカルサーバーを立ち上げるタスク
gulp.task('server', function() {
    browser({
        server: {baseDir: "./src"}
    });
});

//htmlの文法チェックを行うタスク
gulp.task('html', function() {
    gulp.src('./src/**/*.html')
        .pipe(htmlhint()) //実際に処理を行う
        .pipe(htmlhint.reporter());
});

//htmlファイル変更時に実行するタスク(デフォルトに追加)
gulp.task('html-watch', ['html'], function() {
    var htmlhint = gulp.watch('./src/**/*.html', ['html']);
    htmlhint.on('change', function(event) {
        console.log('File ' + event.path + ' was ' + event.type + ', running tasks...');
    });
});

//デフォルトで実行されるタスク
gulp.task('default', ['server','html-watch']);

これでhtmlを修正した際に文法チェックができるようになりました。また$ gulpを実行した際にブラウザーが立ち上がるのでこれでローカル環境での検証もできます。

そして参考までに今回のサンプルの最終的なpackage.jsonは下記のような形になります。
(こちらは私の開発環境での例になります)

package.json
{
  "name": "file_upload_api_mode",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "browser-sync": "^2.12.8",
    "gulp": "^3.9.1",
    "gulp-htmlhint": "^0.3.1"
  }
}

今回は一緒に検証を行うためにフロントエンド側のローカルサーバーをnode.jsで構築していますが、frontendディレクトリの部分を他の自分のローカルサーバー等に置いて検証することも可能です。

あとがき

最近はサーバーサイドだけでiOSアプリ開発も行うようになったので、記事や画像のデータ管理を自作のAPIと必要最低限の管理画面などで構成したいと思う局面がよく出てきました。また本業ではサーバーサイドのAPI開発や機能改善等にも関わることが多かったことやいくつかの開発現場ではマイクロサービスアーキテクチャを活用した設計をしていたという経緯もあり、その理解やメリットも感じることが多かったので今回は自分なりにAPIモードで実際に簡単なサンプルを実装することで、「こんな感じで作成すれば自分のアプリでも使えるかも!」という感覚をつかんでみたいという思いからまとめてみました。これからRailsを始める方や普段はフロントエンドに近い部分を得意としているけれども簡単なAPIを自分で実際に開発してみたいと感じている方の少しでも手助けになれば嬉しく思います。

説明が足りない部分に関しては随時補足や修正を加えていく所存ですので、何卒宜しくお願い致しますm(_ _)m

補足事項

  • 1) こちらのサンプルは今後も機能を加えたり、Railsのバージョンアップにも引き続き対応していく所存です。
  • 2) ご質問やPullRequest・要望等がありましたらお気軽にどうぞ^^
37
41
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
37
41