ZEALS TECH BLOG

チャットボットによる会話広告『fanp』を開発する株式会社ZEALSのテックブログです。技術やエンジニア文化について情報を発信します。

Rails + ReactでS3に画像データをuploadするまで

スクリーンショット 2019-05-19 19.53.26.png

こんにちは!
普段Rails + Reactを書いていますkannachiです。

Rails + Reactを使った自作アプリで画像データをAmazon S3にuploadすることがあったのですが、 せっかくなので、その時の実装を簡略化して紹介したいと思います。

githubに今回のコードを残しているので良ければ参考にしてみてください!

github.com

今回は以下のようなことをやっていきます。

それでよろしくお願いします!

1. rails newとcreate-react-appを使って簡単なAppを作成

まずプロジェクトを作成しましょう。

$ rails new sample-app-with-aws -d mysql --api

gemfileでrack-corsのコメントアウトを外してbundle installしてください。

gem 'rack-cors'
$ bundle install 

railsでは3001ポートを利用します。

以下のコマンドでサーバーが立ち上がることを確認してください。

$ rails s -p 3001

application.rbを変更します。

module SampleAppWithAws
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 5.2

    config.api_only = true
    config.middleware.insert_before 0, Rack::Cors do
      allow do
        origins 'http://localhost:3000'
        resource '*',
        :headers => :any,
        :methods => [:get, :post, :put, :patch, :delete, :options]
      end
    end
  end
end

つぎにcreate-react-appを使ってviewを作成していきます。

$ create-react-app front_end

以下のコマンドで、reactサーバーが起動されるか確認してください。

cd front_end
npm start

スクリーンショット 2019-05-19 19.53.26.png

App.jsを編集します。

import React from "react";
import logo from "./logo.svg";
import "./App.css";
import User from "./Views/User";

function App() {
  return (
    <div className="App">
      <User />
    </div>
  );
}

export default App;

2. フロントで画像を選択と描写

formikというライブラリを使うと簡単にユーザー登録ができるようになります。

$ yarn add formik

今回はわかりやすいようにバリデーションは考慮せずに、cssも記述しません。
users componetを作成し、編集していきます。

import React from "react";
import { Formik, Form, Field } from "formik";

export class Users extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      // 画像を表示するためにstateを作成します.
      profileImage: ""
    };
  }

  // 後ほど記述
  createUser = payload => {};

  setImage = (e, setFieldValue) => {
    let files = e.target.files;
    let reader = new FileReader();
    // 画像をbase64にエンコードします.
    reader.readAsDataURL(files[0]);
    reader.onload = () => {
      // stateに画像を入れることで描写させます.
      this.setState({ profileImage: reader.result });
      // formikで送信できるようにsetFieldValue()を呼び出します.
      setFieldValue("profile_image", reader.result);
    };
  };

  render() {
    return (
      <Formik
        initialValues={{
          name: "",
          profile_image: ""
        }}
        onSubmit={this.CreateUser}
      >
        {({ setFieldValue, isSubmitting }) => {
          return (
            <Form>
              <label>プロフィール画像</label>
              <img
                className="profile-image"
                src={!this.state.profileImage ? "" : this.state.profileImage}
              />
              <React.Fragment>
                <Field
                  id="select_profile_image"
                  type="file"
                  name="profile_image2"
                  onChange={e => this.setImage(e, setFieldValue)}
                />
                <Field type="hidden" name="profile_image" />
              </React.Fragment>
              <label>名前</label>
              <Field className="input" type="text" name="name" />
              <button
                className="submit-button"
                type="submit"
                disabled={isSubmitting}
              >
                送信
              </button>
            </Form>
          );
        }}
      </Formik>
    );
  }
}

export default Users;

これで画像の描写ができるようになりました。

スクリーンショット 2019-05-19 19.45.06.png

3. 画像をリサイズしてエンコード化

しかし、このままでは大きな画像データをバックエンド及びS3へ、際限無く送ることになってしまいます。

そのため、フロント側で画像をリサイズしてからバックエンドに送るようにします。

修正後

