編寫具備描述性的 RESTful API (一): Workflow

這是個系列,將會以結構較爲簡單明瞭的 簡書 爲參考,編寫一套簡潔,可讀的社區類型的 RESTful API.php

我使用的laravel版本是5.7, 且使用 tree-ql 做爲api開發基礎工具.前端

該系列並不會一步一步來教你怎麼實現,只會闡明一些基本點,以及一些關鍵的地方laravel

相關的代碼我會放在 weiwenhao/community-api 你能夠隨時參閱一些細節部分git

開始咯

建表

先來看看**設計稿**,根據設計稿能夠設計出基礎的帖子表結構. 固然這不會是最終的表結構,後面會根據實際狀況來一點點完善該表.github

Schema::create('posts', function (Blueprint $table) {
    $table->increments('id');
    $table->string('code')->index();
    $table->string('title');
    $table->string('description');
    $table->text('content')->nullable();
    $table->string('cover')->nullable();
    $table->unsignedInteger('comment_count')->default(0)->comment('評論數量');
    $table->unsignedInteger('like_count')->default(0)->comment('點贊數量');
    $table->unsignedInteger('read_count')->default(0)->comment('閱讀數量');
    $table->unsignedInteger('word_count')->default(0)->comment('字數');
    $table->unsignedInteger('give_count')->default(0)->comment('讚揚數量');

    $table->unsignedInteger('user_id')->index();

    $table->timestamp('published_at')->nullable()->comment('發佈時間');
    $table->timestamp('selected_at')->nullable()->comment('是否精選/精選時間');
	$table->timestamp('edited_at')->nullable()->comment('內容編輯時間');
    
    $table->timestamps();
});
複製代碼

而後看看評論表.簡書的評論並非無限級的,而是分爲兩層,結構簡單.數據庫

Schema::create('comments', function (Blueprint $table) {
    $table->increments('id');
    $table->text('content');
    $table->unsignedInteger('user_id')->index();
    $table->unsignedInteger('post_id')->index();
    $table->unsignedInteger('like_count')->default(0);
    $table->unsignedInteger('reply_count')->default(0);
    $table->unsignedInteger('floor')->comment('樓層');
    $table->unsignedInteger('selected')->comment('是否精選')->default(0);
    $table->timestamps();
});

Schema::create('comment_replies', function (Blueprint $table) {
    $table->increments('id');
    $table->unsignedInteger('comment_id')->index();
    $table->unsignedInteger('user_id')->index();
    $table->text('content');
    $table->json('call_user')->nullable()->comment('@用戶,{id: null, nickname: null}');
    $table->timestamps();
});
複製代碼

而後是用戶表json

Schema::create('users', function (Blueprint $table) {
    $table->increments('id');
    $table->string('nickname');
    $table->string('avatar');
    $table->string('email');
    $table->string('phone_number');
    $table->string('password');

    $table->unsignedInteger('follow_count')->default(0)->comment("關注了多少個用戶");
    $table->unsignedInteger('fans_count')->default(0)->comment("擁有多少個粉絲");
    $table->unsignedInteger('post_count')->default(0);
    $table->unsignedInteger('word_count')->default(0);
    $table->unsignedInteger('like_count')->default(0);

    $table->json('oauth')->nullable()->comment("第三方登陸");

    $table->timestamps();
});
複製代碼

爲何從數據庫上開始設計?後端

從軟件開發的角度來講,數據是固有存在的,不會隨着交互與設計的變化而變化. 因此對於後端來講有了產品文檔,就能夠設計出接近完整的數據結構和80%左右的API了.api

建模

創建相關的Model關聯關係數組

這裏須要多作一步,創建一個**Model基類**,其餘的如 Post,Comment 繼承自該 Model . 固然 咱們不是要讓 Model 成爲一個 Super 類,只是經過該 Model 得到對其餘 Model 的統一配置權.

已 Comment 爲例

# Comment.php

<?php

namespace App\Models;


class Comment extends Model {
    public function user() {
        return $this->belongsTo(User::class);
    }

    public function replies() {
        return $this->hasMany(CommentReply::class);
    }

    public function post() {
        return $this->belongsTo(Post::class);
    }
}
複製代碼

其餘的 Model 參考源碼便可

填充 Seeder

爲了讓前端更加順暢的調試 api,seeder 是必不可少的一步. 接下來咱們須要爲上面建的幾張表添加相應的 factoryseeder

以 CommentFactory 爲例

# CommentFactory.php

<?php

use Faker\Generator as Faker;

