LaravelとVue.jsを利用し、画像をアップロードし表示させる機能をSPA(Single Page Application)で実装します。
作成するアプリケーションの概要
下記イラストのように、タイトルと画像を登録するアプリケーションを作成します。LaravelでAPIを作成し、フロント側はVue.jsで構築しています。本記事では画像ファイル自体のアップロード先はLaravelのプロジェクト内(ローカルストレージ)で、そのパスのみをDBに保存しています。
1,初期状態です。
2,タイトル未入力およびファイルを選択せずに「アップロード」ボタンをクリックした場合のエラーメッセージです。バリデーションはAPI側で処理されています。
3,「ファイルを選択」で画像形式以外のファイルを選択した場合のエラーメッセージです。バリデーションはVue.js側で処理しています。
4,文字は7文字以内で設定しています。タイトルのバリデーションはAPI側のみで検証しています。
5,タイトルと画像の保存先パスがDBに登録されると「登録しました!」と表示され、下に表示されます。
今回、フロント側のバリデーションは画像形式の検証時のみにおこなっています。エラー処理周りの挙動も画面仕様により変わってくるので簡易的に処置していますが、サーバー側からVue.jsへのエラーメッセージの渡し方などの一連の動きは理解できるかと思います。
API側の実装
あらかじめデータベースを用意しておいて下さい。本記事ではMySQLを利用します。.envに適当な値を設定します。
.env
1 2 3 4 5 6 |
DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=sample01 DB_USERNAME=hoge DB_PASSWORD=pass |
画像ファイルのパスとタイトルを保存するテーブルを作成します。
モデルとマイグレーションファイルを作成します。
1 |
$ php artisan make:model Image --migration |
以上でimagesテーブルに対応したモデル(app/Image.php)とマイグレーションファイルが生成されるので、それぞれ下記のように追記します(ハイライト部分)。
app/Image.php
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Image extends Model { protected $fillable = [ 'title', 'path', ]; } |
database/migrations/2020_02_23_124607_create_images_table
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateImagesTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('images', function (Blueprint $table) { $table->bigIncrements('id'); $table->string('title'); $table->string('path'); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('images'); } } |
マイグレーションを実行します。
1 |
$ php artisan migrate |
完了するとimagesテーブルが生成されているのが確認できます。
1 2 3 4 5 6 7 8 9 10 |
mysql> SHOW COLUMNS FROM images; +------------+---------------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +------------+---------------------+------+-----+---------+----------------+ | id | bigint(20) unsigned | NO | PRI | NULL | auto_increment | | title | varchar(255) | NO | | NULL | | | path | varchar(255) | NO | | NULL | | | created_at | timestamp | YES | | NULL | | | updated_at | timestamp | YES | | NULL | | +------------+---------------------+------+-----+---------+----------------+ |
コントローラーの作成
–apiを付けてコントローラーファイルを作成します。
1 |
$ php artisan make:controller ImageApiController --api |
api.phpに下記コードを追記します。/api/images/〜というエンドポイントになります。
routes/api.php
1 |
Route::apiResource('/images', 'ImageApiController'); |
設定されているルート情報を確認します。
1 2 3 4 5 6 7 8 9 10 11 12 |
$ php artisan route:list +--------+-----------+--------------------+----------------+----------------------------------------------+--------------+ | Domain | Method | URI | Name | Action | Middleware | +--------+-----------+--------------------+----------------+----------------------------------------------+--------------+ | | GET|HEAD | / | | Closure | web | | | GET|HEAD | api/images | images.index | App\Http\Controllers\ImageController@index | api | | | POST | api/images | images.store | App\Http\Controllers\ImageController@store | api | | | GET|HEAD | api/images/{image} | images.show | App\Http\Controllers\ImageController@show | api | | | PUT|PATCH | api/images/{image} | images.update | App\Http\Controllers\ImageController@update | api | | | DELETE | api/images/{image} | images.destroy | App\Http\Controllers\ImageController@destroy | api | | | GET|HEAD | api/user | | Closure | api,auth:api | +--------+-----------+--------------------+----------------+----------------------------------------------+--------------+ |
生成されたコントローラーファイルにはすでにいくつかのメソッドが用意されていますが、今回は表示と保存の機能のみを実装するので index() / store() を利用します。
app/Http/Controllers/ImageApiController.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use App\Image; class ImageApiController extends Controller { /** * Display a listing of the resource. * * @return \Illuminate\Http\Response */ public function index() { return Image::all(); } /** * Store a newly created resource in storage. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function store(Request $request) { $this->validate($request, [ 'title' => 'required|max:7', 'file' => 'required|image' ], [ 'title.required' => 'タイトルを入力して下さい', 'title.max' => '7文字以内で入力して下さい', 'file.required' => '画像が選択されていません', 'file.image' => '画像ファイルではありません', ]); if (request()->file) { $file_name = time() . '.' . request()->file->getClientOriginalName(); request()->file->storeAs('public', $file_name); $image = new Image(); $image->path = 'storage/' . $file_name; $image->title = $request->title; $image->save(); return ['success' => '登録しました!']; } } 〜以下省略 |
39行目
storeへは後述するVue.js側から、画像ファイル(file)とタイトル(title)の2つのデータが送られてきますが、getClientOriginalNameメソッドでそのファイル名を取得し、今回はタイムスタンプをその名前に付与しています。
40行目
またstoreAsメソッドでLaravelのプロジェクト内にファイルを保存しています。デフォルトでは storage/app/public ディレクトリ内に画像を保存するようにします。ただし後述するようにこのstorage/app/publicは、ウェブ(ブラウザ)からはアクセスできないのでシンボリックリンクを公開ディレクトリであるpublic/storageから貼る必要があります。
フロント側の実装
Laravelのバージョン6.xでVue.jsをはじめて利用する方は下記ページをご覧下さい。
Laravel 6 Vue.jsを利用する [axios][Laravel Mix]
まずlaravel/uiパッケージをインストールします。
1 |
$ composer require laravel/ui |
Vue.jsで構築するためのファイル等を生成します。
1 |
$ php artisan ui vue |
package.jsonの内容に依存したパッケージをインストールし、コンパイルします。
1 |
$ npm install && npm run dev |
1 |
$ npm run watch |
サーバーを起動させます。
1 |
$ php artisan serve |
画面を作成していきます。「http://localhost:8000/image」としての一画面のSPAとなります。コントローラーを作成し、ルート情報に追記、後述のようにコントローラーを編集します。
1 |
$ php artisan make:controller ImageController |
routes/web.php
1 |
Route::get('/image', 'ImageController@index'); |
app/Http/Controllers/ImageController.php
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; class ImageController extends Controller { public function index() { return view('imageup.index'); } } |
ビュー(テンプレート)を作成します。imageupディレクトリおよびindex.blade.phpを作成し下記のように記述します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<!DOCTYPE html> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <head> <meta charset="UTF-8"> <title>画像アップサンプル</title> <meta name="csrf-token" content="{{ csrf_token() }}"> </head> <body> <div id="app"> <image-component></image-component> </div> <script src="{{ mix('js/app.js') }}"></script> </body> </html> |
13行目
image-componentという名前のVueコンポーネントを表示させます。
app.jsに下記コードを追記して下さい。
resources/js/app.js
1 |
Vue.component('image-component', require('./components/ImageComponent.vue').default); |
image-componentという名前のコンポーネントに対して下記で作成するImageComponent.vueを割り当てています。
ImageComponent.vueを作成し下記のように記述します。※一番右のアイコンをクリックするとコードが別ウィンドウで開いて説明が読みやすいです。
resources/js/components/ImageComponent.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
<template> <div> <p>タイトル:<input type="text" v-model="title" /></p> <p><input type="file" @change="confirmImage" v-if="view" /></p> <!-- 確認用画像 --> <p v-if="confirmedImage"> <img class="img" :src="confirmedImage" /> </p> <p>{{ message }}</p> <p> <button @click="uploadImage">アップロード</button> </p> <!-- 画像一覧 --> <table border="1"> <tr> <th>title</th> <th>画像</th> </tr> <tr v-for="image in images" :key="image.id"> <td>{{ image.title }}</td> <td><img class="img" :src="`${image.path}`" /></td> </tr> </table> </div> </template> <script> export default { data() { return { message: "", file: "", title: "", view: true, images: {}, confirmedImage: "" }; }, created: function() { this.getImage(); }, methods: { getImage() { axios .get("/api/images/") .then(response => { this.images = response.data; }) .catch(err => { this.message = err; }); }, confirmImage(e) { this.message = ""; this.file = e.target.files[0]; if (!this.file.type.match("image.*")) { this.message = "画像ファイルを選択して下さい"; this.confirmedImage = ""; return; } this.createImage(this.file); }, createImage(file) { let reader = new FileReader(); reader.readAsDataURL(file); reader.onload = e => { this.confirmedImage = e.target.result; }; }, uploadImage() { let data = new FormData(); data.append("file", this.file); data.append("title", this.title); axios .post("/api/images/", data) .then(response => { this.getImage(); this.message = response.data.success; this.confirmedImage = ""; this.title = ""; this.file = ""; //ファイルを選択のクリア this.view = false; this.$nextTick(function() { this.view = true; }); }) .catch(err => { this.message = err.response.data.errors; }); } } }; </script> <style> .img { width: 100px; } </style> |
4行目
68〜72行目
FileReaderのインスタンスを作成しfileを読み込んでいます。画像の場合はreadAsDataURLを利用し読み込みます。読み込み完了と同時にonloadが発動します。
75〜77行目
Laravel側のapiへPOSTするデータとしてFormData オブジェクトを利用し、fileと画像データ/titleとタイトルというキーと値のペアを作成しています。
79〜96行目
axiosに関しては下記ページをご覧下さい。登録が成功するとgetImage()によってデータが画面下に表示されます。
コンポーネントの実装は以上です。最後にシンボリックリンクを貼って完了です。
シンボリックリンク
Laravelのデフォルト設定では、ファイルはstorage/app/public以下に保存し、ここにウェブ(ブラウザ)からアクセスできるようにします。そのためには公開ディレクトリであるpublic/storageから storage/app/public へシンボリックリンクを貼る必要があります。
下記コマンドを実行します。
1 |
$ php artisan storage:link |
コマンドを実行すると下記URLのようにして画像が確認できます。
1 |
http://localhost:8000/storage/1582298494.hoge.png |
本記事では説明はしませんが、これらファイルストレージの設定は config/filesystems.php(.env)に記述されていて、AWSのS3等へのファイル保存の設定も可能です。
関連ページ
参照ページ