Vue on RailsでActive Storageを使って画像を保存する

# はじめに
Rails API モードと Vue.js で作成した [自作ブログ](https://log.tabakazu.com) で Active Storage を使う際に、画像の受け渡しでハマったので実装方法を残します。
実装するのは Active Storage を使って、eyecatch (アイキャッチ画像) 付きの Post (記事) を投稿できるようなサンプルです。

# 環境
```bash
$ ruby -v
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin16]

$ bundle exec rails --version
Rails 5.2.2
```

# 実装方法
## API 部分 (Rails)
### Ⅰ. サンプルアプリケーション新規作成
Rails API モードで作成します。

```bash
$ bundle exec rails new sampleapp --database=mysql --api --force
$ cd sampleapp/
$ bundle exec rails db:create
```

### Ⅱ. Active Storage インストール
Active Storage をインストールします。

```bash
$ bundle exec rails active_storage:install
$ bundle exec rails db:migrate
```

### Ⅲ. Post リソースの作成
今回利用する Post モデルとコントローラを作成します。

```bash
$ bundle exec rails g resource post title body:text
$ bundle exec rails db:migrate
```

### Ⅳ. 各種ファイルの修正
各 Post に eyecatch を設定できるようにモデルを修正します。
追加する eyecatch= メソッドで、Active Storage に画像を保存します。

このメソッドは、Base64 形式で受け取った image データをエンコードし、一時的に /tmp 配下に画像ファイルを作成、作成した画像ファイルをアタッチ、その後画像ファイルを削除します。

```ruby:app/models/post.rb
class Post < ApplicationRecord
  has_one_attached :eyecatch
  attr_accessor :image

  def eyecatch=(image)
    if image.present?
      prefix = image[/(image|application)(\/.*)(?=\;)/]
      type = prefix.sub(/(image|application)(\/)/, '')
      data = Base64.decode64(image.sub(/data:#{prefix};base64,/, ''))
      filename = "#{Time.zone.now.strftime('%Y%m%d%H%M%S%L')}.#{type}"
      File.open("#{Rails.root}/tmp/#{filename}", 'wb') do |f|
        f.write(data)
      end
      eyecatch.detach if eyecatch.attached?
      eyecatch.attach(io: File.open("#{Rails.root}/tmp/#{filename}"), filename: filename)
      FileUtils.rm("#{Rails.root}/tmp/#{filename}")
    end
  end
end
```

モデルで追加した eyecatch= メソッドに POST で受け取る画像のパラメータを渡すように修正します。

```ruby:app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def create
    post = Post.new(post_params)

    if post.save
      post.eyecatch = post_params[:image]
      render json: post, status: :created
    else
      render json: post.errors, status: :unprocessable_entity
    end
  end

  private

  def post_params
    params.require(:post).permit(:title, :body, :image)
  end
end
```


### Ⅴ. 動作確認
アプリケーションを起動し、curl コマンドでアイキャッチ付き Post が作成できるか確認します。

```bash
$ bundle exec rails s

$ curl -v -H "Accept: application/json" -H "Content-type: application/json" -X POST -d '{"post": {"title": "Sample title.", "body": "Sample body.", "image": "data:application/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAKCAYAAACALL/6AAAACXBIWXMAADXU\nAAA11AFeZeUIAAAAgUlEQVQYlZWQMQ6DMAxFnyMGJFZCc4be/yisRYyVmDN0\niTuQULCCoH9z/PLtb/hTsi8SaA1yO85ZWHxgAqb8HuVoJAX+iKPtB17L++D+\nyN6drpOa0mj7AYBg1pmz99NmSKDiVzzmKTM/uOTYMjgQNetYuKoEqj7oCHp2\nteqn2/CVvuDZJy0n3DrVAAAAAElFTkSuQmCC\n"}}' http://localhost:3000/posts
```

curl コマンドで作成後、Rails Console でアイキャッチが正常に追加できているか確認します。

```bash
$ bundle exec rails c
irb(main):001:0> Post.find_by(title: "Sample title.").eyecatch.attached?
=> true

# 画像のパスは以下のように取得できます。
# irb(main):002:0> app.url_for(Post.find_by(title: "Sample title.").eyecatch)
# => "画像のパス"
```

## フロント部分 (Vue.js)
### Ⅰ. Webpacker をインストール
webpacker gem を追加する。

```ruby:Gemfile

gem 'webpacker', '~> 3.5'
```

Webpacker をインストールします。

```bash
$ bundle
$ bundle exec rails webpacker:install
```

### Ⅱ. Vue.js をインストール
Webpacker で Vue.js をインストールします。

```bash
$ bundle exec rails webpacker:install:vue
```

### Ⅲ. Home ページを作成
Vue.js を返すための Home ページを作成します。

```bash
$ bundle exec rails g controller Pages Home
```

Root パスに Home ページを設定します。

```ruby:config/routes.rb
Rails.application.routes.draw do

  root 'pages#home'

end
```

標準の JSON ではなく、ERB を返すために ActionController::Base に修正します。

```ruby:app/controllers/pages_controller.rb
class PagesController < ActionController::Base
  def home
  end
end
```

Home ページに利用する View を作成します。

```bash
$ mkdir -p app/views/pages/
$ touch app/views/pages/home.html.erb
```

### Ⅳ. Vue.js を利用

Vue.js を利用するための設定を行います。

```erb:app/views/pages/home.html.erb
<%= javascript_pack_tag 'main' %>
```

利用する各種ファイルを作成します。

```bash
$ touch app/javascript/packs/main.js
$ touch app/javascript/packs/App.vue
```

```js:app/javascript/packs/main.js
import Vue from 'vue'
import App from './App.vue'

document.addEventListener('DOMContentLoaded', () => {
  const el = document.body.appendChild(document.createElement('main'))
  new Vue({
    el,
    render: h => h(App)
  })
})
```

```vue:app/javascript/packs/App.vue
<template>
  <div id="app">
    <p>投稿フォーム</p>
  </div>
</template>
```

### Ⅴ. 投稿フォームを作成
API コールに利用する axios をインストールします。

```bash
$ yarn add axios
```

画像投稿するフォームを用意します。
画像は POST する前に Base64 にデコードしています。

```vue:app/javascript/packs/App.vue
<template>
  <div id="app">
    <p>投稿フォーム</p>
    <form v-on:submit.prevent="postItem()">
      <p>
        <label>Title</label>
        <input name="post.title" type="text" v-model="post.title"><br />
      </p>
      <p>
        <label>Body</label>
        <input name="post.body" type="text" v-model="post.body"><br />
      </p>
      <p>
        <label>画像</label>
        <input name="uploadedImage" type="file" ref="file" v-on:change="onFileChange()"><br />
      </p>
      <input type="submit" value="Submit">
    </form>
  </div>
</template>

<script>
import axios from 'axios'

export default {
  data() {
    return {
      post: {},
      uploadedImage: ''
    }
  },
  methods: {
    onFileChange() {
      let file = event.target.files[0] || event.dataTransfer.files
      let reader = new FileReader()
      reader.onload = () => {
        this.uploadedImage = event.target.result
        this.post.image = this.uploadedImage
      }
      reader.readAsDataURL(file)
    },
    postItem() {
      return new Promise((resolve, _) => {
        axios({
          url: '/posts',
          data: {
            post: this.post
          },
          method: 'POST'
        }).then(res => {
          this.post = {}
          this.uploadedImage = ''
          this.$refs.file.value = ''
          resolve(res)
        }).catch(e => {
          console.log(e)
        })
      })
    }
  }
}
</script>
```

### Ⅵ. 動作確認

アプリケーションを起動し、投稿フォームから画像を投稿します。
http://localhost:3000/

```bash
$ bundle exec rails s
```

<img width="626" alt="img1.png" src="https://qiita-image-store.s3.amazonaws.com/0/197676/8b192f67-25c4-14f8-fbf8-d150a4da8dd9.png">

Rails Console でアイキャッチが正常に追加できているか確認します。

```bash
$ bundle exec rails c
irb(main):001:0> Post.last.eyecatch.attached?
=> true
```

# 参考記事
https://qiita.com/ozin/items/5ec81a4b126b8ebf7a96

# 最後に
読んでいただいてありがとうございます。
間違っている点などがありましたら、ご指摘いただけると喜びます！