はじめに
Rails API モードと Vue.js で作成した 自作ブログ で Active Storage を使う際に、画像の受け渡しでハマったので実装方法を残します。
実装するのは Active Storage を使って、eyecatch (アイキャッチ画像) 付きの Post (記事) を投稿できるようなサンプルです。
環境
$ 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 モードで作成します。
$ bundle exec rails new sampleapp --database=mysql --api --force
$ cd sampleapp/
$ bundle exec rails db:create
Ⅱ. Active Storage インストール
Active Storage をインストールします。
$ bundle exec rails active_storage:install
$ bundle exec rails db:migrate
Ⅲ. Post リソースの作成
今回利用する Post モデルとコントローラを作成します。
$ bundle exec rails g resource post title body:text
$ bundle exec rails db:migrate
Ⅳ. 各種ファイルの修正
各 Post に eyecatch を設定できるようにモデルを修正します。
追加する eyecatch= メソッドで、Active Storage に画像を保存します。
このメソッドは、Base64 形式で受け取った image データをエンコードし、一時的に /tmp 配下に画像ファイルを作成、作成した画像ファイルをアタッチ、その後画像ファイルを削除します。
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 で受け取る画像のパラメータを渡すように修正します。
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 が作成できるか確認します。
$ 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 でアイキャッチが正常に追加できているか確認します。
$ 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 を追加する。
gem 'webpacker', '~> 3.5'
Webpacker をインストールします。
$ bundle
$ bundle exec rails webpacker:install
Ⅱ. Vue.js をインストール
Webpacker で Vue.js をインストールします。
$ bundle exec rails webpacker:install:vue
Ⅲ. Home ページを作成
Vue.js を返すための Home ページを作成します。
$ bundle exec rails g controller Pages Home
Root パスに Home ページを設定します。
Rails.application.routes.draw do
root 'pages#home'
end
標準の JSON ではなく、ERB を返すために ActionController::Base に修正します。
class PagesController < ActionController::Base
def home
end
end
Home ページに利用する View を作成します。
$ mkdir -p app/views/pages/
$ touch app/views/pages/home.html.erb
Ⅳ. Vue.js を利用
Vue.js を利用するための設定を行います。
<%= javascript_pack_tag 'main' %>
利用する各種ファイルを作成します。
$ touch app/javascript/packs/main.js
$ touch app/javascript/packs/App.vue
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)
})
})
<template>
<div id="app">
<p>投稿フォーム</p>
</div>
</template>
Ⅴ. 投稿フォームを作成
API コールに利用する axios をインストールします。
$ yarn add axios
画像投稿するフォームを用意します。
画像は POST する前に Base64 にデコードしています。
<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/
$ bundle exec rails s
Rails Console でアイキャッチが正常に追加できているか確認します。
$ bundle exec rails c
irb(main):001:0> Post.last.eyecatch.attached?
=> true
参考記事
最後に
読んでいただいてありがとうございます。
間違っている点などがありましたら、ご指摘いただけると喜びます!