$factory->define(\App\Models\Comment::class, function (Faker $faker) {
    static $i = 1;
    return [
        'post_id' => mt_rand(1, \App\Models\Post::count()),
        'user_id' => mt_rand(1, \App\Models\User::count()),
        'content' => $faker->sentence,
        'like_count' => mt_rand(0, 100),
        'reply_count' => mt_rand(0, 10),
        'floor' => $i++,
        'selected' => mt_rand(1, 10) > 2 ? 0 : 1
    ];
});
複製代碼

相應的 CommentSeeder

# CommentSeeder.php

<?php

use Illuminate\Database\Seeder;

class CommentSeeder extends Seeder {
    /** * Run the database seeds. * * @return void */
    public function run() {
        factory(\App\Models\Comment::class, 1000)->create();
    }
}
複製代碼

其餘的 factory 和 seeder 參考源碼呀

Seeder 的規範命名應該是 CommentsTableSeeder.php ,請不要學我!

發射

肯定 API

仍是先來看看 設計稿 , 來創建第一批 API

初步來看文章詳情頁分爲三部分. 文章內容部分,評論回覆部分,和推薦閱讀部分.因爲咱們目前只建了幾張基本表,因此先忽略推薦閱讀部分.

API 設計的一個原則是同一個頁面不要請求太屢次 API ,不然會給服務器帶來很大的壓力.但也不能是一條很是聚合的api包含一個頁面全部的數據. 這樣則失去了 API 的靈活與獨立性. 也不符合 RESTFul API 的設計思路

RESTFul API 是面向資源/數據的,是對資源的增刪改查. 而不是面向界面/具體的業務邏輯

因此上面說從設計稿切入實際是有些誤導的,原則上是不須要設計稿的.這裏的目的是爲了推進文章向下進行,且可以更快的看到成果

按照 tree-ql 的風格,我設計了這樣兩條 API

