【Active Storage不使用】Rails(API) x VueでCarrierWaveを使用して画像を登録・表示する

てきとうにやればできるでしょ、って甘く見積もってたら全然わかってなかったことがわかったので、記録を残しておく。

【環境】
Rails: 6.0.3.7
Vue: 2.6.11
いずれもDockerコンテナで起動


Rails単体であれば、formヘルパーやimage_tagヘルパーでよしなに画像を取り込んで表示してくれるので、画像のデータフローを意識しなくても画像投稿・表示機能を実装できる。
しかし、バックとフロントを分離してAPIでやりとりするパターンでは、Railsが包み隠してくれていた部分を自力で実装する必要がある。

今回の記事は前提として、上の通り分離しているので、Railsではviewを作らない → erbを使わないことが条件。
RailsとVueの役割分担の度合いについては種々議論があると思うが、erbの手っ取り早さに頼らないチャレンジとスキルアップが目的なのであしからず。
おかげで、全然わかってなかった状態から、少しわかる状態までもっていけた。



結論から言うと、方法としては、
JavaScriptのFormDataオブジェクトを使用して、フロント(Vue)からバックへデータを送信(axiosを使用)
→ バックでCarrierWaveを用いてAmazon S3へ画像データをアップロード
→ そのURLを属性にもつオブジェクトを格納したJSONをフロントへ返す
→ 受け取ったJSONからURLを取り出して表示


という形をとった。

画像データフロー
画像データフロー

イメージとしては、多分上のようなかんじになると思う。

Viewでは画像をオブジェクトとして扱い、FormDataに突っ込んだものをaxiosでAPIにPOSTリクエストする。
APIからデータを取得するときのheaderは「content-type: "application/json"」だが、画像をbase64エンコードせずAPIに投げるときは「content-type: "multipart/formdata"」を指定する必要がある。
今回はFormDataを使用するので、自動的にmultipart/formdataになる。ここらへんの話は↓を参考にさせてもらった。

Base64エンコードとCarrierWave | だいそんの技術メモ

FormData の使い方 - JavaScript の基本 - JavaScript 入門


ということで、ソースコードはこんなかんじに。

<template>
  <div>
    <form @submit.prevent="createPost()">
      <ul>
        <li>
          <label>post名</label>
          <input type="text" name="post.name" v-model="post.name">
        </li>
        <li>
          <label>画像</label>
          <input type="file" @change="setImage">
        </li>
        <li>
          <input type="submit" value="登録">
        </li>
      </ul>
    </form>
    # 非同期で画像表示するため、返ってきたデータをreturned_postとした。
    <img :src="returned_post.image.url" alt="postの画像">
  </div>
</template>

<script>
export default {
  data() {
    return {
      post: {
        name: "",
        image: {},
      },
      returned_post: {
        image: {},
      },
    };
  },
  methods: {
    setImage(event) {
      this.post.image = event.target.files[0]
    },
    createPost() {
      const formData = new FormData()
      formData.append("post[name]", this.post.name)
      formData.append("post[image]", this.post.image)
      this.$axios
      .post("/api/v1/posts", formData)
      .then(res => {
        this.returned_post = res.data
      })
      .catch(err => {
        console.log(err)
      })
    }
  },
};
</script>


こんなかんじで、とりあえず投稿した画像は表示できる。ここでの流れとしては、

  1. 画像添付でchangeイベントが起こり、setImageメソッド発火。this.post.imageにevent.target.files[0](つまり添付画像ファイル)を突っ込む。

  2. 登録ボタン押下でsubmitイベントが起こり、createPostメソッド発火。FormDataオブジェクトを作成してpostの属性をappend(追加)する。それをaxiosでPOSTする。

  3. バックでなんやかんや処理した後(後述)、レスポンス(JSON)が返ってくるので、別に用意したdataのオブジェクト(ここではreturned_post)に突っ込む。あとはurl属性に格納されているURLをimgタグで表示するだけ(:srcでバインドを忘れずに)。


ただこのやり方だと、参考記事にもあるように、登録したいデータ属性が増えるとその分appendしないといけないので、そこは面倒。でもbase64エンコ→デコードもめんどくさそうなので、この方法にした。


フロントは以上。次はバックの実装について。


まずはじめに、Railsの画像のアップローダーは何を使用するか。
主にActive StorageかCarrierWaveの2択のよう。どっちを使用すべきかは↓の記事等を参考にした。

ActiveStorage vs CarrierWave - Qiita


今回は後者を選択。理由はerbを使わないから。多分erb使うならActive Storageの方が手っ取り早いと思うし、調べると情報がいっぱい出てくる(今回の記事書いている理由も、Active Storage使わずにRails x Vueで実装してる情報が少ないからだったり)。

CarrierWaveでS3に画像アップロードの設定はここでは割愛。自分は↓の記事を参考した。

【Rails】 CarrierWaveチュートリアル | Pikawaka - ピカ1わかりやすいプログラミング用語サイト


ということでソースコードはこんなかんじに。記事用に必要最小限にしてるので、必要に応じて設定(エラーハンドリングとか)は追加してね。

class Api::V1::PostsController < ApplicationController

  def create
    post = Post.new(post_params)
    post.save
    render json: post
  end

  private

  def post_params
    params.require(:post).permit(
      :name,
      :image,
    )
  end
end


Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      resources :posts, only: [:create]
    end
  end


これで、フロントでPOSTリクエストが送られてくると、createアクションが走り、最初の図に書いた流れでデータが作成される…はず(自信はない)。
ポイントは、DBには画像データを直接保存せず、パス(URL:文字列)のみ保存する、データ自体はS3に保存すること。データ自体をDBに保存しちゃうと、データ量が大きくなりすぎるから。
そして、コントローラでそのURLを格納したJSONを返却する。あとは先にフロントで書いたとおり。


書いてみると割とシンプルな気がしてくるけど、これまでRailsのフォーム機能しか使ってなかったから、「あれ?そもそもHTMLのformってどんなかんじだっけ?」、「画像ってそもそも何者?どういう形式?」と混乱した。
結局、JSで画像をどう扱うかが全然わかってなかったのがわかった。もっとJSと友達になろう。