65.Ajax 图片上传

未匹配的标注

本节说明

  • 对应视频教程第 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>

我们暂时做了功能的第一步:我们上传图片,头像区域会立即将浏览器端上传的图片显示出来。我们看一下效果:
file
接下来我们进行第二步,将图片上传到服务器端:

<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>

现在我们再次上传图片,已经保存到服务器端:
file
我们还需要将头像显示出来。上一节我们使用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
            };
        },
        .
        .

现在我们刷新页面:
file
但是我们现在想做些重构。我们知道,图片上传是很通用的功能,不仅头像上传会用到,其他地方也会用到。所以我们把图片上传的部分抽取成单独的组件:
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);
    }
}

现在我们再试一遍上传头像的流程,看功能是否正常。然后我们再运行一下全部测试:
file
有报错,没关系,我们一个一个进行修复。首先我们要修改话题详情页面头像路径的获取:
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);
    }
}

再次运行测试:
file

本文章首发在 LearnKu.com 网站上。

上一篇 下一篇
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
讨论数量: 1
发起讨论 查看所有版本


zh117
关于 65 节的遗漏?
1 个点赞 | 0 个回复 | 问答