Xây dựng tính năng thông báo trong ứng dụng Rails
Bài đăng này đã không được cập nhật trong 8 năm
Chắc hẳn mọi người đã rất quen thuộc với tính năng thông báo(notifications) ở Facebook hay một số mạng xã hội nào đó rồi. Vậy khi mình tự phát triển một web application nho nhỏ mà muốn xây dựng tính năng đó thì sao? Việc cập nhật thông báo từ rất nhiều hành động khác nhau của người dùng như comment, like, mention, v.v..
Bài viết này mình viết về chủ đề này, triển khai tính năng thông báo mỗi khi có bình luận của người dùng vào bài viết.
Tạo ứng dụng và xây dựng cấu trúc cơ sở dữ liệu
Tạo ứng dụng mới
Để tiện hình dung mình sẽ tạo một app với tên là notification-demo
rails new notification-demo
Triển khai xây dựng cơ sở dữ liệu
Về thiết kế cơ sở dữ liệu. Với app demo này mình chỉ xây dựng với 2 bảng là posts
và comments
.
Từ đây, mỗi khi người dùng viết bình luận (comments) vào bài viết của ai đó thì sẽ thông báo tới người viết bài viết (post) đó.
Để có tính năng về phần users
mình dùng thư viện devise
. Bài viết này mình không đi sâu vào
việc sử dụng thư viện này.
https://github.com/plataformatec/devise
$ cd notification-demo
$ rails g scaffold posts title:string content:text user_id:integer
$ rails g scaffold comments content:text user_id:integer post_id:integer
$ rake db:migrate
Hoàn chỉnh các mối quan hệ
Mở models/user.rb
class User < ActiveRecord::Base
...
has_many :posts
has_many :comments
end
Tiếp theo là models/post.rb
class Post < ActiveRecord::Base
...
belongs_to :user
has_many :comments
end
File models/comment.rb
class Comment < ActiveRecord::Base
...
belongs_to :user
belongs_to :post
end
Tạo model Notification
Đây là model để lưu thông báo tới người dùng. Từ đây với đáp ứng nhu cầu về thông báo
mỗi khi có bình luận thì bảng notifications
cần có các trường cần thiết như:
user_id
- lưu trữ thuộc về người dùng nào?notified_by_id
- thông báo được tạo từ ai?notice_type
- kiểu của thông báo (với ngữ cảnh ở đây làcomment
)
Đối với ngữ cảnh bài viết này thì mình chỉ tạo thông báo cho thực thể là các posts
.
Tuy nhiên để có thể mở rộng về thông báo cho các thực thể khác không phải posts
thì
mình sử dụng polymorphic
Như vậy bảng notifications
sẽ có thêm:
notificationable_id
notificationable_type
Thêm một điểm chú ý nữa là khi có các thông báo xuất hiện (giống như FB). Mỗi khi người dùng bấm vào nút thông báo đó thì số lượng thông báo bị hết, tuy nhiên các thông báo ở dạng chưa xem và xem thì khác nhau.
Do vậy mình sẽ thêm vào bảng notifications
2 trường để kiểm tra điều kiện trên:
read
- boolean, default: :falsechecked
- boolean, default: :false
Vậy cuối cùng thông tin về file migrate tạo bảng notifications
như sau:
class CreateNotifications < ActiveRecord::Migration
def change
create_table :notifications do |t|
t.integer :user_id
t.integer :notified_by_id
t.integer :notificationable_id
t.string :notificationable_type
t.string :notice_type
t.boolean :read, default: false
t.boolean :checked, default: false
t.timestamps null: false
end
end
end
Chúng ta cũng nên tạo index
cho một số trường của bảng trên để nâng cao hiệu suất, tốc độ.
class AddIndexToNotifications < ActiveRecord::Migration
def change
add_index :notifications, :user_id
add_index :notifications, :notified_by_id
add_index :notifications, ["notificationable_id", "notificationable_type"], :name => "fk_notificationables"
add_index :notifications, [:read, :checked]
end
end
Migrate database
$ rake db:migrate
Sửa model models/notification.rb
class Notification < ActiveRecord::Base
belongs_to :notified_by, class_name: 'User'
belongs_to :user
belongs_to :post
end
Sửa thêm vào model User
như sau
...
has_many :notifications, dependent: :destroy
...
Đối với model Post
, chúng ta sử dụng polymorphic
...
has_many :notifications, as: :notificationable
...
Tạo notification mỗi khi tạo comment
Đơn giản mỗi khi save
comment thì chúng ta tạo một notification
.
Mở controllers/comment_controller.rb
respond_to do |format|
if @comment.save
@notification = create_notification @comment
format.html { redirect_to @comment, notice: 'Comment was successfully created.' }
format.js
else
format.html { render :new }
format.json { render json: @comment.errors, status: :unprocessable_entity }
end
end
Hàm create_notification
def create_notification comment
return if comment.post.user_id == current_user.id
comment.post.notifications.create! user_id: comment.post.user_id,
notified_by_id: comment.user_id, notice_type: 'comment'
end
Tuy nhiên để tối ưu mã và quản lý dễ dàng hơn cũng như dễ bảo trì và nâng cấp hơn thì
chúng ta có thể tạo ra một service
để thực hiện việc save
comment và tạo notification như
trên trong một transaction
.
Như vậy chúng ta đã tạo ra notification mỗi khi tạo comment.
Hiển thị notifications
Mình sẽ hiển thị số lượng notifications
và dạng dropdown
trên thanh navbar
.
<li class="dropdown">
<%= link_to "#!", class: "dropdown-toggle", id: "noti-count", data: {toggle: "dropdown"},
aria: {haspopup: "true", expanded: "false"} do %>
<span class="fa fa-globe"></span>
<span class="badge badge-danger" data-noti-count="<%= user_signed_in? ? current_user.notifications.num_not_check : 0 %>">
<%= user_signed_in? ? current_user.notifications.num_not_check : 0 %>
</span>
<span class="caret"></span>
<% end %>
<% if user_signed_in? %>
<ul class="dropdown-menu dropdown-notification scrollable-menu" id="user-notifications">
<%= render current_user.notifications %>
</ul>
<% end %>
</li>
Tạo scope num_not_check
trong models/notification.rb
scope :num_not_check, ->{where(checked: false).count}
Thêm style
hiển thị notification
đã xem và chưa xem
.not-read {
background-color: #E9EBEE;
}
.not-read a:hover, .not-read a:focus, .not-read a:active {
background-color: #F6F7F9;
}
Push real-time notifications
Như phần trên chúng ta đã tạo được notifications, và hiển thị số lượng cũng như ở dạng dropdown
Tuy nhiên cần phải F5 browser mới có kết quả. Mình không muốn như vậy, muốn hiển thị ngay sau khi người dùng tạo comments.
Vậy mình sử dụng private_pub
gem để thực hiện việc này. Gem này sử dụng Faye
.
Chi tiết về gem này mọi người có thể xem thêm trên google.
Ngắn gọn lại thì chúng ta sẽ thực hiện subcribe
kênh và publish
vào nơi mà chúng ta muốn.
Mình đặt thêm trong views/posts/show.html.erb
như sau
...
<%= subscribe_to "/comments/new" %>
Sử dụng ajax
cho post comment. Tại comment form thêm vào thuộc tính remote: true
.
Tiếp theo, trong views/comments/create.js.erb
mình thêm như sau
$("#comment_content").val("");
<% publish_to "/comments/new" do %>
var current_user_id_stored = $('meta[name=user-id]').attr("content");
var receiver_id = "<%= @notification.user_id %>";
$("#comments").prepend("<%= j render(partial: @comment) %>");
if (current_user_id_stored == receiver_id) {
$("#user-notifications").prepend("<%= j render(partial: @notification) %>");
var noti_count = $("#noti-count").children(".badge-danger").data("noti-count");
noti_count = noti_count + 1;
$("#noti-count").children(".badge-danger").html(noti_count);
$("#noti-count").children(".badge-danger").data("noti-count", noti_count);
}
<% end %>
Như trên chúng ta thấy biến current_user_id_stored
. Đây là biến nắm giữ giá trị ID của người dùng
hiện tại. Để lấy được giá trị như trên trong layouts/application.rb
thêm vào
<meta content='<%= user_signed_in? ? current_user.id : "" %>' name='user-id'/>
Như vậy là mình đã triển khai cơ bản xong tính năng notifications
.
Vẫn còn cần nhiều cải tiến thêm nữa khi có thời gian. Mong nhận được góp ý từ các bạn.
Cảm ơn bạn đọc!
All rights reserved