34. Vue 点赞组件
- 本系列文章为
laracasts.com
的系列视频教程——Let's Build A Forum with Laravel and TDD 的学习笔记。若喜欢该系列视频,可去该网站订阅后下载该系列视频, 支持正版 ;- 视频源码地址:github.com/laracasts/Lets-Build-a-...;
- 本项目为一个 forum(论坛)项目,与本站的第二本实战教程 《Laravel 教程 - Web 开发实战进阶》 类似,可互相参照。
本节说明
- 对应视频教程第 34 小节:A Vue Favorite Component
本节内容
本节我们将回复点赞的表单换成 Vue 点赞组件。首先我们来修改视图:
forum\resources\views\threads\reply.blade.php
.
.
<div class="panel-heading">
<div class="level">
<h5 class="flex">
<a href="{{ route('profile',$reply->owner) }}"> {{ $reply->owner->name }}</a>
回复于
{{ $reply->created_at->diffForHumans() }}
</h5>
<div>
<favorite :reply="{{ $reply }}"></favorite>
</div>
</div>
</div>
.
.
接下来新增Favorite.vue
组件:
forum\resources\assets\js\components\Favorite.vue
<template>
<button type="submit" class="btn btn-default">
<span class="glyphicon glyphicon-heart"></span>
<span v-text="favoritesCount"></span>
</button>
</template>
<script>
export default {
data() {
return {
favoritesCount: 10
}
}
}
</script>
现在Favorite.vue
依然是不可用的,因为我们想要在Reply.vue
组件中使用Favorite.vue
,首先必须引入它:
forum\resources\assets\js\components\Reply.vue
<script>
import Favorite from './Favorite.vue';
export default {
props: ['attributes'],
components: { Favorite },
data() {
return {
editing: false,
body: this.attributes.body
};
},
methods:{
update() {
axios.patch('/replies/' + this.attributes.id,{
body:this.body
});
this.editing = false;
flash('Updated!');
},
destroy() {
axios.delete('/replies/' + this.attributes.id);
$(this.$el).fadeOut(300, () => {
flash('Your reply has been deleted!');
});
}
}
}
</script>
现在刷新页面即可看到点赞组件:
但是我们现在点击按钮是不会有任何反应的,因为我们没有为按钮定义动作。让我们来梳理一下接下来要做的事情:首先,我们要给按钮定义动作,点击按钮,点赞该回复,再次点击则取消点赞;接下来,我们要正确显示点赞的数量。
首先,给按钮定义动作:
forum\resources\assets\js\components\Favorite.vue
<template>
<button type="submit" :class="classes" @click="toggle">
<span class="glyphicon glyphicon-heart"></span>
<span v-text="favoritesCount"></span>
</button>
</template>
<script>
export default {
props:['reply'],
data() {
return {
favoritesCount: this.reply.favoritesCount,
isFavorited:false
}
},
computed: {
classes() {
return ['btn',this.isFavorited ? 'btn-primary' : 'btn-default']
}
},
methods: {
toggle() {
if (this.isFavorited){
axios.delete('/replies/' + this.reply.id + '/favorites')
}else {
axios.post('/replies/' + this.reply.id + '/favorites');
this.isFavorited = true;
this.favoritesCount++;
}
}
}
}
</script>
其实在以上我们已经补充完整了两个步骤所需要进行的动作。我们使用favoritesCount
来获取点赞的数量。那么这个属性是如何得到的呢?我们在之前的章节中将获取点赞数的方法封装成了Favoritable
Trait,并且在 Trait 定义了一个 访问器:getFavoritesCountAttribute
,然后我们通过$reply->favorites_count
来获取favorites_count
属性。但是我们在 Vue 组件中无法使用这样的方式获取到favorites_count
:
所以我们改为使用 序列化 的方式,添加一个在数据库中没有对应字段的属性。要使用序列化,首先你需要为这个值定义一个 访问器(我们已经创建)。访问器创建成功后,只需添加该属性到该模型的 appends
属性中:
forum\app\Reply.php
class Reply extends Model
{
use Favoritable,RecordsActivity;
protected $guarded = [];
protected $with = ['owner','favorites'];
protected $appends = ['favoritesCount'];
.
.
我们刷新页面:
可以看到favoritesCount
属性已经添加到模型属性组当中,并且在 Vue 组件的属性组中。我们已经将点赞的动作补充完整,如果我们现在点击按钮,会发现按钮变色,同时点赞数变成了 1:
接下来我们进行取消点赞的动作。首先我们先新建一个测试:
forum\tests\Feature\FavoritiesTest.php
.
.
/** @test */
public function an_authenticated_user_can_unfavorite_a_reply()
{
$this->signIn();
$reply = create('App\Reply');
$this->post('replies/' . $reply->id . '/favorites');
$this->assertCount(1,$reply->favorites);
$this->delete('replies/' . $reply->id . '/favorites');
$this->assertCount(0,$reply->favorites);
}
.
.
添加路由:
forum\routes\web.php
.
.
Route::post('/replies/{reply}/favorites','FavoritesController@store');
Route::delete('/replies/{reply}/favorites','FavoritesController@destroy');
.
.
添加destroy()
方法:
forum\app\Http\Controllers\FavoritesController.php
.
.
public function destroy(Reply $reply)
{
$reply->unfavorite();
}
}
添加unfavorite()
方法:
forum\app\Favoritable.php
.
.
public function favorite()
{
$attributes = ['user_id' => auth()->id()];
if (!$this->favorites()->where($attributes)->exists()) {
return $this->favorites()->create($attributes);
}
}
public function unfavorite()
{
$attributes = ['user_id' => auth()->id()];
$this->favorites()->where($attributes)->delete();
}
.
.
现在我们可以运行测试:
测试未通过,原因是什么呢?如果你对之前的内容记得很清楚的话,那么你应该记得我们是通过Reply
模型的$with
属性组来获取favorites
:
forum\app\Reply.php
.
.
class Reply extends Model
{
use Favoritable,RecordsActivity;
protected $guarded = [];
protected $with = ['owner','favorites'];
protected $appends = ['favoritesCount'];
.
.
而我们使用$reply->favorites
来获取点赞数,这会让我们预加载favorites
属性,所以我们的第二个断言未通过:
.
.
$this->assertCount(0,$reply->favorites);
.
.
我们需要使用fresh()
函数:
.
.
$this->assertCount(0,$reply->fresh()->favorites);
.
.
再次测试,测试通过:
现在我们可以进行一些重构:
.
.
/** @test */
public function an_authenticated_user_can_unfavorite_a_reply()
{
$this->signIn();
$reply = create('App\Reply');
$reply->favorite();
$this->delete('replies/' . $reply->id . '/favorites');
$this->assertCount(0,$reply->favorites);
}
.
.
再次测试:
到目前为止,我们取消点赞的动作已经完成,接下来我们只需要完善我们的组件即可:
forum\resources\assets\js\components\Favorite.vue
<script>
export default {
props:['reply'],
data() {
return {
favoritesCount: this.reply.favoritesCount,
isFavorited:false
}
},
computed: {
classes() {
return ['btn',this.isFavorited ? 'btn-primary' : 'btn-default']
}
},
methods: {
toggle() {
if (this.isFavorited){
axios.delete('/replies/' + this.reply.id + '/favorites');
this.isFavorited = false;
this.favoritesCount--;
}else {
axios.post('/replies/' + this.reply.id + '/favorites');
this.isFavorited = true;
this.favoritesCount++;
}
}
}
}
</script>
现在我们刷新页面,已经可以完整地进行点赞和取消点赞行为了。但是,我们默认了isFavorited
属性是false
:
isFavorited:false
我们该如何正确获取是否已经进行过点赞行为呢?答案是 序列化 。要使用序列化,首先我们要定义一个 访问器:
forum\app\Favoritable.php
.
.
public function getIsFavoritedAttribute()
{
return $this->isFavorited();
}
public function getFavoritesCountAttribute()
{
return $this->favorites->count();
}
}
接着将isFavorited
添加到模型的$appends
属性组:
forum\app\Reply.php
.
.
protected $appends = ['favoritesCount','isFavorited'];
.
.
最后我们在组件中使用:
forum\resources\assets\js\components\Favorite.vue
.
.
data() {
return {
favoritesCount: this.reply.favoritesCount,
isFavorited:this.reply.isFavorited
}
},
.
.
现在你可以刷新页面进行测试。你知道接下来我们要做什么吗?重构。最后让我们来重构组件,使其更具可读性:
<template>
<button type="submit" :class="classes" @click="toggle">
<span class="glyphicon glyphicon-heart"></span>
<span v-text="count"></span>
</button>
</template>
<script>
export default {
props:['reply'],
data() {
return {
count: this.reply.favoritesCount,
active:this.reply.isFavorited
}
},
computed: {
classes() {
return ['btn',
this.active ? 'btn-primary' : 'btn-default'
];
},
endpoint() {
return '/replies/' + this.reply.id + '/favorites';
}
},
methods: {
toggle() {
this.active ? this.destroy() : this.create();
},
create() {
axios.post(this.endpoint);
this.active = true;
this.count++;
},
destroy() {
axios.delete(this.endpoint);
this.active = false;
this.count--;
}
}
}
</script>
再次刷新页面进行测试。最后的最后,运行一下全部测试:
Perfect!