65.Ajax 图片上传
- 本系列文章为
laracasts.com
的系列视频教程——Let's Build A Forum with Laravel and TDD 的学习笔记。若喜欢该系列视频,可去该网站订阅后下载该系列视频, 支持正版 ;- 视频源码地址:github.com/laracasts/Lets-Build-a-...;
- 本项目为一个 forum(论坛)项目,与本站的第二本实战教程 《Laravel 教程 - Web 开发实战进阶》 类似,可互相参照。
本节说明
- 对应视频教程第 65 小节:AJAX Image Uploads
本节内容
本节我们来把上传头像的部分抽取成 Vue 组件,然后利用 Ajax 的方式上传头像。首先我们新建 Vue 组件:
forum\resources\assets\js\components\AvatarForm.vue
<template>
<div>
<h1 v-text="user.name"></h1>
<form v-if="canUpdate" method="POST" enctype="multipart/form-data">
<input type="file" name="avatar" accept="image/*" @change="onChange">
</form>
<img :src="avatar" width="200" height="200">
</div>
</template>
<script>
export default {
props: ['user'],
data() {
return {
avatar:''
};
},
computed: {
canUpdate() {
return this.authorize(user => user.id === this.user.id)
}
},
methods: {
onChange(e){
if (! e.target.files.length) return;
let file = e.target.files[0];
let reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = e => {
this.avatar = e.target.result;
};
}
}
}
</script>
接着我们还需要注册组件:
forum\resources\assets\js\app.js
.
.
Vue.component('flash', require('./components/Flash.vue'));
Vue.component('paginator', require('./components/Paginator.vue'));
Vue.component('user-notifications', require('./components/UserNotifications.vue'));
Vue.component('avatar-form', require('./components/AvatarForm'));
Vue.component('thread-view', require('./pages/Thread.vue'));
const app = new Vue({
el: '#app'
});
然后我们将组件应用到个人页面:
resources\views\profiles\show.blade.php
<div class="page-header">
<avatar-form :user="{{ $profileUser }}"></avatar-form>
</div>
我们暂时做了功能的第一步:我们上传图片,头像区域会立即将浏览器端上传的图片显示出来。我们看一下效果:
接下来我们进行第二步,将图片上传到服务器端:
<template>
<div>
<h1 v-text="user.name"></h1>
<form v-if="canUpdate" method="POST" enctype="multipart/form-data">
<input type="file" name="avatar" accept="image/*" @change="onChange">
</form>
<img :src="avatar" width="200" height="200">
</div>
</template>
<script>
export default {
props: ['user'],
data() {
return {
avatar:''
};
},
computed: {
canUpdate() {
return this.authorize(user => user.id === this.user.id)
}
},
methods: {
onChange(e){
if (! e.target.files.length) return;
let avatar = e.target.files[0];
let reader = new FileReader();
reader.readAsDataURL(avatar);
reader.onload = e => {
this.avatar = e.target.result;
};
this.persist(avatar);
},
persist(avatar) {
let data = new FormData();
data.append('avatar',avatar);
axios.post(`/api/users/${this.user.name}/avatar`,data)
.then(() => flash('Avatar uploaded!'));
}
}
}
</script>
现在我们再次上传图片,已经保存到服务器端:
我们还需要将头像显示出来。上一节我们使用avatar()
获取头像,但是这并不适用于现在的情形。我们把avatar()
方法修改为访问器:
forum\app\User.php
.
.
public function getAvatarPathAttribute($avatar)
{
return $avatar ?: 'avatars/default.jpg';
}
public function visitedThreadCacheKey($thread)
{
return $key = sprintf("users.%s.visits.%s",$this->id,$thread->id);
}
}
现在我们可以在组件设定初始值:
template>
<div>
<h1 v-text="user.name"></h1>
<form v-if="canUpdate" method="POST" enctype="multipart/form-data">
<input type="file" name="avatar" accept="image/*" @change="onChange">
</form>
<img :src="avatar" width="200" height="200">
</div>
</template>
<script>
export default {
props: ['user'],
data() {
return {
avatar:'/storage/'+this.user.avatar_path
};
},
.
.
现在我们刷新页面:
但是我们现在想做些重构。我们知道,图片上传是很通用的功能,不仅头像上传会用到,其他地方也会用到。所以我们把图片上传的部分抽取成单独的组件:
forum\resources\assets\js\components\ImageUpload.vue
<template>
<input type="file" accept="image/*" @change="onChange">
</template>
<script>
export default {
methods: {
onChange(e){
if (! e.target.files.length) return;
let file = e.target.files[0];
let reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = e => {
let src = e.target.result;
this.$emit('loaded',{ src,file });
};
}
}
}
</script>
然后引入该组件:
forum\resources\assets\js\components\AvatarForm.vue
<template>
<div>
<div class="level">
<img :src="avatar" width="50" height="50">
<h1 v-text="user.name"></h1>
</div>
<form v-if="canUpdate" method="POST" enctype="multipart/form-data">
<image-upload name="avatar" class="mr-1" @loaded="onLoad"></image-upload>
</form>
</div>
</template>
<script>
import ImageUpload from './ImageUpload.vue';
export default {
props: ['user'],
components: { ImageUpload },
data() {
return {
avatar:'/storage/'+this.user.avatar_path
};
},
computed: {
canUpdate() {
return this.authorize(user => user.id === this.user.id)
}
},
methods: {
onLoad(avatar){
this.avatar = avatar.src;
this.persist(avatar.file);
},
persist(avatar) {
let data = new FormData();
data.append('avatar',avatar);
axios.post(`/api/users/${this.user.name}/avatar`,data)
.then(() => flash('Avatar uploaded!'));
}
}
}
</script>
既然现在我们是 Ajax 的方式上传头像,那么我们可以修改控制器,不用返回重定向,返回 json 响应即可:
forum\app\Http\Controllers\Api\UserAvatarController.php
<?php
namespace App\Http\Controllers\Api;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class UserAvatarController extends Controller
{
public function store()
{
$this->validate(request(),[
'avatar' => ['required','image']
]);
auth()->user()->update([
'avatar_path' => request()->file('avatar')->store('avatars','public')
]);
return response([],204);
}
}
现在我们再试一遍上传头像的流程,看功能是否正常。然后我们再运行一下全部测试:
有报错,没关系,我们一个一个进行修复。首先我们要修改话题详情页面头像路径的获取:
forum\resources\views\threads\show.blade.php
.
.
<div class="panel-heading">
<div class="level">
<img src="/storage/{{ $thread->creator->avatar_path }}" alt="{{ $thread->creator->name }}" width="25" height="25" class="mr-1">
<span class="flex">
<a href="{{ route('profile',$thread->creator) }}">{{ $thread->creator->name }}</a> posted:
{{ $thread->title }}
</span>
@can('update',$thread)
<form action="{{ $thread->path() }}" method="POST">
{{ csrf_field() }}
{{ method_field('DELETE') }}
<button type="submit" class="btn btn-link">Delete Thread</button>
</form>
@endcan
</div>
</div>
.
.
接着我们修改a_user_can_determine_their_avatar_path
测试:
forum\tests\Unit\UserTest.php
.
.
/** @test */
public function a_user_can_determine_their_avatar_path()
{
$user = create('App\User');
$this->assertEquals('avatars/default.jpg',$user->avatar_path);
$user->avatar_path = 'avatars/me.jpg';
$this->assertEquals('avatars/me.jpg',$user->avatar_path);
}
}
再次运行测试: