上一節講到了用戶行爲,由用戶行爲天然的便引出了通知系統。當用戶喜歡了一篇帖子,那麼該帖子的做者應該收到一條提醒。php
laravel 提供了一套 Notification 組件,用於處理通知,其支持經過多種頻道發送通知,包括郵件、短信 、通知還能存儲到數據庫(站內信)以便後續在 Web 頁面中顯示,本節將重點放在站內信。前端
laravel 爲咱們擺平了通知的推送問題,可是還有一個問題,即通知(數據庫)的存儲問題須要咱們處理。通知的存儲一般有兩種作法。laravel
前者的好處是較小的查詢壓力,且數據具備持久性,不會由於被刪帖等問題而影響到通知內容。缺點則是佔用存儲空間,且缺少靈活性,後者則反之。git
源碼中選擇了前者,既默認形式。github
經過抽象能夠獲得,一條通知由三部分組成 行爲的觸發者 trigger 、行爲主體(可能攜帶 內容) target 、須要通知的用戶 notifiable數據庫
此處最讓人疑惑的應該是 行爲的主體,根據實際需求稍微圖解一下。json
已 「Comment Post」爲例,在簡書中其實際的表現行爲以下api
根據上面的分析 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」 行爲爲例,當用戶點贊文章後,須要給文章的做者發送一條通知。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
用來記錄壓縮條數,前端能夠經過該字段來判斷通知是否被壓縮過。
unread_notification_count
,而不是進行實時計數。