api.test.com/api/posts/{…

api.test.com/api/posts/{…

上面的api是真實能夠點擊測試的,你能夠隨意修改include中的字段,來觀察API的變化

執行請求的詳細信息能夠經過 telescope 查看

咱們來解讀一下上面兩條 API

  1. 取出帖子 {post},而且包含該帖子的詳情,用戶(用戶須要包含描述)和這篇帖子的全部精選評論
  2. 取出帖子 {post}下的評論,而且每條評論須要包含相關用戶和回覆/限制三條(回覆須要包含相關用戶)

絕不知羞恥的說,上面的API是極其富於可讀性的,而且有了 include 的存在,可控性也達到了很是高的地步

路由

# api.php

Route::get('posts/{post}', 'PostController@show');

Route::get('posts/{post}/comments', 'CommentController@index');
複製代碼

還要爲{post}進行 路由模型綁定

# RouteServiceProvider.php

public function boot() {
    parent::boot();
    
    Route::bind('post', function ($value) {
        // columns的做用稍後會解釋
        return Post::columns()->where('id', $value)->first();
    });
}
複製代碼

控制器

因爲沒有作版本控制,因此沒有添加相似Api/V1這樣的目錄. 以第二條 API 對應的控制器爲例

# app/Http/Controllers/Api/CommentController

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Comment;
use App\Resources\CommentResource;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

class CommentController extends Controller {
    /** * @param null $parent * @return \Weiwenhao\TreeQL\Resource */
    public function index($parent = null) {
        // 1.
        $query = $parent ? $parent->comments() : Comment::query();
        
        // 2.
        $comments = $query->columns()->latest()->paginate();

        // 3.
        return CommentResource::make($comments);
    }
}
複製代碼

至此咱們構成了 api.test.com/api/posts/{… 這條路由的訪問控制器,但此時還不能include任何東西.在說明如何定義include以前,咱們先對控制器中的三處標註進行講解.

  1. 進行了路由模型綁定的兼容處理,使得一個控制器能夠兼容多條路由. 具體能夠參考 優雅的使用路由模型綁定
  2. 這是常見的 Builder 查詢構造器,不嚴格討論的話 get() ∈ paginate(), 所以使用適用範圍更廣的 paginate 做爲結果輸出. columns 是一個查詢做用域,由 tree-ql 提供,其賦予了精確查詢數據庫字段的能力.
  3. 將查詢的結果集交付給 Resource, 此 Resource 並不是 laravel 原生的 Resource,而是 tree-ql 提供的 Resource ,其會賦予咱們 include 的能力,下面介紹一下該 Resource.

在閱讀下面的內容以前你須要閱讀一下 tree-ql 的文檔

Resource

已 CommentResource 爲例

# CommentResource.php

<?php

namespace App\Resources;

use Weiwenhao\TreeQL\Resource;

class CommentResource extends Resource {
    protected $default = [
        'id', 
        'content', 
        'user_id', 
        'like_count',
        'reply_count',
        'floor'
    ];

    protected $columns = [
        'id', 
        'content', 
        'user_id', 
        'post_id', 
        'like_count',
        'reply_count', 
        'floor'
    ];

    protected $relations = [
        'user',
        'replies' => [
            'resource' => CommentReplyResource::class,
        ]
    ];
}
複製代碼

其中 columns 表明着 comments 表的字段, relations 定義的內容爲 表明 comment 模型中已經定義的關聯關係.

API 請求中有些數據每次都須要加載,所以 default 中定義的字段會被默認 include ,而不須要在 url 中顯式的定義.

因爲 CommentResource 的 relations 部分還依賴了 user 和 replies ,按照 tree-ql 的規則咱們須要分別定義 UserResource 和 RepliesResource.

# UserResource.php

<?php

namespace App\Resources;

use Weiwenhao\TreeQL\Resource;

class UserResource extends Resource {
    protected $default = ['id', 'nickname', 'avatar'];

    protected $columns = ['id', 'nickname', 'avatar', 'password'];
}

複製代碼
# CommentReplyResource.php

<?php

namespace App\Resources;

use Weiwenhao\TreeQL\Resource;

class CommentReplyResource extends Resource {
    protected $default = ['id', 'comment_id', 'user_id', 'content', 'call_user'];

    protected $columns = ['id', 'comment_id', 'user_id', 'content', 'call_user'];

    protected $relations = ['user'];

    /** * ...{post}/comments?include=...replies(limit:3)... * * ↓ ↓ ↓ * * $comments->load(['replies' => function ($builder) { * $this->loadConstraint($builder, ['limit' => 3]) * }); * * ↓ ↓ ↓ * @param $builder * @param array $params */
    public function loadConstraint($builder, array $params) {
        isset($params['limit']) && $builder->limit($params['limit']);
    }
}
複製代碼

wo~, 咱們已經完成了代碼編寫,客戶端能夠請求API了 …… 嗎?

再來品味一下第二條api, 取出帖子 {post}下的評論,且每條評論攜帶3條回覆, ORM(MySQL) 能夠作到這樣的事情嗎?

點我看答案

這裏我選擇了 PLAN C ,至此咱們纔算完成第二條api的編寫,愉快的 request

可是

上面的API這麼花裏胡哨,會不會有性能問題?

來看看這條 API 的實際 SQL 表現,能夠看到 SQL 符合預期,並無任何的 n+1 問題,在速度方面能夠說是有保障的. 實際上只要按照 tree-ql 的規範,不管多麼花裏胡哨的 include ,都不會有性能問題.

調試工具 laravel/telescope

Workflow

走完了一套流程,稍微總結一下↓

Workflow 中去掉了肯定 API 這一步,由於咱們只要按照 RESTful 編寫路由,按照 tree-ql 編寫 Resource , API 天然而然的就出來啦~

補充

文章詳情API

api.test.com/api/posts/{…

這裏的 selected_comment 意爲精選的評論,簡書此處使用了單獨的 api 來請求精選的評論.可是考慮到一篇帖子的精選評論一般不會太多.所以我採用 include 的方式 將精選評論與帖子一種返回.

帖子和精選評論之間的的關係就是 data 和 meta 的關係. 來看看相關的配置代碼

# CommentResource.php

<?php

namespace App\Resources;

use Weiwenhao\TreeQL\Resource;

class PostResource extends Resource {
    protected $default = [
        'id',
        'title',
        'description',
        'cover',
        'comment_count',
        'like_count',
        'user_id'
    ];

    protected $columns = [
        'id',
        'title',
        'description',
        'cover',
        'read_count',
        'word_count',
        'give_count',
        'comment_count',
        'like_count',
        'user_id',
        'content',
        'selected_at',
        'published_at'
    ];


    protected $meta = [
        'selected_comments'
    ];


    public function selectedComments($params) {
        $post = $this->getModel();

        $comments = $post->selectedComments;

        // 這裏的操做相似於 $comments->load(['user', 'replies.user'])
        // 可是load可不會幫你管理Column. 所以咱們使用Resource來構造
        $commentResource = CommentResource::make($comments, 'user,replies.user');

        // getResponseData既獲取CommentResource解析後並構造後的結構數組
        return $commentResource->getResponseData();
    }
}
複製代碼

推薦閱讀

設計稿 的最後一部分,分爲兩個小點. 分別是專題收錄和推薦閱讀.專題和帖子之間是多對多的關係.

推薦的作法比較豐富,簡單且推薦的作法就是經過標籤來推薦.可是這裏咱們有了專題這個概念後,其就充當了標籤的概念.

下一篇會介紹專題與推薦閱讀的一些須要注意的細節.

相關文章
相關標籤/搜索