用户行为泛指用户在网站上的交互,如点赞/关注/收藏/评论/发帖等等. 这些行为具备共同的特征,所以能够用一致的编码来描述用户的行为php
咱们先来分析一下简书中的用户行为, user_follow_user (用户关注用户) / user_like_comment / user_like_post / user_give_post (用户赞扬帖子) / user_follow_collection (用户关注专题)/ user_comment_post 等等前端
按照 RESTful 规范将上面的行为抽象为资源, 能够获得 user_follower , comment_liker , post_liker, post_giver , collection_follower 等资源.vue
这里我忽略了一个可能的资源 post_commenters , 所以它已经存在了,就是 comments 表, 所以不作考虑ios
接下来以 post_liker 为例来看一下实际的编码git
Schema::create('post_likers', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('post_id');
$table->unsignedInteger('user_id');
$table->timestamps();
$table->index('post_id');
$table->index('user_id');
$table->unique(['post_id', 'user_id']);
});
复制代码
这里 post_liker 已经被提取成了资源的形式,虽然其依旧充当着中间表的做用,但形式上须要按照资源的形式来处理.github
即表名须要复数形式,且须要相应的模型与控制器. 概言之axios
> php artisan make:model Models/PostLiker -a
后端
<?php
namespace App\Models;
class PostLiker extends Model {
public function post() {
return $this->belongsTo(Post::class);
}
public function user() {
return $this->belongsTo(User::class);
}
}
复制代码
post 请求表示建立 liker 资源 即用户喜欢帖子时须要调用该 API ,反之 delete 则表示删除 liker 资源.api
# api.php
Route::post('posts/{post}/liker', 'PostLikerController@store');
Route::delete('posts/{post}/liker', 'PostLikerController@destroy');
复制代码
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Post;
use App\Models\PostLiker;
use Illuminate\Http\Response;
class PostLikerController extends Controller {
/** * @param Post $post * @return \Illuminate\Contracts\Routing\ResponseFactory|Response */
public function store(Post $post) {
$postLiker = new PostLiker();
$postLiker->user_id = \Auth::id();
$postLiker->post()->associate($post);
$postLiker->save();
return $this->created();
}
/** * @param Post $post * @return \Illuminate\Contracts\Routing\ResponseFactory|Response */
public function destroy(Post $post) {
$postLiker = PostLiker::where('user_id', \Auth::id())
->where('post_id', $post->id)
->firstOrFail();
// Observer 中须要用到该关联,防止重复查询
$postLiker->setRelation('post', $post);
$postLiker->delete();
return $this->noContent();
}
}
复制代码
至此咱们就成功把喜欢帖子的行为转换成了 RESTful API.浏览器
destroy 方法为了触发 observer 显得有些繁琐,还有待优化.
在 Observer 中,咱们处理了与主逻辑不想关的附加逻辑, 即增减 post.like_count
<?php
namespace App\Observers;
use App\Models\PostLiker;
class PostLikerObserver {
/** * @param PostLiker $postLiker */
public function created(PostLiker $postLiker) {
$postLiker->post()->increment('like_count');
}
public function deleted(PostLiker $postLiker) {
$postLiker->post()->decrement('like_count');
}
}
复制代码
还没完,还有一步重要的实现,即是判断当前用户是否已经喜欢了该帖子,或者说在一个帖子列表中当前用户喜欢了哪些帖子
为了进一步增长描述性和前端绑定数据的便利性. 咱们须要借助 tree-ql 来实现该功能
# PostResource.php
<?php
namespace App\Resources;
use Illuminate\Support\Facades\Redis;
use Weiwenhao\TreeQL\Resource;
class PostResource extends Resource {
// 单次请求简单缓存
private $likedPostIds;
// ...
protected $custom = [
'liked'
];
/** * @param $post * @return mixed */
public function liked($post) {
if (!$this->likedPostIds) {
$this->likedPostIds = $user->likedPosts()->pluck('posts.id')->toArray();
}
return in_array($post->id, $this->likedPostIds);
}
}
复制代码
定义好以后,就能够愉快的 include 啦
文末会附上 Postman 文档.其他用户行为,同理实现便可.
用户行为既然具备一致性,那么其实能够进一步抽象来避免大量的用户行为控制器和路由.
彻底可使用一个 UserActionController , 前端配套 userAction.js 来统一实现用户行为,
更进一步的想法则是,前端在 localStorate 中维护一份用户行为的资源数据,能够直接经过 localStorate 来判断当前用户是否喜欢过某个用户/是否喜欢过某篇文章等等,从而减轻后端的计算压力.
localStorage 中的资源示例
post_likers: [12, 234, 827, 125]
comment_likers: [222, 352, 122]
下面的示例是我曾经在项目中按照该想法实现的 userAction.js
import Vue from 'vue'
import userAction from '~/api/userAction'
/** * 用户行为统一接口 * * 因为该类使用了浏览器端的 localStorage 因此只能在客户端调用 * actionName 和对应的 primaryKey 分别为 * follow_user | 被关注的用户的 id - 关注用户行为 * join_community | 加入的社区的 id - 加入社区行为 * collect_product | 收藏的产品的 id - 收藏产品行为 * * +---------------------------------------------------------------------------- * * 推荐在组件的 mounted 中调用 action 中的方法.已保证调用栈在浏览器端 * is() 方法属于异步方法,返回一个 Promise 对象,所以须要使用 await承接其结果 * * +---------------------------------------------------------------------------- * mounted 中的调用示例. 判断当前用户是否关注了用户 id 为 1 的用户. * async mounted () { * this.isAction = await this.$action.is('follow_user', 1, this) * } */
Vue.prototype.$action = {
/** * 是否对某个对象产生过某种行为 * @param actionName * @param primaryKey * @param vue * @returns {Promise<boolean>} */
async is (actionName, primaryKey, vue) {
if (!vue.$store.state.auth.user) {
throw new Error('用户未登陆')
}
if (!primaryKey) {
throw new Error('primaryKey必须传递')
}
primaryKey = Number(primaryKey)
// 若是本地没有在 localStorage 中发现相应的 actionName 则去请求后端进行同步
if (!window.localStorage.getItem(actionName)) {
let {data} = await vue.$axios.get(userAction.fetch(actionName))
Object.keys(data).forEach(function (item) {
localStorage.setItem(item, JSON.stringify(data[item]))
})
}
let item = localStorage.getItem(actionName)
item = JSON.parse(item)
if (item.indexOf(primaryKey) !== -1) {
return true
}
return false
},
/** * 建立一条用户行为 * @param actionName * @param primaryKey * @param vue * @returns {boolean} */
create (actionName, primaryKey, vue) {
if (!vue.$store.state.auth.user) {
throw new Error('用户未登陆')
}
if (!primaryKey) {
throw new Error('primaryKey必须传递')
}
primaryKey = Number(primaryKey)
// 发送 http 请求
vue.$axios.post(userAction.store(actionName, primaryKey))
// localStorage
let item = window.localStorage.getItem(actionName)
if (!item) {
return false
}
item = JSON.parse(item)
if (item.indexOf(primaryKey) === -1) {
item.push(primaryKey)
localStorage.setItem(actionName, JSON.stringify(item))
}
},
/** * 删除一条用户行为 * @param actionName 用户行为名称 * @param primaryKey 该行为对应的primaryKey * @param vue * @returns {boolean} */
delete (actionName, primaryKey, vue) {
if (!vue.$store.state.auth.user) {
throw new Error('用户未登陆')
}
if (!primaryKey) {
throw new Error('primaryKey必须传递')
}
primaryKey = Number(primaryKey)
// 发送 http 请求
vue.$axios.delete(userAction.destroy(actionName, primaryKey))
let item = window.localStorage.getItem(actionName)
if (!item) {
return false
}
item = JSON.parse(item)
let index = item.indexOf(primaryKey)
if (index !== -1) {
item.splice(index, 1)
localStorage.setItem(actionName, JSON.stringify(item))
}
}
}
复制代码
理论上按照该想法来实现,能够极大先后端的编码压力.
相应的后端代码就相似上面的 PostLikerController, 只是前端请求中增长了action_name 作数据导向.
用户行为的存储结构很是符合 Redis 的 SET 数据结构, 所以来尝试使用 set 对用户行为进行缓存,依旧使用 post_likers 做为例子.
Resource
<?php
namespace App\Resources;
use App\Models\Post;
use Illuminate\Support\Facades\Redis;
use Weiwenhao\TreeQL\Resource;
class PostResource extends Resource {
// ...
protected $custom = [
'liked'
];
/** * @param Post $post * @return boolean */
public function liked($post) {
$user = \Auth::user();
$key = "user_likers:{$user->id}";
if (!Redis::exists($key)) {
$likedPostIds = $user->likedPosts()->pluck('posts.id')->toArray();
Redis::sadd($key, $likedPostIds);
}
return (boolean)Redis::sismember($key, $post->id);
}
}
复制代码
相关的更新和删除缓存在 Observe中进行,控制器编码无需变更
# PostLikerObserver.php
<?php
namespace App\Observers;
use App\Models\PostLiker;
use Illuminate\Support\Facades\Redis;
class PostLikerObserver {
/** * @param PostLiker $postLiker */
public function created(PostLiker $postLiker) {
$postLiker->post()->increment('like_count');
// cache add
$user = \Auth::user();
$key = "user_likers:{$user->id}";
if (Redis::exists($key)) {
Redis::sadd($key, [$postLiker->post_id]);
}
}
public function deleted(PostLiker $postLiker) {
$postLiker->post()->decrement('like_count');
// cache remove
$userId = \Auth::id();
$key = "user_likers:{$userId}";
if (Redis::exists($key)) {
Redis::srem($key, $postLiker->post_id);
}
}
}
复制代码
上面的作法是我过去在项目中使用的方法,可是在编写该篇时,个人脑海中冒出了另一个想法
稍微去源码看看 PostResource 便明白了. 使用该方法可以优化上文提到的 destroy 方法过于繁琐的问题.