APIモードのRailsでActive StorageとS3を使ってバックエンド、Reactでフロントエンドを実装したい
作り終わってから思い出して書き始めたので抜けがあるかもしれない
バックエンド(Rails API)
- APIモードでRailsアプリを作ってCORSの設定とかをいい感じにする
- aws-sdk-s3とactive_model_serializersをGemfileに追加してbundle install
/config/storage.ymlにS3使うためにservice、access_key_id、secret_access_key、region、bucketの指定をする qiita.com
$ rails active_storage:install
して$ rails db:migrate
するUserモデルを作る
/app/models/user.rbに
has_one_attached :avatar
を追加する qiita.com/config/environments/development.rbと/config/environments/production.rbと/config/environments/test.rbに
config.active_storage.service = :amazon
を追加(開発環境ではS3じゃなくてローカルに保存したいとかならここで変更)/app/serializers/user_serializer.rbにUserのSerializerを実装する。以下のavatarメソッドはlocalに保存する設定のときは失敗するので要注意。
#例 class UserSerializer < ActiveModel::Serializer attributes %i[id name avatar] def avatar object.avatar.attachment.service.send(:object_for, object.avatar.key).public_url if object.avatar.attached? end end
ルーティングを追加
post '/users/current/avatar', to: 'users#update_avatar
UsersControllerを作ってUserのavatarを更新するためのメソッドを作る
# 例 def update_avatar @user.avatar = params[:avatar] if @user.save render json: @user, status: :ok else render json: @user.errors.full_messages, status: :bad end end
- あとは適当に必要なAPIを作る。
フロントエンド(React+Redux)
- アップロードページの例
import React from "react"; import { useDispatch } from "react-redux"; import { upload_user_avatar } from "../../actions/user"; const MyPage = props => { const file_input = React.useRef(); const dispatch = useDispatch(); const handle_submit = e => { e.preventDefault(); if ( file_input && file_input.current && file_input.current.files && file_input.current.files.length > 0 ) { const submit_data = new FormData(); submit_data.append("avatar", file_input.current.files[0]); dispatch(upload_user_avatar(submit_data)); } } return ( <form onSubmit={handle_submit}> <input type="file" ref={file_input} accept="image/*" /> <input type="submit" /> </form> ); }
- actionの例
export const UPLOAD_USER_AVATAR_REQUEST = "UPLOAD_USER_AVATAR_REQUEST"; export const UPLOAD_USER_AVATAR_SUCCESS = "UPLOAD_USER_AVATAR_SUCCESS"; export const UPLOAD_USER_AVATAR_FAILURE = "UPLOAD_USER_AVATAR_FAILURE"; export const upload_user_avatar = submit_data => async dispatch => { dispatch({ type: UPLOAD_USER_AVATAR_REQUEST }); setAuthKeys(); try { const res = await axios.post( `${config.backend_api_url}/users/current/avatar`, submit_data, { headers: { "content-type": "multipart/form-data" } } ); return dispatch({ type: UPLOAD_USER_AVATAR_SUCCESS, res }); } catch (error) { dispatch({ type: UPLOAD_USER_AVATAR_FAILURE, error }); } };
- reducerの例
import { UPLOAD_USER_AVATAR_REQUEST, UPLOAD_USER_AVATAR_SUCCESS, UPLOAD_USER_AVATAR_FAILURE, } from "../actions/user"; const initialState = {}; export default (state = initialState, action) => { switch (action.type) { case UPLOAD_USER_AVATAR_REQUEST: return { ...state }; case UPLOAD_USER_AVATAR_SUCCESS: return { ...state, user: action.res.data.user }; case UPLOAD_USER_AVATAR_FAILURE: return { ...state, error: action.error }; default: return { ...state }; } };