這是個系列,將會以結構較為簡單明瞭的 簡書 為參考,編寫一套簡潔,可讀的社群型別的 RESTful API.
我使用的laravel版本是5.7, 且使用 tree-ql 作為api開發基礎工具.
該系列並不會一步一步來教你怎麼實現,只會闡明一些基本點,以及一些關鍵的地方
相關的程式碼我會放在 weiwenhao/community-api 你可以隨時參閱一些細節部分
開始咯
建表
先來看看設計稿,根據設計稿可以設計出基礎的帖子表結構. 當然這不會是最終的表結構,後面會根據實際情況來一點點完善該表.
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();
});
然後是使用者表
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了.
建模
這裡需要多做一步,建立一個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 是必不可少的一步. 接下來我們需要為上面建的幾張表新增相應的 factory 和 seeder
以 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
test.com/api/posts/{post}?include=content,user.description,selected_comments
test.com/api/posts/{post}/comments?include=user,replies(limit:3).user
上面的api是真實可以點選測試的,你可以隨意修改include中的欄位,來觀察API的變化
執行請求的詳細資訊可以透過 telescope 檢視
我們來解讀一下上面兩條 API
- 取出帖子
{post}
,並且包含該帖子的詳情,使用者(使用者需要包含描述)和這篇帖子的所有精選評論 - 取出帖子
{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);
}
}
至此我們構成了 test.com/api/posts/{post}/comments 這條路由的訪問控制器,但此時還不能include任何東西.在說明如何定義include之前,我們先對控制器中的三處標註進行講解.
- 進行了路由模型繫結的相容處理,使得一個控制器可以相容多條路由. 具體可以參考 優雅的使用路由模型繫結
- 這是常見的 Builder 查詢構造器,不嚴格討論的話
get() ∈ paginate()
, 因此使用適用範圍更廣的 paginate 作為結果輸出. columns 是一個查詢作用域,由 tree-ql 提供,其賦予了精確查詢資料庫欄位的能力. - 將查詢的結果集交付給 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/{post}?include=content,user.description,selected_comments
這裡的 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();
}
}
推薦閱讀
設計稿 的最後一部分,分為兩個小點. 分別是專題收錄和推薦閱讀.專題和帖子之間是多對多的關係.
推薦的做法比較豐富,簡單且推薦的做法就是透過標籤來推薦.但是這裡我們有了專題這個概念後,其就充當了標籤的概念.
下一篇會介紹專題與推薦閱讀的一些需要注意的細節.
本作品採用《CC 協議》,轉載必須註明作者和本文連結