編寫具有描述性的簡書 RESTfu

這是個系列,將會以結(jié)構(gòu)較為簡單明了的 [簡書](http://www.itdecent.cn/) 為參考,編寫一套簡潔,**可讀**的社區(qū)類型的 **RESTful** API.

> 我使用的laravel版本是5.7, 且使用 [tree-ql](https://github.com/weiwenhao/tree-ql) 作為api開發(fā)基礎(chǔ)工具.

>

> 該系列并不會一步一步來教你怎么實現(xiàn),只會闡明一些基本點,以及一些關(guān)鍵的地方

>

> 相關(guān)的代碼我會放在 [weiwenhao/community-api](https://github.com/weiwenhao/community-api) 你可以隨時參閱一些細(xì)節(jié)部分

## 開始咯

#### [建表](https://learnku.com/docs/laravel/5.7/migrations/2291)

先來看看**[設(shè)計稿](http://www.itdecent.cn/p/aff99d1d194b)**,根據(jù)設(shè)計稿可以設(shè)計出基礎(chǔ)的帖子表結(jié)構(gòu). 當(dāng)然這不會是最終的表結(jié)構(gòu),后面會根據(jù)實際情況來一點點完善該表.

```php

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('評論數(shù)量');

? ? $table->unsignedInteger('like_count')->default(0)->comment('點贊數(shù)量');

? ? $table->unsignedInteger('read_count')->default(0)->comment('閱讀數(shù)量');

? ? $table->unsignedInteger('word_count')->default(0)->comment('字?jǐn)?shù)');

? ? $table->unsignedInteger('give_count')->default(0)->comment('贊賞數(shù)量');

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

? ? $table->timestamp('published_at')->nullable()->comment('發(fā)布時間');

? ? $table->timestamp('selected_at')->nullable()->comment('是否精選/精選時間');

$table->timestamp('edited_at')->nullable()->comment('內(nèi)容編輯時間');


? ? $table->timestamps();

});

```

然后看看評論表.簡書的評論并不是無限級的,而是分為兩層,結(jié)構(gòu)簡單.

```php

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();

});

```

然后是用戶表

```php

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("關(guān)注了多少個用戶");

? ? $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();

});

```

> 為什么從數(shù)據(jù)庫上開始設(shè)計?

>

> 從軟件開發(fā)的角度來說,**數(shù)據(jù)是固有存在的,不會隨著交互與設(shè)計的變化而變化**. 所以對于后端來說有了產(chǎn)品文檔,就可以設(shè)計出接近完整的數(shù)據(jù)結(jié)構(gòu)和80%左右的API了.

#### 建模

建立相關(guān)的[Model](https://learnku.com/docs/laravel/5.7/eloquent/2294) 及 [關(guān)聯(lián)關(guān)系](https://learnku.com/docs/laravel/5.7/eloquent-relationships/2295)

> 這里需要多做一步,建立一個**[Model基類](https://learnku.com/docs/laravel-specification/5.5/data-model/503)**,其他的如 Post,Comment 繼承自該 Model . 當(dāng)然 我們不是要讓 Model 成為一個 Super 類,只是通過該 Model 獲得對其他 Model 的統(tǒng)一配置權(quán).

已 Comment 為例

```php

# 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

為了讓前端更加順暢的調(diào)試 api,seeder 是必不可少的一步. 接下來我們需要為上面建的幾張表添加相應(yīng)的 [factory](https://learnku.com/docs/laravel/5.7/database-testing/2304) 和 [seeder](https://learnku.com/docs/laravel/5.7/seeding/2292)

以 CommentFactory 為例

```php

# 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

? ? ];

});

```

相應(yīng)的 CommentSeeder

```php

# 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 的規(guī)范命名應(yīng)該是? CommentsTableSeeder.php ,請不要學(xué)我!

## 發(fā)射

#### 確定 API

還是先來看看 [設(shè)計稿](http://www.itdecent.cn/p/aff99d1d194b) , 來建立第一批 API

初步來看文章詳情頁分為三部分. 文章內(nèi)容部分,評論回復(fù)部分,和推薦閱讀部分.由于我們目前只建了幾張基本表,所以先忽略推薦閱讀部分.

API 設(shè)計的一個原則是同一個頁面不要請求太多次 API ,否則會給服務(wù)器帶來很大的壓力.但也不能是一條非常聚合的api包含一個頁面所有的數(shù)據(jù). 這樣則失去了 API 的靈活與獨立性. 也不符合 RESTFul API 的設(shè)計思路

> **RESTFul API 是面向資源/數(shù)據(jù)的,是對資源的增刪改查. 而不是面向界面/具體的業(yè)務(wù)邏輯**

>

> 所以上面說從設(shè)計稿切入實際是有些誤導(dǎo)的,原則上是不需要設(shè)計稿的.這里的目的是為了推動文章向下進(jìn)行,且能夠更快的看到成果

按照 [tree-ql](https://github.com/weiwenhao/tree-ql) 的風(fēng)格,我設(shè)計了這樣兩條 API

[http://api.test.com/api/posts/{post}?include=content,user.description,selected_comments ](http://community.eienao.com/api/posts/1?include=content,user.description,selected_comments)

[http://api.test.com/api/posts/{post}/comments?include=user,replies(limit:3).user](http://community.eienao.com/api/posts/1/comments?include=user,replies(limit:3).user)

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

>

> 執(zhí)行請求的詳細(xì)信息可以通過 [telescope](http://community.eienao.com/telescope) 查看

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

1. 取出帖子 `{post}`,并且包含該帖子的詳情,用戶(用戶需要包含描述)和這篇帖子的所有精選評論

2. 取出帖子 `{post}`下的評論,并且每條評論需要包含相關(guān)用戶和**回復(fù)/限制三條**(回復(fù)需要包含相關(guān)用戶)

毫不知羞恥的說,上面的API是極其富于可讀性的,并且有了 include 的存在,可控性也達(dá)到了非常高的地步

#### 路由

```php

# api.php

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

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

```

還要為`{post}`進(jìn)行 [路由模型綁定](https://learnku.com/docs/laravel/5.7/routing/2253#2f0069)

```php

# RouteServiceProvider.php

public function boot()

{

? ? parent::boot();


? ? Route::bind('post', function ($value) {

? ? ? ? // columns的作用稍后會解釋

? ? ? ? return Post::columns()->where('id', $value)->first();

? ? });

}

```

#### 控制器

由于沒有做版本控制,所以沒有添加類似`Api/V1`這樣的目錄. 以第二條 API 對應(yīng)的控制器為例

```php

# 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);

? ? }

}

```

至此我們構(gòu)成了 [http://api.test.com/api/posts/{post}/comments](http://community.eienao.com/api/posts/1/comments) 這條路由的訪問控制器,但此時還不能include任何東西.在說明如何定義include之前,我們先對控制器中的三處標(biāo)注進(jìn)行講解.

1. 進(jìn)行了路由模型綁定的兼容處理,使得一個控制器可以兼容多條路由. 具體可以參考 [優(yōu)雅的使用路由模型綁定](https://learnku.com/articles/17476)

2. 這是常見的 Builder 查詢構(gòu)造器,不嚴(yán)格討論的話 `get() ∈ paginate()`, 因此使用適用范圍更廣的 paginate 作為結(jié)果輸出.? columns 是一個查詢作用域,由 tree-ql 提供,其賦予了精確查詢數(shù)據(jù)庫字段的能力.

3. 將查詢的結(jié)果集交付給 Resource, 此 Resource 并非 laravel 原生的 [Resource](https://learnku.com/docs/laravel/5.7/eloquent-resources/2298),而是 **tree-ql 提供的 Resource** ,其會賦予我們 include 的能力,下面介紹一下該 Resource.

> 在閱讀下面的內(nèi)容之前你需要閱讀一下 tree-ql 的文檔

#### Resource

已 CommentResource 為例

```php

# 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 定義的內(nèi)容為 代表 comment 模型中已經(jīng)定義的關(guān)聯(lián)關(guān)系.

API 請求中有些數(shù)據(jù)每次都需要加載,因此 **default 中定義的字段會被默認(rèn) include** ,而不需要在 url 中顯式的定義.

由于 CommentResource 的 relations 部分還依賴了 user 和 replies ,按照 tree-ql 的規(guī)則我們需要分別定義 UserResource 和 RepliesResource.

```php

# 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'];

}

```

```php

# 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~, 我們已經(jīng)完成了代碼編寫,客戶端可以請求API了 …… 嗎?

再來品味一下第二條api, **取出帖子 `{post}`下的評論,且每條評論攜帶3條回復(fù),? ORM(MySQL) 可以做到這樣的事情嗎?**

[點我看答案](https://learnku.com/articles/24787)

這里我選擇了 PLAN C ,至此我們才算完成第二條api的編寫,愉快的 [request](http://community.eienao.com/api/posts/1/comments?include=user,replies(limit:3).user) 吧

#### 但是

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

來看看這條 API 的實際 SQL 表現(xiàn),可以看到 SQL 符合預(yù)期,并沒有任何的 n+1 問題,在速度方面可以說是有保障的. 實際上只要按照 tree-ql 的規(guī)范,無論多么花里胡哨的 include ,都不會有性能問題.

![](https://iocaffcdn.phphub.org/uploads/images/201903/12/10960/H9uZKWjav8.png!large)

> 調(diào)試工具 [laravel/telescope](https://github.com/laravel/telescope)

#### Workflow

走完了一套流程,稍微總結(jié)一下↓

![](https://iocaffcdn.phphub.org/uploads/images/201903/12/10960/QGYs8Eoz6m.png!large)

> Workflow 中去掉了確定 API 這一步,因為我們只要按照 RESTful 編寫路由,按照 tree-ql 編寫 Resource , API 自然而然的就出來啦~

## 補充

#### 文章詳情API

[http://api.test.com/api/posts/{post}?include=content,user.description,selected_comments ](http://community.eienao.com/api/posts/1?include=content,user.description,selected_comments)

這里的 selected_comment 意為精選的評論,簡書此處使用了單獨的 api 來請求精選的評論.但是考慮到一篇帖子的精選評論通常不會太多.因此我采用 include 的方式 將精選評論與帖子一種返回.

**帖子和精選評論之間的的關(guān)系就是 data 和 meta 的關(guān)系**. 來看看相關(guān)的配置代碼

```php

# 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來構(gòu)造

? ? ? ? $commentResource = CommentResource::make($comments, 'user,replies.user');

? ? ? ? // getResponseData既獲取CommentResource解析后并構(gòu)造后的結(jié)構(gòu)數(shù)組

? ? ? ? return $commentResource->getResponseData();

? ? }

}

```

#### 推薦閱讀

[設(shè)計稿](http://www.itdecent.cn/p/aff99d1d194b) 的最后一部分,分為兩個小點. 分別是專題收錄和推薦閱讀.專題和帖子之間是多對多的關(guān)系.

推薦的做法比較豐富,簡單且推薦的做法就是通過標(biāo)簽來推薦.但是這里我們有了專題這個概念后,其就充當(dāng)了標(biāo)簽的概念.

下一篇會介紹專題與推薦閱讀的一些需要注意的細(xì)節(jié).

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容