Xây dựng tính năng thông báo trong ứng dụng Rails
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ư ...
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: :false
- checked - 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!