編寫具備描述性的 RESTful API (三): 用戶行爲

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

在 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');
    }
}

複製代碼

還沒完,還有一步重要的實現,即是判斷當前用戶是否已經喜歡了該帖子,或者說在一個帖子列表中當前用戶喜歡了哪些帖子

Is like

爲了進一步增長描述性和前端綁定數據的便利性. 咱們須要藉助 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 啦

test.com/api/posts?i…

test.com/api/posts/1…

文末會附上 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緩存

用戶行爲的存儲結構很是符合 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);
        }
    }
}
複製代碼

更進一步

上面的作法是我過去在項目中使用的方法,可是在編寫該篇時,個人腦海中冒出了另一個想法

test.com/api/posts?i…

稍微去源碼看看 PostResource 便明白了. 使用該方法可以優化上文提到的 destroy 方法過於繁瑣的問題.

相關

相關文章
相關標籤/搜索