Εισαγωγή
Σε αυτό το βοήθημα θα δημιουργήσουμε μια εφαρμογή συνομιλιών (chat) από το μηδέν χρησιμοποιώντας το framework Ruby on Rails και την τεχνολογία των WebSockets.
Ενημέρωση: Δημοσίευσα άλλο ένα βοήθημα για τη δημιουργία docker image της εφαρμογής του παρόντος, μπορείτε να το βρείτε στα αγγλικά για την ώρα εδώ.
Κώδικας και σχόλια
Μπορείτε να βρείτε τον κώδικα του παρόντος άρθρου στο
GitHub.
Για σχόλια, feedback, λάθη κ.λπ. παρακαλώ ανοίξτε ένα
issue στο αποθετήριο.
Τι είναι τα WebSockets;
Πρόκειται ουσιαστικά για ένα πρωτόκολλο που επιτρέπει την αμφίδρομη επικοινωνία μεταξύ πελάτη και εξυπηρετητή μιας διαδικτυακής εφαρμογής μέσω μιας και μόνης σύνδεσης TCP μακράς διαρκείας.
The WebSocket protocol enables interaction between a web browser (or other client application) and a web server with lower overheads, facilitating real-time data transfer from and to the server.
This is made possible by providing a standardized way for the server to send content to the client without being first requested by the client, and allowing messages to be passed back and forth while keeping the connection open. In this way, a two-way ongoing conversation can take place between the client and the server.
The communications are done over TCP port number 80 (or 443 in the case of TLS-encrypted connections), which is of benefit for those environments which block non-web Internet connections using a firewall. Similar two-way browser-server communications have been achieved in non-standardized ways using stopgap technologies such as Comet.
– WebSocket @ Wikipedia
Γιατί να χρησιμοποιήσουμε WebSockets;
Υποθέστε ότι θέλετε να δημιουργήσετε μια ιστοσελίδα που εμφανίζει τις καταστάσεις κάποιων διαδικασιών που βρίσκονται υπό εκτέλεση.
Χωρίς τη χρήση WebSockets, θα έπρεπε να κάνετε ένα από τα παρακάτω:
- να χρησιμοποιήσετε AJAX με Javascript intervals για να ζητάτε και να εμφανίζεται τις τρέχουσες καταστάσεις των διαδικασιών
- να φορτώνεται ξανά τη σελίδα κάθε x δευτερόλεπτα (
<meta http-equiv="refresh" content="x">
)
- να προσθέσετε ένα μήνυμα στη σελίδα τύπου “Οι καταστάσεις δεν ενημερώνονται αυτόματα ¯\_(ツ)_/¯ Πατήστε εδώ για να φορτώσετε ξανά τη σελίδα.”
Και οι τρεις παραπάνω προτάσεις θα κατέληγαν στο να ζητήσουν από τον εξυπηρετητή τις καταστάσεις των διαδικασιών ακόμη και να αυτές δεν είχαν αλλάξει.
Τα WebSockets είναι αυτά που θα μας επιτρέψουν αυτού του είδους η επικοινωνία να λάβει χώρα κατά απαίτηση. Το κόστος αυτής της λειτουργικότητας είναι η διατήρηση TCP συνδέσεων μεταξύ του εξυπηρετητή και όλων των πελατών του (μια για κάθε ανοιγμένη σελίδα στον φυλλομετρητή).
Χτίζοντας την εφαρμογή
Θα χτίσουμε την εφαρμογή χρησιμοποιώντας:
- Ruby: έκδοση 2.6.2
- Rails: έκδοση 5.2.3
Στήνοντας το περιβάλλον
Θα εγκαταστήσουμε τις κατάλληλες εκδόσεις της Ruby και του Ruby on Rails.
Εγκατάσταση Ruby
Χρησιμοποιώ το πρόγραμμα rvm για να διαχειρίζομαι τις εκδόσεις της Ruby που εγκαθιστώ στο σύστημά μου. Για να εγκαταστήσετε την Ruby του παρόντος βοηθήματος εκτελέστε:
Εγκατάσταση Ruby on Rails
Δημιουργήστε ένα φάκελο στο σύστημά σας με το όνομα rails-chat-tutorial
.
Περιηγηθείτε σε αυτόν τον φάκελο και δημιουργήστε τα παρακάτω αρχεία:
.ruby-version
.ruby-gemset
Με αυτά τα δύο αρχεία, ενημερώνουμε τον rvm (Ruby version manager) ότι όταν εργαζόμαστε σε αυτόν τον φάκελο, θέλουμε να γίνεται χρήση της συγκεκριμένης έκδοσης Ruby (.ruby-version
) και των συγκεκριμένων βιβλιοθηκών που έχουν εγκατασταθεί στο gemset με το όνομα rails-chat-tutorial
(.ruby-gemset
).
Τώρα, αν περιηγηθείτε ξανά στον φάκελο θα δείτε κάτι σαν τα παρακάτω:
$ cd .
ruby-2.6.2 - #gemset created /home/iridakos/.rvm/gems/ruby-2.6.2@rails-chat-tutorial
ruby-2.6.2 - #generating rails-chat-tutorial wrappers...........
Εγκαταστήστε την επιθυμητή έκδοση του Ruby on Rails με:
gem install rails -v 5.2.3
Δημιουργία της εφαρμογής
Είμαστε έτοιμοι να δημιουργήσουμε την rails εφαρμογή, στη γραμμή εντολών πληκτρολογήστε:
Σημείωση: Δεν προσδιορίσαμε κάποιο όνομα για την εφαρμογή οπότε το framework θα χρησιμοποιήσει το όνομα του φακέλου: rails-chat-tutorial
.
Το Rails θα δημιουργήσει όλα τα αρχεία της εφαρμογής και θα εγκαταστήσει τις απαιτούμενες βιβλιοθήκες (gems).
Ας ξεκινήσουμε την εφαρμογή για να επιβεβαιώσουμε ότι όλα είναι εντάξει.
You should see something like:
=> Booting Puma
=> Rails 5.2.3 application starting in development
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.12.1 (ruby 2.6.2-p47), codename: Llamas in Pajamas
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://localhost:3000
Use Ctrl-C to stop
Ανοίξτε έναν φυλλομετρητή και επισκεφθείτε τη διεύθυνση: http://localhost:3000
. Αν βλέπετε το παρακάτω είμαστε εντάξει για να συνεχίσουμε. Αν όχι, δε με ξέρετε δε σας ξέρω :P
Χρήστες και βιβλιοθήκη devise
Θα χρησιμοποιήσουμε την φοβερή λύση της βιβλιοθήκης devise για πιστοποίηση χρηστών.
Προσαρτήστε την παρακάτω απαίτηση βιβλιοθήκης στο τέλος του αρχείου Gemfile
βρίσκεται στον κεντρικό φάκελο της εφαρμογής (Rails root directory).
Στη γραμμή εντολών, εγκαταστήστε τη νέα βιβλιοθήκη εκτελώντας:
Ολοκληρώστε την ενσωμάτωση της devise
με:
rails generate devise:install
Θα δημιουργήσουμε το μοντέλο που αναπαριστά τους χρήστες μας χρησιμοποιώντας τις γεννήτριες της devise
.
Εκτελέστε στη γραμμή εντολών:
rails generate devise User username:string
Σημείωση: προσθέσαμε ένα επιπλέον χαρακτηριστικό username
στο μοντέλο (πέραν των όσων εξ ορισμού παρήγαγε η βιβλιοθήκη) ούτως ώστε να έχουμε κάτι πιο φιλικό να εμφανίζουμε για τους χρήστες αντί της διεύθυνσης email.
Ανοίξτε το αρχείο προσαρμογής της βάσης (migration) που βρίσκεται στην τοποθεσία db/migrate/<datetime>_devise_create_users.rb
και προσθέστε ένα ευρετήριο μοναδικότητας (unique index) για το χαρακτηριστικό που προσθέσαμε (username
)[4]:
add_index :users, :username, unique: true
Βρείτε τη γραμμή που ορίζει τη στήλη username
αλλάξτε την ως εξής:
t.string :username, null: false
για να δηλώσουμε το χαρακτηριστικό ως υποχρεωτικό.
Στη συνέχεια, στο μοντέλο User
που βρίσκεται στην τοποθεσία app/models/user.rb
προσθέστε τον παρακάτω κανόνα επικύρωσης της μοναδικότητας και της υποχρεωτικής παρουσίας του χαρακτηριστικού:
validates :username, uniqueness: true, presence: true
Τέλος, προσαρμόστε τη βάση με:
Δωμάτια και μηνύματα
Κάθε μήνυμα θα εμφανίζεται σε ένα δωμάτιο.
Ας δημιουργήσουμε και τα δύο.
Χρησιμοποιήστε την παρακάτω εντολή για δημιουργήσετε το δωμάτιο - Room
:
rails generate resource Room name:string:uniq
και την ακόλουθη εντολή για να δημιουργήσετε το μήνυμα δωματίου - RoomMessage
:
rails generate resource RoomMessage room:references user:references message:text
Τώρα θα ορίσουμε τις σχέσεις αυτών των δύο[7].
Ανοίξτε το αρχείο που βρίσκεται στην τοποθεσία app/models/room.rb
και προσθέστε την παρακάτω σχέση μέσα στην κλάση:
has_many :room_messages, dependent: :destroy,
inverse_of: :room
Ανοίξτε το αρχείο που βρίσκεται στην τοποθεσία app/models/room_message.rb
και προσθέστε τις παρακάτω σχέσεις:
belongs_to :user
belongs_to :room, inverse_of: :room_messages
Προσαρμόστε τη βάση με την εντολή:
Τώρα μπορούμε να ορίσουμε με τέτοιο τρόπο τις διαδρομές (routes) ώστε τα αιτήματα στην αρχική σελίδα (root requests) να σερβίρονται από την ενέργεια index
του ελεγκτή RoomsController
(που δημιουργήθηκε αυτόματα όταν εκτελέσαμε την εντολή για τη δημιουργία του πόρου RoomMessage
παραπάνω) - RoomsController#index
.
Ανοίξτε το αρχείο που βρίσκεται στην τοποθεσία config/routes.rb
και αλλάξτε τα περιεχόμενά του στα παρακάτω:
Rails.application.routes.draw do
devise_for :users
root controller: :rooms, action: :index
resources :room_messages
resources :rooms
end
Επανεκκινήστε τον εξυπηρετητή και προσπαθήστε να περιηγηθείτε στην αρχική σελίδα της εφαρμογής (http://localhost:3000).
Θα δείτε το παρακάτω λάθος, μην ανησυχείτε:
Πρέπει να δημιουργήσουμε την ενέργεια index
στον ελεγκτή RoomsController
. Ανοίξτε τον ελεγκτή που βρίσκεται στην τοποθεσία app/controllers/rooms_controller.rb
και αλλάξτε τα περιεχόμενά του στα παρακάτω:
class RoomsController < ApplicationController
def index
end
end
Κατόπιν, δημιουργήστε το αρχείο app/views/rooms/index.html.erb
και για την ώρα απλά προσθέστε μόνο την παρακάτω επικεφαλίδα:
Φορτώστε ξανά την αρχική σελίδα και voilà.
Προσθήκη πιστοποίησης χρηστών
Θέλουμε όλοι οι χρήστες να είναι πιστοποιημένοι πριν να μπορέσουν να συμμετάσχουν σε συνομιλίες οπότε θα προσθέσουμε την παρακάτω γραμμή στην κλάση ApplicationController
που βρίσκεται στην τοποθεσία app/controllers/application_controller.rb και από την οποία κληρονομούν οι ελεγκτές μας:
before_action :authenticate_user!
Αν πλοηγηθούμε εκ νέου στην αρχική σελίδα http://localhost:3000
θα πρέπει να ανακατευθυνθούμε στη σελίδα πιστοποίησης χρηστών [10].
Πριν συνεχίσουμε με τα ωραία, ας εξοπλίσουμε πρώτα την εφαρμογή με κάποια καλά χαρακτηριστικά.
Προσθήκη bootstrap
Θα χρησιμοποιήσουμε το Bootstrap και θα το ενσωματώσουμε στην εφαρμογή χρησιμοποιώντας τη βιβλιοθήκη bootstrap-rubygem.
Ακολουθώντας τις οδηγίες της βιβλιοθήκης, προσθέστε τις παρακάτω απαιτήσεις βιβλιοθηκών στο μητρώο των απαιτήσεων Gemfile
.
gem 'bootstrap', '~> 4.3.1'
gem 'jquery-rails'
και εκτελέστε την εντολή bundle
για να κατέβουν και να εγκατασταθούν.
Αλλάξτε την κατάληξη του αρχείου app/assets/stylesheets/application.css
σε scss
και αντικαταστήστε τα περιεχόμενά του με:
Προσθέστε τις ακόλουθες γραμμές στο αρχείο app/assets/javascript/application.js
αμέσως πριν τη γραμμή //= require_tree .
[9]:
//= require jquery3
//= require popper
//= require bootstrap-sprockets
Θα χρησιμοποιήσουμε αυτή την εξαιρετική βιβλιοθήκη για να δημιουργούμε τις φόρμες μας εύκολα.
Προσθέστε την απαίτηση στο αρχείο Gemfile
και εκτελέστε την εντολή bundle
για να το εγκαταστήσετε.
Τέλος, ολοκληρώστε την ενσωμάτωση στην εφαρμογή χρησιμοποιώντας:
rails generate simple_form:install --bootstrap
Σημείωση: Χρησιμοποιήσαμε την οδηγία –bootstrap στην παραπάνω εντολή αφού το bootstrap
είναι το CSS framework που θα χρησιμοποιούμε.
Η βιβλιοθήκη devise
χρησιμοποιεί τις δικές τις προβολές (views) για πιστοποιήσεις, καταχωρήσεις νέων χρηστών κ.λπ. Παρόλα αυτά, έχουμε τρόπο να προσαρμόσουμε αυτές τις προβολές και εφόσον επιλέξαμε να χρησιμοποιήσουμε τις βιβλιοθήκες bootstrap
και simple_form
, μπορούμε να παράξουμε αυτές τις προβολές ούτως ώστε οι επιλογές μας να γίνουν σεβαστές.
Στη γραμμή εντολών σας:
rails generate devise:views
Η προβολή για την πιστοποίηση των χρηστών βρίσκεται στην τοποθεσία app/views/devise/sessions/new.html.erb
και για την καταχώρηση χρηστών στην τοποθεσία app/views/devise/registrations/new.html.erb
. Ανοίξτε αυτά τα δύο αρχεία και αλλάξτε τις κλάσεις των κουμπιών υποβολής αντικαθιστώντας την παρακάτω γραμμή[6]:
<%= f.button :submit, "Sign up" %>
με
<%= f.button :submit, "Sign up", class: 'btn btn-success' %>
ώστε τα κουμπιά να εμφανίζονται ακολουθώντας το στυλ του bootstrap.
Πριν δούμε τις αλλαγές που κάναμε, ας κάνουμε ένα τελευταίο πράγμα στην προκαθορισμένη δομή των προβολών μας (default layout).
Ανοίξτε το αρχείο app/views/layouts/application.html.erb
και αντικαταστήστε τα περιεχόμενά του με τα παρακάτω:
<!DOCTYPE html>
<html>
<head>
<title>RailsChatTutorial</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-12">
<%= yield %>
</div>
</div>
</div>
</body>
</html>
ώστε να χρησιμοποιούμε το Bootstrap’s πλέγμα (grid) στις προβολές.
Περιηγηθείτε στην αρχική σελίδα http://localhost:3000
και δείτε τι δημιουργήσαμε.
Ας προσπαθήσουμε να κάνουμε καταχώρηση χρήστη ακολουθώντας το σύνδεσμο Sign up
της φόρμας:
Όπως βλέπετε, δεν υπάρχει πεδίο για τη συμπλήρωση του ονόματος χρήστη (username
) που προσθέσαμε όταν δημιουργήσαμε το μοντέλο User
. Για να μπορούν οι χρήστες να καταχωρούν το όνομά τους πρέπει:
- να προσθέσουμε το πεδίο στη φόρμα
- να ενημερώσουμε τη βιβλιοθήκη
devise
ώστε να αναγνωρίζει και να δέχεται το νέο χαρακτηριστικό (username
) διαφορετικά ο ελεγκτής ApplicationController
θα το αγνοεί όταν θα υποβάλλουμε τη φόρμα.
Για να προσθέσουμε το πεδίο στη φόρμα καταχώρησης χρηστών, ανοίξτε το αρχείο app/views/devise/registrations/new.html.erb
και προσθέστε αυτές τις γραμμές μεταξύ των πεδίων email
και password
.
<%= f.input :username,
required: true %>
Κατόπιν, ανοίξτε τον ελεγκτή της εφαρμογής app/controllers/application_controller.rb
για να ρυθμίσετε καταλλήλως τα επιτρεπόμενα χαρακτηριστικά. Αλλάξτε τα περιεχόμενά του στα παρακάτω:
class ApplicationController < ActionController::Base
before_action :authenticate_user!
before_action :configure_permitted_parameters, if: :devise_controller?
protected
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_up, keys: [:email, :username])
end
end
Αυτό ήταν, περιηγηθείτε ξανά στην αρχική σελίδα και δοκιμάστε να καταχωρηθείτε ως χρήστες[5].
Καθάρισμα μη χρησιμοποιούμενων στοιχείων
Δε θα χρησιμοποιήσουμε coffee script
ή τα turbolinks
οπότε ας αφαιρέσουμε τις σχετικές αναφορές.
Ανοίξτε το Gemfile
και αφαιρέστε τις παρακάτω γραμμές:
# Use CoffeeScript for .coffee assets and views
gem 'coffee-rails', '~> 4.2'
# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
gem 'turbolinks', '~> 5'
Ανοίξτε το αρχείο app/assets/javascripts/application.js
και αφαιρέστε την παρακάτω γραμμή:
Ανοίξτε το app/views/layouts/application.html.erb
και αλλάξτε τις παρακάτω γραμμές[3] από:
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
σε
<%= stylesheet_link_tag 'application', media: 'all' %>
<%= javascript_include_tag 'application' %>
Ελέγξτε ότι ο φάκελος app/assets/javascripts
δεν έχει αρχεία με κατάληξη .coffee
και αν έχει, αφαιρέστε τα.2
Στη γραμμή εντολών τέλος, εκτελέστε:
για να καθαρίσετε ότι αρχεία coffee scripts είχαν ενδεχομένως ενταχθεί στην cache.
Αυτά. Επανεκκινήστε τον εξυπηρετητή.
Προσθήκη μπάρας πλοήγησης
Για να βελτιώσουμε τη χρηστικότητα της εφαρμογής θα προσθέσουμε μια μπάρα πλοήγησης.
Δημιουργήστε το φάκελο app/views/shared
και ένα αρχείο μέσα του με το όνομα _navigation_bar.html.erb
. Αυτό θα είναι ένα απόσπασμα (partial) που θα εμφανίζει την μπάρα πλοήγησης και που στη συνέχεια θα προσθέσουμε στην κεντρική δομή των ιστοσελίδων της εφαρμογής (application layout) για να εμφανίζεται σε όλες τις σελίδες. Προσθέστε μέσα του τα παρακάτω:
<nav class="navbar navbar-expand-lg navbar-dark bg-dark justify-content-between">
<a class="navbar-brand" href="#">Rails chat tutorial</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#nav-bar-collapse" aria-controls="navbarColor01" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<% if current_user %>
<div class="dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<img class="avatar" src="<%= gravatar_url(current_user) %>">
<%= current_user.username %>
</a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
<%= link_to 'Logout', destroy_user_session_path, method: :delete, class: 'dropdown-item' %>
</div>
</div>
<% end %>
</nav>
Δώστε προσοχή στη γραμμή gravatar_url(current_user)
. Αυτή είναι μια βοηθητική μέθοδος που θα χρησιμοποιούμε για να εμφανίζουμε την εικόνα προφίλ (gravatar
) των πιστοποιημένων χρηστών. Δεν είναι συστημική μέθοδος, θα την υλοποιήσουμε εμείς αλλά είναι αρκετά απλή.
Ανοίξτε το αρχείο με τις βοηθητικές μεθόδους app/helpers/application_helper.rb
και προσθέστε τον παρακάτω κώδικα:
def gravatar_url(user)
gravatar_id = Digest::MD5::hexdigest(user.email).downcase
url = "https://gravatar.com/avatar/#{gravatar_id}.png"
end
Σημείωση:
- Όπως μπορείτε να δείτε το όνομα του χρήστη, η εικόνα προφίλ και ο σύνδεσμος εξόδου από την εφαρμογή θα εμφανίζονται μόνο όταν ο χρήστης έχει συνδεθεί.
Η εικόνα προφίλ έχει μια CSS κλάση avatar
. Πρέπει να ορίσουμε αυτήν την κλάση σε κάποιο αρχείο stylesheet της εφαρμογής. Δημιουργήστε ένα αρχείο (στο οποίο θα συγκεντρώσουμε όλες τις CSS κλάσεις που θα χρησιμοποιήσουμε πέραν αυτών που παρέχονται από το bootstrap
) με το όνομα app/assets/stylesheets/rails-chat-tutorial.scss
.
Για την ώρα προσθέστε τον κανόνα για την εικόνα προφίλ:
.avatar {
max-height:30px;
border-radius: 15px;
width:auto;
vertical-align:middle;
}
και ανοίξτε το αρχείο application.scss
για να το προσθέσετε στο μητρώο stylesheet της εφαρμογής. Προσθέστε τη γραμμή:
@import "rails-chat-tutorial"
Τώρα θα προσθέσουμε το απόσπασμα της μπάρας πλοήγησης που δημιουργήσαμε στην προκαθορισμένη δομή των σελίδων της εφαρμογής. Ανοίξτε το αρχείο app/views/layouts/application.html.erb
και αλλάξτε τα περιεχόμενά του στα παρακάτω:
<!DOCTYPE html>
<html>
<head>
<title>RailsChatTutorial</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag 'application', media: 'all' %>
<%= javascript_include_tag 'application' %>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-12">
<%= render partial: 'shared/navigation_bar' %>
<div class="my-3">
<%= yield %>
</div>
</div>
</div>
</div>
</body>
</html>
Φορτώστε ξανά την αρχική σελίδα και δείτε την μπάρα.
Τέλεια. Συμπληρώστε τα επιθυμητά στοιχεία εισόδου σας και υποβάλετε τη φόρμα.
Διαχείριση δωματίων
Θα δημιουργήσουμε μια απλή δομή για τα δωμάτια.
- Μια μικρή στήλη που θα εμφανίζει κάθετα όλα τα διαθέσιμα δωμάτια
- Μια μεγάλη στήλη που θα περιέχει τα μηνύματα και τη φόρμα υποβολής μηνύματος.
Οι αρχική σελίδα των δωματίων θα έχει τη δεξιά στήλη άδεια, η τελευταία θα εμφανίζεται μόνο όταν ο χρήστης έχει επιλέξει συγκεκριμένο δωμάτιο.
Τέλος, στην αρχική σελίδα των δωματίων θα παρέχουμε και την επιλογή για δημιουργία νέου δωματίου.
Αρχική σελίδα δωματίων
Πρώτα θα πρέπει να φορτώσουμε όλα τα διαθέσιμα δωμάτια στον ελεγκτή RoomsController
. Ανοίξτε το αρχείο app/controllers/rooms_controller.rb
και αλλάξτε την μέθοδο index
όπως παρακάτω:
def index
@rooms = Room.all
end
Ανοίξτε την προβολή app/views/rooms/index.html.erb
και αλλάξτε τα περιεχόμενά της στα παρακάτω[8]:
<div class="row">
<div class="col-12 col-md-3">
<div class="mb-3">
<%= link_to new_room_path, class: "btn btn-primary" do %>
Create a room
<% end %>
</div>
<% if @rooms.present? %>
<nav class="nav flex-column">
<% @rooms.each do |room| %>
<%= link_to room.name, room_path(room), class: "nav-link room-nav-link" %>
<% end %>
</nav>
<% else %>
<div class="text-muted">
The are no rooms
</div>
<% end %>
</div>
<div class="col">
<div class="alert alert-primary">
<h4 class="alert-heading">
Welcome to the RailsChatTutorial!
</h4>
<p>
We need to talk.
</p>
<hr />
<p>
You can create or join a room from the sidebar.
</p>
</div>
</div>
Αν υπάρχουν δωμάτια, η αριστερή στήλη θα εμφανίζει μια κάθετη πλοήγηση με συνδέσμους που οδηγούν στη σελίδα κάθε δωματίου. Η δεξιά στήλη εμφανίζει ένα απλό μήνυμα καλωσορίσματος στην εφαρμογή.
Πατώντας το κουμπί Create a room
εμφανίζεται ένα αναμενόμενο λάθος καθώς ο ελεγκτής δεν υποστηρίζει ακόμη την ενέργεια δημιουργίας δωματίου.
Δημιουργία και επεξεργασία δωματίου
Θα ορίσουμε τις ενέργειες για τη δημιουργία και την επεξεργασία ενός δωματίου.
Ανοίξτε τον ελεγκτή των δωματίων app/controllers/rooms_controller.rb
και αλλάξτε τα περιεχόμενά του ως εξής:
class RoomsController < ApplicationController
# Loads:
# @rooms = all rooms
# @room = current room when applicable
before_action :load_entities
def index
@rooms = Room.all
end
def new
@room = Room.new
end
def create
@room = Room.new permitted_parameters
if @room.save
flash[:success] = "Room #{@room.name} was created successfully"
redirect_to rooms_path
else
render :new
end
end
def edit
end
def update
if @room.update_attributes(permitted_parameters)
flash[:success] = "Room #{@room.name} was updated successfully"
redirect_to rooms_path
else
render :new
end
end
protected
def load_entities
@rooms = Room.all
@room = Room.find(params[:id]) if params[:id]
end
def permitted_parameters
params.require(:room).permit(:name)
end
end
Σημείωση: φορτώνουμε από πριν τις μεταβλητές που κρατούν τα δωμάτια (@rooms
) και το τρέχον δωμάτιο (@room
) ώστε να είναι διαθέσιμες σε όλες τις ενέργειες χρησιμοποιώντας την οδηγία before_action :load_entities
(action hook).
Θα δημιουργήσουμε μια απλή φόρμα για το μοντέλο Room
και θα τη χρησιμοποιήσουμε και για τη δημιουργία αλλά και για την επεξεργασία ενός δωματίου. Δημιουργήστε την προβολή app/views/rooms/_form.html.erb
και προσθέστε τα παρακάτω:
<%= simple_form_for @room do |form| %>
<%= form.input :name %>
<%= form.submit "Save", class: 'btn btn-success' %>
<% end %>
Στη συνέχεια, δημιουργήστε τις προβολές των ενεργειών new
και edit
αντίστοιχα:
app/views/rooms/new.html.erb
<h1>
Creating a room
</h1>
<%= render partial: 'form' %>
app/views/rooms/edit.html.erb
<h1>
Editing room <%= @room.name %>
</h1>
<%= render partial: 'form' %>
Ώρα να φτιάξουμε το πρώτο μας δωμάτιο. Από την αρχική σελίδα των δωματίων, πατήστε το κουμπί Create a room
Σώστε το και ορίστε:
Προσθέστε την παρακάτω κλάση στο αρχείο app/assets/stylesheets/rails-chat-tutorial.scss
για να βελτιώσουμε την εμφάνιση των δωματίων.
.room-nav-link {
border: 1px solid lighten($primary, 40%);
background: lighten($primary, 45%);
& + .room-nav-link {
border-top: 0 none;
}
}
Σημείωση: Θα προσθέσουμε παρακάτω τον σύνδεσμο επεξεργασίας (edit
) ενός δωματίου.
Πριν προχωρήσουμε στη σελίδα προβολής ενός δωματίου, θα αλλάξουμε τον κώδικα της αρχικής σελίδας των δωματίων ώστε να μπορούμε να χρησιμοποιήσουμε την αριστερή στήλη και μέσα στη σελίδα ενός δωματίου.
Δημιουργήστε το απόσπασμα προβολής app/views/rooms/_rooms.html.erb
με τα εξής περιεχόμενα:
<div class="mb-3">
<%= link_to new_room_path, class: 'btn btn-primary' do %>
Create a room
<% end %>
</div>
<% if @rooms.present? %>
<nav class="nav flex-column">
<% @rooms.each do |room| %>
<%= link_to room.name, room_path(room), class: 'nav-link room-nav-link' %>
<% end %>
</nav>
<% else %>
<div class="text-muted">
The are no rooms
</div>
<% end %>
και προσαρμόστε την αρχική σελίδα των δωματίων app/views/rooms/index.html.erb
ώστε να το χρησιμοποιεί:
<div class="row">
<div class="col-12 col-md-3">
<%= render partial: 'rooms' %>
</div>
<div class="col">
<div class="alert alert-primary">
<h4 class="alert-heading">
Welcome to the RailsChatTutorial!
</h4>
<p>
We need to talk.
</p>
<hr />
<p>
You can create or join a room from the sidebar.
</p>
</div>
</div>
</div>
Σελίδα δωματίου
Προσθέστε την ενέργεια προβολής ενός δωματίου show
στον ελεγκτή των δωματίων app/controllers/rooms_controller.rb
:
def show
@room_message = RoomMessage.new room: @room
@room_messages = @room.room_messages.includes(:user)
end
Σημειώσεις:
- Δημιουργούμε ένα νέο μήνυμα (
@room_message
) για να το χρησιμοποιήσουμε στην προβολή όταν θα δημιουργήσουμε τη φόρμα για την υποβολή νέου μηνύματος.
- Όταν εμφανίζουμε ένα μήνυμα στο δωμάτιο, προσπελαύνουμε την email διεύθυνση του χρήστη που το δημιούργησε για να μπορούμε να υπολογίσουμε το hash της
gravatar
εικόνας προφίλ του. Χρησιμοποιήσαμε λοιπόν την μέθοδο .includes(:user)
στο ερώτημα της βάσης για τα μηνύματα δωματίου @room_messages
ώστε να προσκομίζονται και οι χρήστες ταυτόχρονα για να αποφύγουμε το πρόβλημα με τα N+1 ερωτήματα[1].
Δημιουργήστε την προβολή δωματίου app/views/rooms/show.html.erb
:
<h1>
<%= @room.name %>
</h1>
<div class="row">
<div class="col-12 col-md-3">
<%= render partial: 'rooms' %>
</div>
<div class="col">
<div class="chat">
<% @room_messages.each do |room_message| %>
<%= room_message %>
<% end %>
</div>
<%= simple_form_for @room_message, remote: true do |form| %>
<div class="input-group mb-3">
<%= form.input :message, as: :string,
wrapper: false,
label: false,
input_html: {
class: 'chat-input'
} %>
<div class="input-group-append">
<%= form.submit "Send", class: 'btn btn-primary chat-input' %>
</div>
</div>
<%= form.input :room_id, as: :hidden %>
<% end %>
</div>
</div>
Σημειώσεις:
- Επαναχρησιμοποιήσαμε το απόσπασμα προβολής
app/views/rooms/_rooms.html.erb
που δημιουργήσαμε στο προηγούμενο βήμα
- Προσθέσαμε ένα
div
με κλάση .chat
και σε αυτό θα εμφανίζονται τα μηνύματα του δωματίου
- Προσθέσαμε μια φόρμα για το μήνυμα
@room_message
που αρχικοποιήσαμε στον ελεγκτή. Επίσης χρησιμοποιήσαμε την οδηγία remote: true
όταν αρχικοποιήσαμε τη φόρμα συνεπώς η υποβολή της θα γίνεται μέσω Ajax.
- Προσθέσαμε ένα κρυφό πεδίο για το χαρακτηριστικό
:room_id
ώστε να είναι διαθέσιμο στον ελεγκτή των μηνυμάτων RoomMessagesController
κατά την υποβολή της φόρμας
Ας ομορφύνουμε τα στοιχεία της συνομιλίας προσθέτοντας τις παρακάτω γραμμές στο αρχείο app/assets/stylesheets/rails-chat-tutorial.scss
:
.chat {
border: 1px solid lighten($secondary, 40%);
background: lighten($secondary, 50%);
height: 50vh;
border-radius: 5px 5px 0 0;
overflow-y: auto;
}
.chat-input {
border-top: 0 none;
border-radius: 0 0 5px 5px;
}
Περιηγηθείτε σε ένα δωμάτιο για να δούμε τι έχουμε κάνει μέχρι στιγμής.
Πατώντας το κουμπή Send
δε συμβαίνει τίποτα στη σελίδα αλλά αν κοιτάξετε το αρχείο καταγραφής (log) του εξυπηρετητή θα παρατηρήσετε το εξής:
AbstractController::ActionNotFound (The action 'create' could not be found for RoomMessagesController):
Ας το διορθώσουμε.
Δημιουργία μηνυμάτων
Αυτό θα είναι απλό. Το μόνο που έχουμε να κάνουμε είναι να υλοποιήσουμε την ενέργεια της δημιουργίας - create
στον ελεγκτή των μηνυμάτων - RoomMessagesController
.
app/controllers/room_messages_controller.rb
class RoomMessagesController < ApplicationController
before_action :load_entities
def create
@room_message = RoomMessage.create user: current_user,
room: @room,
message: params.dig(:room_message, :message)
end
protected
def load_entities
@room = Room.find params.dig(:room_message, :room_id)
end
end
Σημειώσεις:
- φορτώνουμε εκ των προτέρων το δωμάτιο χρησιμοποιώντας την παράμετρο
room_id
που προσθέσαμε προηγουμένως σαν κρυφό πεδίο στη φόρμα του μηνύματος
- δημιουργούμε ένα νέο μήνυμα θέτοντας του ως χρήστη τον χρήστη που είναι πιστοποιημένος κατά την υποβολή
Αν δοκιμάσετε να υποβάλετε ένα μήνυμα τώρα, και πάλι δε θα δείτε να συμβαίνει κάτι στη σελίδα αλλά στα logs του εξυπηρετητή θα δείτε ότι το μήνυμα δημιουργήθηκε επιτυχώς.
Started POST "/room_messages" for ::1 at 2019-04-04 19:24:33 +0300
Processing by RoomMessagesController#create as JS
Parameters: {"utf8"=>"✓", "room_message"=>{"message"=>"My first message", "room_id"=>"8"}, "commit"=>"Send"}
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? ORDER BY "users"."id" ASC LIMIT ? [["id", 1], ["LIMIT", 1]]
↳ /home/iridakos/.rvm/gems/ruby-2.6.2@rails-chat-tutorial/gems/activerecord-5.2.3/lib/active_record/log_subscriber.rb:98
Room Load (0.2ms) SELECT "rooms".* FROM "rooms" WHERE "rooms"."id" = ? LIMIT ? [["id", 8], ["LIMIT", 1]]
↳ app/controllers/room_messages_controller.rb:13
(0.1ms) begin transaction
↳ app/controllers/room_messages_controller.rb:5
RoomMessage Create (0.7ms) INSERT INTO "room_messages" ("room_id", "user_id", "message", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) [["room_id", 8], ["user_id", 1], ["message", "My first message"], ["created_at", "2019-04-04 16:24:33.456641"], ["updated_at", "2019-04-04 16:24:33.456641"]]
↳ app/controllers/room_messages_controller.rb:5
(4.0ms) commit transaction
↳ app/controllers/room_messages_controller.rb:5
No template found for RoomMessagesController#create, rendering head :no_content
Completed 204 No Content in 88ms (ActiveRecord: 5.1ms)
Οι χρήστες περιμένουν το πεδίο του μηνύματος να καθαρίζει μετά την αποστολή ενός μηνύματος. Ας μην τους αφήσουμε να περιμένουν.
Δημιουργήστε ένα αρχείο app/assets/javascripts/room.js
και προσθέστε τα παρακάτω:
$(function() {
$('#new_room_message').on('ajax:success', function(a, b,c ) {
$(this).find('input[type="text"]').val('');
});
});
Συνδέουμε το συμβάν ajax:success
που πυροδοτείται από το Rails στην επιτυχή υποβολή της φόρμας και το μόνο που κάνουμε είναι ότι καθαρίζουμε τα περιεχόμενα του πεδίου κειμένου του μηνύματος.
Φορτώστε εκ νέου τη σελίδα και προσπαθήστε να υποβάλετε ξανά και δείτε την αλλαγή που μόλις κάναμε. Το πεδίο του μηνύματος θα πρέπει να καθαρίσει μετά την υποβολή της φόρμας.
Εμφάνιση μηνυμάτων
Αν φορτώσετε πάλι τη σελίδα του δωματίου θα δείτε κάτι σαν αυτό:
Ας ομορφύνουμε και τα μηνύματα.
Αντικαταστήστε τα περιεχόμενα της προβολής του δωματίου app/views/rooms/show.html.erb
με τα παρακάτω:
<h1>
<%= @room.name %>
</h1>
<div class="row">
<div class="col-12 col-md-3">
<%= render partial: 'rooms' %>
</div>
<div class="col">
<div class="chat">
<% @room_messages.each do |room_message| %>
<div class="chat-message-container">
<div class="row no-gutters">
<div class="col-auto text-center">
<img src="<%= gravatar_url(room_message.user) %>" class="avatar" alt="">
</div>
<div class="col">
<div class="message-content">
<p class="mb-1">
<%= room_message.message %>
</p>
<div class="text-right">
<small>
<%= room_message.created_at %>
</small>
</div>
</div>
</div>
</div>
</div>
<% end %>
</div>
<%= simple_form_for @room_message, remote: true do |form| %>
<div class="input-group mb-3">
<%= form.input :message, as: :string,
wrapper: false,
label: false,
input_html: {
class: 'chat-input'
} %>
<div class="input-group-append">
<%= form.submit "Send", class: 'btn btn-primary chat-input' %>
</div>
</div>
<%= form.input :room_id, as: :hidden %>
<% end %>
</div>
</div>
και προσθέστε τις παρακάτω CSS κλάσεις μέσα στην κλάση .chat:
.chat-message-container {
padding: 5px;
.avatar {
margin: 5px;
}
.message-content {
padding: 5px;
border: 1px solid $primary;
border-radius: 5px;
background: lighten($primary, 10%);
color: $white;
}
& + .chat-message-container {
margin-top: 10px;
}
}
Φορτώστε ξανά τη σελίδα. Magic.
WebSockets - ActionCable
Ήρθε η ώρα να χρησιμοποιήσουμε τα WebSockets μέσω της λειτουργικότητας ActionCable του Rails.
Action Cable seamlessly integrates WebSockets with the rest of your Rails application. It allows for real-time features to be written in Ruby in the same style and form as the rest of your Rails application, while still being performant and scalable. It’s a full-stack offering that provides both a client-side JavaScript framework and a server-side Ruby framework. You have access to your full domain model written with Active Record or your ORM of choice.
– Action Cable Overview @ Ruby on Rails Guides (v5.2.3)
Εγκατάσταση redis
Θα χρησιμοποιήσουμε τον προσαρμογέα (adapter) redis
που αποτελεί μια ασφαλή επιλογή για παραγωγικά περιβάλλοντα σε αντίθεση με την επιλογή async
.
Πρώτα πρέπει να εγκαταστήσετε την εφαρμογή redis στο σύστημά σας.
Για να το εγκαταστήσω σε Ubuntu χρειάστηκε απλά να εκτελέσω τις παρακάτω εντολές.
sudo apt update
sudo apt install redis-server
Για να ελέγξετε ότι η εγκατάσταση ήταν επιτυχής, βεβαιωθείτε ότι παίρνετε PONG όταν εκτελείτε:
$ redis-cli
127.0.0.1:6379> ping
PONG
Ρύθμιση του ActionCable
Αφου εργαζόμαστε σε περιβάλλον ανάπτυξης (development environment), ανοίξτε το αρχείο ρυθμίσεων του στοιχείου ActionCable config/cable.yml
και αντικαταστήστε τα περιεχόμενά του με τα παρακάτω:
development:
adapter: redis
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
channel_prefix: rails-chat-tutorial_development
test:
adapter: async
production:
adapter: redis
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
channel_prefix: rails-chat-tutorial_production
Σημείωση: προσθέσαμε την επιλογή channel_prefix
επειδή:
Additionally, a channel_prefix may be provided to avoid channel name collisions when using the same Redis server for multiple applications
– Action Cable Overview # Redis Adapter @ Ruby on Rails Guides (v5.2.3)
Τέλος, θα προσθέσουμε την απαίτηση βιβλιοθήκης για τον προσαρμογέα redis
στο μητρώο απαιτήσεων Gemfile
:
Μην ξεχνάτε να εκτελείτε bundle
κάθε φορά που αλλάζετε αυτό το αρχείο.
Ρύθμιση devise
για πιστοποίηση συνδέσεων WebSocket
Όταν εγκαθίσταται μια σύνδεση WebSocket, δεν έχουμε πρόσβαση στη συνεδρία του χρήστη (user session) αλλά έχουμε πρόσβαση τα cookies του. Έτσι, για να μπορούμε να πιστοποιήσουμε τον χρήστη χρειάζεται να κάνουμε κάποια πράγματα σχετικά με το devise
(credits to Greg Molnar).
Δημιουργήστε έναν αρχικοποιητή για τα “warden hooks” με το όνομα config/initializers/warden_hooks.rb
και προσθέστε τις παρακάτω γραμμές:
Warden::Manager.after_set_user do |user,auth,opts|
scope = opts[:scope]
auth.cookies.signed["#{scope}.id"] = user.id
end
Warden::Manager.before_logout do |user, auth, opts|
scope = opts[:scope]
auth.cookies.signed["#{scope}.id"] = nil
end
Επεξήγηση: Προσθέτουμε ένα cookie με το αναγνωριστικό του χρήστη (user id) μετά την επιτυχή πιστοποίηση του και το αφαιρούμε κατά την έξοδό του από την εφαρμογή.
Ρύθμιση της σύνδεση WebSocket
Ανοίξτε το αρχείο app/channels/application_cable/connection.rb
και αλλάξτε τα περιεχόμενά του ως εξής:
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
end
private
def find_verified_user
if verified_user = User.find_by(id: cookies.signed['user.id'])
verified_user
else
reject_unauthorized_connection
end
end
end
end
Επεξήγηση:
Here identified_by is a connection identifier that can be used to find the specific connection later. Note that anything marked as an identifier will automatically create a delegate by the same name on any channel instances created off the connection.
– Action Cable Overview # Connection setup @ Ruby on Rails Guides (v5.2.3)
Στη μέθοδο find_verified_user
προσπελαύνουμε το cookie που θέσαμε νωρίτερα στον αρχικοποιητή των warden hooks
.
Δημιουργία του καναλιού δωματίου
A channel encapsulates a logical unit of work, similar to what a controller does in a regular MVC setup.
– Action Cable Overview # Channels @ Ruby on Rails Guides (v5.2.3)
Θα δημιουργήσουμε το κανάλι RoomChannel
στο οποίο θα εγγράφονται όλες οι σελίδες δωματίων.
Δημιουργήστε το κανάλι με το όνομα app/channels/room_channel.rb
και τα εξής περιεχόμενα:
class RoomChannel < ApplicationCable::Channel
def subscribed
room = Room.find params[:room]
stream_for room
# or
# stream_from "room_#{params[:room]}"
end
end
Επεξήγηση:
- Η μέθοδος
subscribed
εκτελείται κάθε φορά που εγγράφεται κάποιος στο κανάλι και είναι υπεύθυνη να ρυθμίσει τη ροή μέσω της οποίας τα δεδομένα θα πηγαινοέρχονται.
Θα ρυθμίσουμε τον κώδικα της σελίδα δωματίου αργότερα με τέτοιο τρόπο ώστε κατά την εγγραφή στο κανάλι να αποστέλλεται και η παράμετρος του δωματίου room
.
Έχουμε δύο επιλογές:
- Να χρησιμοποιήσουμε τη μέθοδο
stream_for
: με αυτό τον τρόπο το Rails αυτόματα παράγει ένα όνομα ροής για το εκάστοτε μοντέλο (room
στην περίπτωσή μας), για παράδειγμα: “room:asdfwer234”. Όταν στη συνέχεια θέλουμε να στείλουμε δεδομένα σε αυτή τη ροή, το μόνο που έχουμε να κάνουμε είναι να εκτελέσουμε τη μέθοδο RoomChannel.broadcast_to(room_object, data)
όπου το Rails θα ανακτήσει αυτόματα το όνομα της ροής βάσει της παραμέτρου room_object
. Με άλλα λόγια, δε χρειάζεται εμείς να υπολογίζουμε το όνομα μιας ροής δωματίου στην οποία θέλουμε να στείλουμε/λάβουμε δεδομένα.
- αυτή η επιλογή είναι μόνο όταν το κανάλι χειρίζεται εγγραφές (subscriptions) που συνδέονται με μοντέλα, στην περίπτωσή μας κάποιο δωμάτιο
- Να χρησιμοποιήσουμε τη μέθοδο
stream_from
: ορίζουμε μόνοι μας το όνομα της ροής και στη συνέχεια, όταν θέλουμε να στείλουμε δεδομένα στη ροή χρησιμοποιούμε: ActionCable.server.broadcast("room_#{a_room_id_here}", data)
.
Διαβάστε περισσότερα εδώ.
Αναμετάδοση μηνυμάτων
Κάθε φορά που δημιουργείται ένα μήνυμα, θέλουμε να το αναμεταδώσουμε στη ροή του καναλιού για το συγκεκριμένο δωμάτιο.
Για να το κάνουμε αυτό, αλλάξτε την ενέργεια δημιουργίας μηνύματος - create
του αντίστοιχου ελεγκτή app/controllers/room_messages_controller.rb
ως εξής:
def create
@room_message = RoomMessage.create user: current_user,
room: @room,
message: params.dig(:room_message, :message)
RoomChannel.broadcast_to @room, @room_message
end
Επεξήγηση: προσθέσαμε τη γραμμή RoomChannel.broadcast_to @room, @room_message
που θα αναμεταδώσει στη ροή του καναλιού του δωματίου (όπως εξηγήσαμε προηγουμένως) το μήνυμα @room_message
μετασχηματισμένο σε μορφή JSON μέσω της μεθόδου to_json
.
Έτσι, στην άλλη πλευρά, στην πλευρά του πελάτη δηλαδή, θα παραλάβουμε την JSON μορφή του μοντέλου RoomMessage
. Για να δούμε πως είναι αυτή:
{
"id":29,
"room_id":8,
"user_id":1,
"message":"My first message",
"created_at":"2019-04-04T17:09:00.637Z",
"updated_at":"2019-04-04T17:09:00.637Z"
}
Έχουμε σκοπό να εμφανίσουμε τα μηνύματα στη σελίδα του δωματίου μέσω Javascript και αυτή η πληροφορία που λαμβάνουμε προς το παρόν δεν είναι αρκετή. Μας λείπει η διεύθυνση της εικόνας προφίλ gravatar
. Ας την προσθέσουμε.
Ανοίξτε το μοντέλου του χρήστη app/models/user.rb
και προσθέστε την παρακάτω μέθοδο.
def gravatar_url
gravatar_id = Digest::MD5::hexdigest(email).downcase
"https://gravatar.com/avatar/#{gravatar_id}.png"
end
Έχουμε ήδη υλοποιήσει αυτή τη μέθοδο στις βοηθητικές μεθόδους app/helpers/application_helper.rb
αλλά δε θα τη χρειαστούμε πλέον οπότε αφαιρέστε τη.
Ενημερώστε το απόσπασμα της μπάρας πλοήγησης app/views/shared/_navigation_bar.html.erb
ώστε ο υπολογισμός της διεύθυνσης της εικόνας προφίλ να γίνεται πλέον από τη μέθοδο που μόλις δημιουργήσαμε:
<img class="avatar" src="<%= current_user.gravatar_url %>">
Ενημερώστε και τη σελίδα δωματίου app/views/rooms/show.html.erb
αλλάξτε τη και εκεί επίσης, με:
<img src="<%= room_message.user.gravatar_url %>" class="avatar" alt="">
Τέλος, θα αλλάξουμε το μετασχηματισμό JSON
των μηνυμάτων - RoomMessage
ώστε να εμπεριέχει και τη διεύθυνση της εικόνας προφίλ:
app/models/room_message.rb
def as_json(options)
super(options).merge(user_avatar_url: user.gravatar_url)
end
Ας επιβεβαιώσουμε ότι ο μετασχηματισμός είναι επιτυχής:
{
"id":29,
"room_id":8,
"user_id":1,
"message":"My first message",
"created_at":"2019-04-04T17:09:00.637Z",
"updated_at":"2019-04-04T17:09:00.637Z",
"user_avatar_url":"https://gravatar.com/avatar/02a28db6886d578f75a820b50f2dd334.png"
}
Μια χαρά, προχωράμε.
Εγγραφή στη ροή δωματίου
Θα προσθέσουμε κάποια δεδομένα στη σελίδα των δωματίων ώστε να μπορούμε να τα χρησιμοποιήσουμε μέσω Javascript για να πραγματοποιούμε εγγραφή στις κατάλληλες ροές κάθε φορά που επισκεπτόμαστε ένα δωμάτιο.
Ανοίξτε το αρχείο app/views/rooms/show.html.erb
και αλλάξτε τη γραμμή στην οποία ορίζουμε το div chat
ως εξής:
<div class="chat" data-channel-subscribe="room" data-room-id="<%= @room.id %>">
Επεξήγηση: Προσθέσαμε δύο χαρακτηριστικά δεδομένων, ένα που ορίζει σε ποιο κανάλι θέλουμε να γραφτούμε (room
) και ένα που ορίζει σε ποιο συγκεκριμένο δωμάτιο βρισκόμαστε (@room.id
).
Στο τέλος του αρχείου προσθέστε το παρακάτω απόσπασμα κώδικα:
<div class="d-none" data-role="message-template">
<div class="chat-message-container">
<div class="row no-gutters">
<div class="col-auto text-center">
<img src="" class="avatar" alt="" data-role="user-avatar">
</div>
<div class="col">
<div class="message-content">
<p class="mb-1" data-role="message-text"></p>
<div class="text-right">
<small data-role="message-date"></small>
</div>
</div>
</div>
</div>
</div>
</div>
Αυτό το απόσπασμα θα χρησιμοποιείται σαν πρότυπο (template) για κάθε εισερχόμενο μήνυμα. Κάθε φορά που έρχεται κάποιο μήνυμα θα:
- κλωνοποιούμε αυτό το απόσπασμα HTML
- θα αλλάζουμε τις τιμές των κατάλληλων στοιχείων του κλωνοποιημένου πλέον αποσπάσματος και τέλος
- θα προσαρτούμε τον παραγόμενο HTML κώδικα στο τέλος του div
chat
.
Τώρα θα δημιουργήσουμε το Javascript που θα είναι υπεύθυνο για τις εγγραφές στις ροές των καναλιών και για τον χειρισμό των εισερχόμενων δεδομένων.
Δημιουργήστε το αρχείο app/assets/javascripts/channels/room_channel.js
και προσθέστε τον παρακάτω κώδικα:
$(function() {
$('[data-channel-subscribe="room"]').each(function(index, element) {
var $element = $(element),
room_id = $element.data('room-id')
messageTemplate = $('[data-role="message-template"]');
$element.animate({ scrollTop: $element.prop("scrollHeight")}, 1000)
App.cable.subscriptions.create(
{
channel: "RoomChannel",
room: room_id
},
{
received: function(data) {
var content = messageTemplate.children().clone(true, true);
content.find('[data-role="user-avatar"]').attr('src', data.user_avatar_url);
content.find('[data-role="message-text"]').text(data.message);
content.find('[data-role="message-date"]').text(data.updated_at);
$element.append(content);
$element.animate({ scrollTop: $element.prop("scrollHeight")}, 1000);
}
}
);
});
});
Επεξήγηση:
- Για κάθε στοιχείο που έχει χαρακτηριστικό δεδομένων (data attribute)
channel-subscribe
με τιμή room
- Δημιούργησε μια εγγραφή (subscription) στο κανάλι “RoomChannel” περνώντας ως παράμετρο με όνομα
room
την τιμή του room-id
χαρακτηριστικού δεδομένων (θυμηθείτε αυτή τη γραμμή στο κανάλι των δωματίων (RoomChannel
): Room.find params[:room]
)
- Όταν έρχονται δεδομένα, κλωνοποίησε το πρότυπο και άλλαξε τα περιεχόμενά του βάσει των χαρακτηριστικών του εισερχόμενου αντικειμένου.
- Πρόσθεσε το νέο περιεχόμενο στο div
chat
και τέλος
- Χρησιμοποίησε animation για να εντυπωσιάσεις τους χρήστες μετακινώντας σταδιακά την τρέχουσα θέση του div μηνυμάτων στο κάτω μέρος του (αν δεν έγινε κατανοητό στα ελληνικά, εννοώ να γίνει smooth scrolling στο bottom του div :).
Αυτάααααααα! Μεγάλο άρθρο, κουράστηκε η γάτα.
Ευχαριστίες
Ευχαριστώ πολύ για τα σχόλια και το feedback!