用戶行爲泛指用戶在網站上的交互,如點贊/關注/收藏/評論/發帖等等. 這些行爲具備共同的特徵,所以能夠用一致的編碼來描述用戶的行爲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 方法過於繁瑣的問題.