こんにちは!kossyです!
さて、今回はRails6 API + Vue.js3 + TypeScript で画像アップロード機能の実装手順をブログに残してみたいと思います。
環境
Ruby 2.6.6
Rails 6.0.3.4
Docker for Mac
Vue 3.0.5
Vue CLI 4.5.9
TypeScript 3.9.7
なお、既にRailsAPIモードでのApp作成やVue.js Appの作成は済んでいるものとし、拡張子制限等の考慮はしないものとします。
API側
今回はActiveStorageを使って画像アップロード機能を実現させたいと思います。(導入は済んでいるものとします)
任意のmodel(今回はPostModelとします)に画像を保存するフィールドを定義します。
class Post < ApplicationRecord belongs_to :user has_one_attached :icatch end
任意のcontroller(例としてPostsControllerとします)に画像を保存する処理を追加します。
class PostsController < ApplicationController before_action :set_post, only: [:show, :update] def show end def create @post = Post.new(post_params) if @post.save render :show, status: :created else render json: @post.errors, status: :unprocessable_entity end end private def set_post @post = Post.find(params[:id]) end def post_params params.permit(:title, :body, :icatch).merge(user: current_user) end
Vue側からicatch keyに画像が格納されて送信されてくる想定をしています。
保存された画像をBase64エンコードしたものをクライアントに送るメソッドも定義します。
class Post < ApplicationRecord belongs_to :user has_one_attached :icatch def encoded_icatch "data:image/png;base64,#{Base64.encode64(icatch.download)}" if icatch.attached? end end
このメソッドをviewから呼び出します。
# app/views/posts/show.json.jbuiler json.partial! "posts/post", post: @post
# app/views/posts/_post.json.jbuilder json.extract! post, :id, :title, :body, :user_id, :created_at json.encoded_icatch post.encoded_icatch
最後にroutingの定義をします。
Rails.application.routes.draw do scope format: 'json' do resources :users do resources :posts, only: [:show, :create] end end end
これでAPI側の準備は完了です。
Vue側
Vue側はAPIとの通信にAxiosを用いることとします。
まずはAPI側にPostリクエストをするコードを記述します。
// src/api/client.ts import axios from 'axios' export default axios.create({ baseURL: process.env.VUE_APP_API_BASE }) // .env.development VUE_APP_API_BASE=http://localhost:3000 // src/api/post.ts export const createPost = async (id: integer, formData: any) => { return await Client.post( 'users/:id/posts', formData, { headers: { 'Content-Type': 'multipart/form-data' } } ) .then((response) => { return response.data }) }
このコードをcomponentから呼び出すようにします。今回はviews/NewPost.vueから呼び出すこととします。(tailwind cssを使っています)
<template> <div class="flex items-center h-screen w-full bg-teal-lighter"> <div class="w-full bg-white rounded shadow-lg p-8 m-4"> <h1 class="block w-full text-center text-grey-darkest mb-6">New Post</h1> <div class="flex flex-col mb-4"> <label class="mb-2 font-bold text-lg text-grey-darkest" for="title">Title</label> <input v-model='title' class="border py-2 px-3 text-grey-darkest" type="text" name="title" id="title"> </div> <div class="flex flex-col mb-4"> <label class="mb-2 font-bold text-lg text-grey-darkest" for="body">Body</label> <textarea v-model='body' class="border py-2 px-3 text-grey-darkest" name="body" id="body"></textarea> </div> <div class="flex flex-col mb-4"> <label class="mb-2 font-bold text-lg text-grey-darkest" for="icatch">iCatch</label> <input @change="setIcatch($event)" accept="image/png,image/jpeg" class="border py-2 px-3 text-grey-darkest" type="file"> </div> <button @click='handleCreatePost()' class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 uppercase text-lg mx-auto rounded" type="submit">Create Post</button> </div> </div> </template> <script lang="ts"> import { createPost } from '@/api/post' import { defineComponent, reactive, toRefs } from 'vue' export default defineComponent({ name: 'NewPost', setup () { const postData = reactive({ title: '', body: '' }) const formData = new FormData() const setIcatch = (e: Event) => { e.preventDefault() if (e.target instanceof HTMLInputElement && e.target.files) { formData.append('icatch', e.target.files[0]) } } return { ...toRefs(postData), setIcatch, handleCreatePost: async () => { formData.append('title', postData.title) formData.append('body', postData.body) await createPost(formData) .then((data) => { console.log(data) // indexページやshowページに遷移するといい }) } } } }) </script> <style scoped> </style>
Vue.js 3系から導入されたreactive関数を使ってプロパティをリアクティブにし、template内の変更を受け取れるようにしています。
画像の添付はformDataオブジェクトを使って実現しています。
ネットに落ちている情報だと、
<input @change="setIcatch()" accept="image/png,image/jpeg" class="border py-2 px-3 text-grey-darkest" type="file">
changeの箇所に$eventを記載しない例が散見されましたが、私の環境だと$eventを引数として与えないとうまくイベントを検知してくれませんでした。
テンプレートの表示は概ねこのようになるかと。
APIとの通信に成功すると以下の返り値を得られます。
{ "id": 9, "title": "Vue + RailsのSPA", "body": "画像添付のテスト", "user_id": 1, "created_at": "2021-03-07T11:51:01.604+09:00", "encoded_icatch": "data:image/png;base64,iVBORw0KGgo..." }
後はこの返り値を表示すればOKです。
勉強になりました。