Menu

36. Blade 模板抽取成 Vue 组件

本节说明

  • 对应视频第 36 小节:Extracting Components With Blade Functionality

本节内容

我们来看一下我们的话题详情页面:
file
对于每一个回复,我们都有一个Reply.vue组件。本节我们将整个详情页面都抽取成 Vue 的组件:首先我们需要一个Thread.vue组件,用来表示完整的话题详情页面;接着我们将每个回复组件放进Replies.vue组件,表示所有回复。
首先新建Thread.vue组件:
forum\resources\assets\js\pages\Thread.vue

<script>
    import Replies from '../components/Replies';

    export default {
        components: { Replies }
    }
</script>

接着注册Thread.vue组件,同时取消注册Reply.vue组件,因为我们会在Replies.vue组件中引用它:
forum\resources\assets\js\app.js

.
.
Vue.component('flash', require('./components/Flash.vue'));

Vue.component('thread-view', require('./pages/Thread.vue'));
.
.

然后新建Replies.vue组件:
forum\resources\assets\js\components\Replies.vue

<template>
    <div>
        <div v-for="reply in items">
            <reply :data="reply"></reply>
        </div>
    </div>
</template>

<script>
    import Reply from './Reply';

    export default {
        props: ['data'],

        components: { Reply },

        data() {
            return {
                items:this.data
            }
        }
    }
</script>

修改Reply.vue组件:

<template>
    <div :id="'reply'+id" class="panel panel-default">
        <div class="panel-heading">
            <div class="level">
                <h5 class="flex">
                    <a :href="'/profiles/'+data.owner.name"
                        v-text="data.owner.name">
                    </a>said {{ data.created_at }}...
                </h5>

                <!--@if(Auth::check())-->
                <!--<div>-->
                    <!--<favorite :reply="{{ $reply }}"></favorite>-->
                <!--</div>-->
                <!--@endif-->
            </div>
        </div>

        <div class="panel-body">
            <div v-if="editing">
                <div class="form-group">
                    <textarea class="form-control" v-model="body"></textarea>
                </div>

                <button class="btn btn-xs btn-primary" @click="update">Update</button>
                <button class="btn btn-xs btn-link" @click="editing = false">Cancel</button>
            </div>

            <div v-else v-text="body"> </div>
        </div>

        <!--@can('update',$reply)-->
            <!--<div class="panel-footer level">-->
                <!--<button class="btn btn-xs mr-1" @click="editing = true">Edit</button>-->
                <!--<button class="btn btn-xs btn-danger mr-1" @click="destroy">Delete</button>-->
            <!--</div>-->
        <!--@endcan-->
    </div>
</template>
<script>
    import Favorite from './Favorite.vue';

    export default {
        props: ['data'],

        components: { Favorite },

        data() {
            return {
              editing: false,
              id: this.data.id,
              body: this.data.body
            };
        },

        methods:{
            update() {
                axios.patch('/replies/' + this.data.id,{
                    body:this.body
                });

                this.editing = false;

                flash('Updated!');
            },

            destroy() {
                axios.delete('/replies/' + this.data.id);

                $(this.$el).fadeOut(300, () => {
                    flash('Your reply has been deleted!');
                });
            }
        }
    }
</script>

我们把reply视图中内容放到了组件当中,最后我们在详情页面引入thread-view.vuereplies.vue组件:
forum\resources\views\threads\show.blade.php

@extends('layouts.app')

