上一節講到了使用者行為,由使用者行為自然的便引出了通知系統。當使用者喜歡了一篇帖子,那麼該帖子的作者應該收到一條提醒。
laravel 提供了一套 Notification 元件,用於處理通知,其支援通過多種頻道傳送通知,包括郵件、簡訊 、通知還能儲存到資料庫(站內信)以便後續在 Web 頁面中顯示,本節將重點放在站內信。
laravel 為我們擺平了通知的推送問題,但是還有一個問題,即通知(資料庫)的儲存問題需要我們處理。通知的儲存通常有兩種做法。
- 將通知的主體內容與部分附加內容一同冗餘儲存到資料庫中,即 laravel 的預設形式。
- 將通知的主體內容的主鍵與其附加內容的主鍵 既 id 儲存到資料庫中。
前者的好處是較小的查詢壓力,且資料具有永續性,不會因為被刪帖等問題而影響到通知內容。缺點則是佔用儲存空間,且缺乏靈活性,後者則反之。
原始碼中選擇了前者,既預設形式。
資料分析
通過抽象可以得到,一條通知由三部分組成 行為的觸發者 trigger 、行為主體(可能攜帶 內容) target 、需要通知的使用者 notifiable
此處最讓人疑惑的應該是 行為的主體,根據實際需求稍微圖解一下。
已 「Comment Post」為例,在簡書中其實際的表現行為如下
根據上面的分析 notifications 表中的 data 需要冗餘如下資料,可以根據運營的實際需求調整。
public function toArray($notifiable)
{
$data = [
'trigger' => [
'id' => 1, // default type users
'type' => 'users',
'nickname' => 'nickname',
'avatar' => 'xxx',
],
'target' => [
'id' => 12,
'type' => 'posts',
'text' => 'xxx',
],
'content' => [
'id' => 1,
'type' => 'comment',
'text' => 'xxx',
'call_user' => [
'id' => 'xxx',
'nickname' => 'xxx'
]
]
];
return $data;
}
建立通知
已上一次的 「Like Post」 行為為例,當使用者點贊文章後,需要給文章的作者傳送一條通知。
# PostLikerObserver
/**
* @param PostLiker $postLiker
*/
public function created(PostLiker $postLiker)
{
// ...
// notify App\Notifications\LikePost
User::findOrFail($postLiker->post->user_id)
->notify(new LikePost($user, $postLiker->post));
}
# LikerPost.php
namespace App\Notifications;
class LikePost extends Notification
{
use Queueable;
private $post;
private $trigger;
private $target;
public function __construct(User $trigger, Post $target)
{
$this->trigger = $trigger;
$this->target = $target;
}
// ...
public function toArray($notifiable)
{
$data = [
'trigger' => [
'id' => $this->trigger->id,
'type' => $this->trigger->getTable(),
'nickname' => $this->trigger->nickname,
'avatar' => $this->trigger->avatar,
],
'target' => [
'id' => $this->target->id,
'type' => $this->target->getTable(),
'text' => $this->target->title,
]
];
return $data;
}
}
這樣就成功建立了一條 Notification , 類似「Comment Post」等使用者行為依舊可以按照這種思路完成。無非「Comment Post」需要在其 data 中新增 content 而已,這裡就不做展示了。
這裡需要提一下程式碼優化,通過上面的「資料分析」,我們已經把通知抽象為 trigger / target / content ,因此並不需要再每種使用者行為都編寫一堆 重複的構造方法,toArray 等方法。
完全可以編寫一個 Notification 基類來編寫上面的大部分程式碼,從而減少重複的程式碼。
相關優化已經完成,歡迎參考原始碼。
通知的另一種儲存方式
這裡稍微提一下另外一種形式的通知儲存,即上文中提到的非冗餘形式。不過該方式需要修改 notifications 表的預設表結構。
Schema::create('notifications', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->string('type');
$table->morphs('notifiable');
// $table->json('data')->nullable()->comment('target/content/trigger');
$table->morphs('triggerable')->nullable();
$table->morphs('targetable')->nullable()
$table->morphs('contentable')->nullable();
$table->timestamp('read_at')->nullable();
$table->timestamps();
});
通過表結構相信你已經一目瞭然。萬變不離其宗,我們始終都是在圍繞著 trigger / target / content 轉圈圈。
當然如果你瞭解 「 MySQL 生成列 」的話,完全可以寫出下敘語句,將通知從冗餘形式平滑過渡到到非冗餘形式。
$table->string('targetable_id')->virtualAs('data->>"$.target.id"')->index();
有了這樣的表結構,關聯關係走起來,需要的資料如 文章的 點贊量 / 閱讀量 等等都能夠得到,這裡就不詳細描述程式碼編寫了。後續簡書的使用者動態模組會再次運用這種非冗餘結構的編碼,到時再深入講解相關的細節。
通知壓縮
我們的通知採用了冗餘形式儲存,所以資料儲存空間優化是必須考慮的一個點。尤其是在點贊這類通知中,對資料的浪費是非常巨大的。
因此可以通過類似下面這樣的方式壓縮未讀通知。
當然,如果你選擇非冗餘形式儲存通知資料,那麼將難以進行資料壓縮。
Notification 元件提供了通知後 事件 ,所以相應的邏輯將會在該事件的監聽者中完成。邏輯比較簡單,直接看編碼吧
# App\Listeners\CompressNotification
class CompressNotification
{
public function handle(NotificationSent $event)
{
$channel = $event->channel;
if ($channel !== DatabaseChannel::class) {
return;
}
$currentNotification = $event->response;
if (!in_array($currentNotification->type, ['like_post', 'like_comment'])) {
return;
}
$notifiable = $event->notifiable;
// 查詢相同 target 的上一條通知
$previousNotification = $notifiable->unreadNotifications()
->where('data->target->type', $currentNotification->data['target']['type'])
->where('data->target->id', $currentNotification->data['target']['id'])
->where('id', '<>', $currentNotification->id)
->first();
if ($previousNotification) {
$compressCount = $previousNotification->data['compress_count'] ?? 1;
$triggers = $previousNotification->data['triggers'] ?? [$previousNotification->data['trigger']];
$compressCount += 1;
// 最多儲存三個觸發者
if (count($triggers) < 3) {
$triggers[] = $currentNotification->data['trigger'];
}
$previousNotification->delete();
$data = $currentNotification->data;
$data['compress_count'] = $compressCount;
$data['triggers'] = $triggers;
unset($data['trigger']);
$currentNotification->data = $data;
$currentNotification->save();
}
}
}
壓縮後的通知的 data 的 trigger Object 變成了 triggers Array ,並且增加了 compress_count
用來記錄壓縮條數,前端可以通過該欄位來判斷通知是否被壓縮過。
補充
- 相應的 API 如通知列表,標記為已讀等已經完成,歡迎參考原始碼。
- 通常會有一個與通知類似性質的功能,稱為訊息,或者說私信/聊天等。該功能依舊可以使用 Notification 來完成,因為其也是由 行為的觸發者 trigger 、行為主體(可能攜帶 內容) target 、需要通知的使用者 notifiable 構成。但是從解耦的角度來看,將其單獨成一個 「Chat 模組」,且訊息提醒依舊使用 「Notification」 來完成會是更好的選擇。
- 通常會有一個與通知類似結構的功能,稱為使用者動態,或者說使用者日誌。上文中有提到該功能,後續會完成該功能。
- 未讀訊息數在 users 表中冗餘
unread_notification_count
,而不是進行實時計數。
相關
- Api document postman
- 線上除錯工具 telescope
- 本節原始碼 weiwenhao/community-api
- Api Tool weiwenhao/tree-ql
- 上一節 編寫具有描述性的 RESTful API (三): 使用者行為