+1

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ư 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à postscomments.

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!


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.