@section('content')
    <thread-view inline-template>
        <div class="container">
            <div class="row">
                <div class="col-md-8">
                    <div class="panel panel-default">
                        <div class="panel-heading">
                            <div class="level">
                            <span class="flex">
                                <a href="{{ route('profile',$thread->creator) }}">{{ $thread->creator->name }}</a>
                                {{ $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>

                        <div class="panel-body">
                            {{ $thread->body }}
                        </div>
                    </div>

                    <replies :data="{{ $thread->replies }}"></replies>

                    {{--@foreach ($replies as $reply)--}}
                        {{--@include('threads.reply')--}}
                    {{--@endforeach--}}

                    {{--{{ $replies->links() }}--}}

                    @if (auth()->check())
                        <form method="post" action="{{ $thread->path() . '/replies' }}">

                            {{ csrf_field() }}

                            <div class="form-group">
                                <textarea name="body" id="body" class="form-control" placeholder="说点什么吧..."rows="5"></textarea>
                            </div>

                            <button type="submit" class="btn btn-default">提交</button>
                        </form>
                    @else
                        <p class="text-center">请先<a href="{{ route('login') }}">登录</a>,然后再发表回复 </p>
                    @endif
                </div>

                <div class="col-md-4">
                    <div class="panel panel-default">
                        <div class="panel-body">
                            <p>
                                <a href="#">{{ $thread->creator->name }}</a> 发布于 {{ $thread->created_at->diffForHumans() }},
                                当前共有 <span v-text="repliesCount"></span> 个回复。
                            </p>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </thread-view>
@endsection

编译后我们刷新页面:
file
可以看到我们的组件已经应用成功,接下来我们来完善组件。我们知道,每个reply都是一个组件,且包含在父组件Replies.vue中。那么我们现在的回复删除动作就可以更改为:给自己绑定删除事件,让父组件监听到,然后父组件重新渲染区域,达到删除回复的目的。首先我们去掉删除按钮的注释,并且绑定删除事件:
forum\resources\assets\js\components\Reply.vue

.
.
<div class="panel-footer level">
    <button class="btn btn-xs mr-1" @click="editing = true">Edit</button>
    <button class="btn btn-xs btn-danger mr-1" @click="destroy">Delete</button>
</div>
.
.
destroy() {
    axios.delete('/replies/' + this.data.id);

    this.$emit('deleted',this.data.id);

    // $(this.$el).fadeOut(300, () => {
    //     flash('Your reply has been deleted!');
    // });
}
.
.

接着在父组件中进行监听:
forum\resources\assets\js\components\Replies.vue

<template>
    <div>
        <div v-for="(reply ,index) in items">
            <reply :data="reply" @deleted="remove(index)"></reply>
        </div>
    </div>
</template>

<script>
    import Reply from './Reply';

    export default {
        props: ['data'],

        components: { Reply },

        data() {
            return {
                items:this.data
            }
        },

        methods: {
            remove(index) {
                this.items.splice(index,1);

                flash('Reply has been deleted!');
            }
        }
    }
</script>

测试一下:
file
接下来我们来关联侧边栏的统计数据:
forum\resources\views\threads\show.blade.php

@extends('layouts.app')

@section('content')
    <thread-view :initial-replies-count="{{ $thread->replies_count }}" inline-template>
    .
    .
    <replies :data="{{ $thread->replies }}" @removed="repliesCount--"></replies>
    .
    .

我们给Thread.vue组件绑定了initialRepliesCount属性,用来初始化回复的数量,同时给Replies.vue组件绑定了removed事件,一旦该事件被监听到,initialRepliesCount就会减少 1 个。为组件设置属性:
forum\resources\assets\js\pages\Thread.vue

<script>
    import Replies from '../components/Replies';

    export default {
        props: ['initialRepliesCount'],

        components: { Replies },

        data() {
            return {
                repliesCount:this.initialRepliesCount
            }
        }
    }
</script>

监听removed事件:
forum\resources\assets\js\components\Replies.vue

<template>
    <div>
        <div v-for="(reply ,index) in items">
            <reply :data="reply" @deleted="remove(index)"></reply>
        </div>
    </div>
</template>

<script>
    import Reply from './Reply';

    export default {
        props: ['data'],

        components: { Reply },

        data() {
            return {
                items:this.data
            }
        },

        methods: {
            remove(index) {
                this.items.splice(index,1);

                this.$emit('removed');

                flash('Reply has been deleted!');
            }
        }
    }
</script>

注:为了与教程一致,现将Carbon的中文支持取消。

测试一下:
file
别忘了我们的点赞功能。我们在之前的设定是,点赞动作只能登录用户进行。所以我们还要做权限控制:
forum\resources\views\layouts\app.blade.php

.
.
<script>
    window.App = {!! json_encode([
        'csrfToken' => csrf_token(),
        'signIn' => Auth::check()
    ]) !!};
</script>
.
.

现在我们可以根据window.AppsignIn属性来判断用户是否登录:
file
接下来在引入favorite.vue组件时加上权限控制:
forum\resources\assets\js\components\Reply.vue

<template>
    <div :id="'reply'+id" class="panel panel-default">
        <div class="panel-heading">
            <div class="level">
                <h5 class="flex">
                    <a :href="'/profiles/'+data.owner.name"
                        v-text="data.owner.name">
                    </a> said {{ data.created_at }}...
                </h5>

                <div v-if="signIn">
                    <favorite :reply="data"></favorite>
                </div>
            </div>
        </div>

        <div class="panel-body">
            <div v-if="editing">
                <div class="form-group">
                    <textarea class="form-control" v-model="body"></textarea>
                </div>

                <button class="btn btn-xs btn-primary" @click="update">Update</button>
                <button class="btn btn-xs btn-link" @click="editing = false">Cancel</button>
            </div>

            <div v-else v-text="body"> </div>
        </div>

        <div class="panel-footer level" >
            <button class="btn btn-xs mr-1" @click="editing = true">Edit</button>
            <button class="btn btn-xs btn-danger mr-1" @click="destroy">Delete</button>
        </div>

    </div>
</template>
<script>
    import Favorite from './Favorite.vue';

    export default {
        props: ['data'],

        components: { Favorite },

        data() {
            return {
              editing: false,
              id: this.data.id,
              body: this.data.body
            };
        },

        computed: {
            signIn() {
                return window.App.signIn;
            },
        },

        methods:{
            update() {
                axios.patch('/replies/' + this.data.id,{
                    body:this.body
                });

                this.editing = false;

                flash('Updated!');
            },

            destroy() {
                axios.delete('/replies/' + this.data.id);

                this.$emit('deleted',this.data.id);
            }
        }
    }
</script>

进行测试,已成功生效:
file
接着我们给回复的编辑、删除按钮加上权限控制:
forum\resources\views\layouts\app.blade.php

.
.
<script>
    window.App = {!! json_encode([
        'csrfToken' => csrf_token(),
        'user' => Auth::user(),
        'signIn' => Auth::check()
    ]) !!};
</script>
.
.

如下可见:
file
在组件中加上权限控制:
forum\resources\assets\js\components\Reply.vue

.
.
<div class="panel-footer level" v-if="canUpdate">
    <button class="btn btn-xs mr-1" @click="editing = true">Edit</button>
    <button class="btn btn-xs btn-danger mr-1" @click="destroy">Delete</button>
</div>
.
.
computed: {
    signIn() {
        return window.App.signIn;
    },

    canUpdate() {
        return this.data.user_id == window.App.user.id;
    }
},
.
.

刷新页面可以看到权限控制已经加上:
file
但是我们现在所做的权限控制不利于扩展。例如,对于Admin而言,应该拥有所有权限。如果要引入Admin的概念,我们必须重写所有权限控制的代码。我们来进行一下改造:
forum\resources\assets\js\bootstrap.js

.
.
window.Vue = require('vue');

Vue.prototype.authorize = function (hander) {
  // if Admin,just return true

  return hander(window.App.user);
};
.
.

在组件中应用:
forum\resources\assets\js\components\Reply.vue

.
.
computed: {
    signIn() {
        return window.App.signIn;
    },

    canUpdate() {
        return this.authorize(user => this.data.user_id == user.id);
    }
},
.
.

我们调用authorize方法,并传递user => this.data.user_id == user.id参数,然后函数返回处理规则的结果,即此处的this.data.user_id == user.id。但是现在还有一个问题,那就是对于未登录用户而言,此处的代码会报错:
file
我们增加未登录用户的处理逻辑:

.
.
Vue.prototype.authorize = function (hander) {
  // if Admin,just return true
  let user = window.App.user;

  if(! user) return false;

  return hander(user);
};
.
.

还可以更简洁一些:

.
.
Vue.prototype.authorize = function (hander) {
  // if Admin,just return true
  let user = window.App.user;

  return user ? hander(user) : false;
};
.
.

最后,让我们进行测试:
file

本文章首发在 Laravel China 社区
上一篇 下一篇
讨论数量: 2
发起讨论


zh117
关于本节的遗漏?
3 个点赞 | 1 个回复 | 问答
tiroGuang
Windows 属性不同?
0 个点赞 | 0 个回复 | 问答