.
.
  //canvasにresizeした画像を描写した後にエンコード
  setImage = (e, setFieldValue) => {
    let canvas = document.getElementById("canvas");
    let ctx = canvas.getContext("2d");
    let maxW = 250;
    let maxH = 250;

    let img = new Image();
    img.onload = () => {
      let iw = img.width;
      let ih = img.height;
      let scale = Math.min(maxW / iw, maxH / ih);
      let iwScaled = iw * scale;
      let ihScaled = ih * scale;
      canvas.width = iwScaled;
      canvas.height = ihScaled;
      ctx.drawImage(img, 0, 0, iwScaled, ihScaled);
      const resizeData = canvas.toDataURL("image/jpeg", 0.5);
      this.setState({ profileImage: resizeData });
      setFieldValue("profile_image", resizeData);
    };
    img.src = URL.createObjectURL(e.target.files[0]);
  };

  render() {
    return (
      <Formik
        initialValues={{
          profile_image: "",
          name: ""
        }}
        onSubmit={updateUser}
      >
        {({ setFieldValue, isSubmitting }) => {
          return (
            <Form>
              <label>プロフィール画像</label>
              <img
                src={!this.state.profileImage ? "" : this.state.profileImage}
              />
              <React.Fragment>
                <Field
                  type="file"
                  onChange={e => this.setImage(e, setFieldValue)}
                />
                <Field type="hidden" name="profile_image" />
              </React.Fragment>
              {/* resizeした画像を描写するためのcanvasを作成 */}
              <canvas
                id="canvas"
                style={{
                  display: "none"
                }}
                width="64"
                height="64"
              />
              <label>名前</label>
              <Field className="input" type="text" name="name" />
              <button
                className="submit-button"
                type="submit"
                disabled={isSubmitting}
              >
                送信
              </button>
            </Form>
          );
        }}
      </Formik>
    );
  }
}

これで画像データをresizeして送ることができるようになりました。

4. バックエンドにデータを送信

次にバックエンドにデータを送信します。
axiosを使えば簡単にHTTP通信が扱えるのでREST-API を簡単に実装できます。

$ npm install axios --save

github.com

以下のメソッドを作成して追加してください。

import axios from "axios";

.
.

 createUser = payload => {
    axios
      .post("http://localhost:3001/users", payload)
      .then(({ data, message }) => {
        if (data) {
          this.setState({ user: data });
        } else {
          throw new Error(message);
        }
      })
      .catch(e => alert(e.message));
  };

Databaseを作成しましょう。

$ mysql -u root -p

mysql> CREATE DATABASE sample_app_with_aws;

database.ymlの内容を変更しておきます。

development:
  <<: *default
  database: sample_app_with_aws

Userテーブルを作成します。

$ rails g migration CreateUsers
class CreateUsers < ActiveRecord::Migration[5.2]
  def change
    create_table :users do |t|
      t.string :name, null: true, comment: '名前'
      t.text :image_data, comment: '画像データの名前'

      t.timestamps
    end
  end
end

migrateを行います。

$ rails db:migrate

controllerを作成します。

$ rails g controller users

users_controllerの中はこんな感じです。

class UsersController < ApplicationController

  def create
    user = User.create!(user_params)
    render json: user
  end

  private

  def user_params
    params.permit(:name, :image_data)
  end
end

config/routesを設定します。

Rails.application.routes.draw do
  resources :users
end

これでバックエンドにデータを送ることができるようになりました。

5. S3に画像データをupload

S3にアクセスできるように以下のgem file以下を記述してbundle installします。

gem 'aws-sdk'

次にaccess_key, secret_access_keyを取得します。
取得方法についてはたくさん記事がありますので割愛します。

keyを.envファイルに保管してくれるgemがありますのでこちら利用します。

https://github.com/bkeepers/dotenv/

gem 'dotenv-rails'

.envに以下を記述します。 (gitignoreに.envを追加するのを忘れないでください。)

AWS_ACCCES_KEY='######'
AWS_ACCCES_SECRET_KEY='######'

自分のS3にsample_bucketという名前のbucketを作成してください。

controllerを編集してにAWSにuploadできるようにします。
(今回の実装だとmysqlにも同時に保存しています。)

class UsersController < ApplicationController

  def create
    user = User.create!(user_params)
    # bucketを設定
    bucket = Aws::S3::Resource.new(
      :region => 'ap-northeast-1',
      # keyは.envファイルに補完しています. 
      :access_key_id => ENV['AWS_ACCCES_KEY'],
      :secret_access_key => ENV['AWS_ACCCES_SECRET_KEY'],
      ).bucket('sample_bucket') 
    # sample_bucketにencodeされた画像データをupload
    bucket.object("user_id_#{user.id}_profile_image").put(:body => params[:profile_image])
    render json: user
  end

  private

  def user_params
    params.permit(:name, :image_data)
  end
end

S3を確認すると、encodeされた画像データがuplaodされていることを確認できると思います。