Ένα chat με JQuery

Όσο κι αν δεν μου άρεσε η σκέψη ότι θα χρειαστεί να μάθω άλλο ένα framework, ήρθε κάποτε ο καιρός που δεν μπορούσα άλλο να αποφύγω το JQuery. Τότε, προκειμένου να αποκτήσω πρακτική εμπειρία με αυτό, αποφάσισα να υλοποιήσω ένα web-based chat. Η φιλοσοφία είναι σχετικά απλή: Ο server διατηρεί ένα chat room (πχ τους συνδεδεμένους χρήστες, τι λέει ο καθένας και πότε). Στην άλλη άκρη, ένα πρόγραμμα-πελάτης χρησιμεύει στο να στέλνει ο χρήστης μηνύματα και, σε τακτά χρονικά διαστήματα, ζητά από τον server ενημέρωση για την κατάσταση του chat room.

Ενώ η οντότητα “χρήστης” ορίζεται από ένα πεδίο username, μία “συζήτηση” είναι ένα σύνολο πλειάδων με τα παρακάτω πεδία:

  • message: τι είπε κάποιος
  • username: ποιος το είπε
  • msg_time: πότε το είπε
  • mark: ένα διατεταγμένο πεδίο (κατά προτίμηση ένας ακέραιος) που προσδιορίζει τη σειρά των μηνυμάτων μέσα στο αρχείο καταγραφής. Μηνύματα με μεγαλύτερα marks καταγράφονται μετά από μηνύματα με μικρότερα.

Όταν το πρόγραμμα-πελάτης ζητά ενημέρωση, όλα τα παραπάνω δεδομένα μεταβιβάζονται με τη μορφή ενός αντικειμένου σε κωδικοποίηση JSON.

Για να αρχίσουμε με την υλοποίηση, θα χρειαστούμε ένα περιβάλλον σε HTML — θα το πούμε chat.html.

<!doctype html>
<html lang="en">
    <head>
		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
		<title>Chat Demo</title>
		<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.1/jquery.min.js"></script>
		<script src="chat.js" type="text/javascript"></script>
		<link href="chat.css" type="text/css" rel="stylesheet"/> 
 	</head>
	<body>
		<div id='main'>
			<div id="chatbox"><div id="room"></div><div id="room_panel"></div></div>
			<div id="chat_message">
				<label>
					<em>Type your text and press enter:</em>
					<input id="usermsg" type="text" name="usermsg"/>
				</label>
			</div>
		</div>
	</body>
</html>	

Ας δούμε τα κυριότερα στοιχεία:

  • Το chatbox div θα οπτικοποιήσει το chat room. Χωρίζεται σε δύο μέρη, το room και το room_panel. Το πρώτο θα περιέχει τη συζήτηση ενώ το δεύτερο θα παρέχει μία λίστα με τους συνδεδεμένους χρήστες.
  • Το usermsg input θα χρησιμοποιηθεί για να στέλνει ο χρήστης μηνύματα στο server. Όπως φαίνεται και από το label του, όταν πατηθεί το πλήκτρο enter, τα περιεχόμενά του θα αποσταλούν.

Όλη η επικοινωνία με το server θα γίνεται μέσω ενός προγράμματος σε php, το chat.php. Αν η αίτηση δεν είναι τύπου POST, η απάντηση θα περιλαμβάνει το παραπάνω περιβάλλον. Για τις αιτήσεις POST, το πρόγραμμα ελέγχει αν έχει αποσταλεί ένα μη κενό πεδίο με όνομα msg. Αν ναι, το μήνυμα θα πρέπει να καταγραφεί. Αλλιώς, αν παρέχεται αντίστοιχα ένα πεδίο ονόματι mark, το πρόγραμμα επιστρέφει ένα αντικείμενο, κωδικοποιημένο σε JSON, με δύο στοιχεία: τη συζήτηση (conversation) και τους χρήστες (users).

<?php
require "chat_aux.php";
$me=get_username();
if(count($_POST)==0) require "chat.html";
elseif(isset($_POST['msg'])&&($_POST['msg'])) say($me,$_POST['msg']);
elseif(isset($_POST['mark']))
    echo json_encode(
		array(
			'conversation'=>get_conversation($_POST['mark']),
			'users'=>get_active_users(),
			)
		);

Στον παραπάνω κώδικα, υπάρχουν τέσσερις συναρτήσεις που εξαρτώνται από την εκάστοτε εφαρμογή: get_username(), say(), get_conversation() και get_active_users(). Αυτές θα πρέπει να υλοποιηθούν στο αρχείο chat_aux.php, και δεν θα τις αναλύσουμε εδώ. Παρόλα αυτά, ας δούμε τις προδιαγραφές τους:

  • get_username(): επιστρέφει το όνομα χρήστη του συνδεδεμένου χρήστη. Είναι υπεύθυνη να ελέγξει αν ο χρήστης έχει αυθεντικοποιηθεί και/ή να ξεκινήσει μία διαδικασία αυθεντικοποίησης.
  • say($user,$message): καταγράφει το μήνυμα ($message) του χρήστη $user στο αρχείο καταγραφής
  • get_conversation($mark): επιστρέφει μία λίστα με τα τελευταία μηνύματα από το αρχείο καταγραφής, από το $mark έως το πιο πρόσφατο
  • get_active_users(): επιστρέφει μία λίστα με τους συνδεδεμένους χρήστες

Σημειώστε ότι, για τις δύο τελευταίες, το αποτέλεσμα μπορεί να είναι είτε πίνακας με συσχετίσεις (associative array) είτε αντικείμενα (objects) που θα περιέχουν τα πεδία που αναφέρθηκαν παραπάνω.

Τώρα που καλύψαμε την απαραίτητη λειτουργικότητα του server, ας ασχοληθούμε με το κυρίως θέμα του άρθρου: το πρόγραμμα-πελάτη. Θα τοποθετήσουμε όλον τον κώδικα σε ένα αρχείο javascript, το chat.js:

var updTimer, curMark=0, lock=0;

function roomScrollDown()
    {
	$("#room").scrollTop($("#room")[0].scrollHeight);
	}
	
function showUsers(u)
	{
	$('#room_panel').empty();
	for(i=0;i<u.length;i++) 
		$('#room_panel').append("<p><strong><em>"+u[i].username+"</em></strong></p>");
	}

function showConversation(conv)
	{
	if(conv.length)
		{
		scrolled=($('#room').scrollTop()+$('#room').innerHeight()==$('#room')[0].scrollHeight);
		for(i=0;i<conv.length;i++) 
			{
			el=conv[i];
			u="<span title='"+el.msg_time+"'><strong><em>"+el.username+":</em></strong></span>";
			$('#room').append("<p>"+u+" "+el.message+"</p>");
			}
		curMark=el.mark;
		if(scrolled) roomScrollDown();
		}
	}
	
function updateEnv(arr)
	{
	showConversation(arr['conversation']);
	showUsers(arr['users']);
	}
	
function setLock()
	{
	clearTimeout(updTimer);
	lock=1;
	}
	
function clearLock()
	{
	lock=0;
	updTimer=setTimeout(updateRoom,5000);
	}
	
function updateRoom()
	{
	if(lock) 
		setTimeout(updateRoom,1000);
	else
		{
		setLock();
		$.ajax({
			type: "POST",
			data: {mark: curMark},
			dataType: 'json',
			success: updateEnv,
			complete: clearLock
			});
		}
	}
	
function messageSent(msg)
	{
	if(!msg) 
		{
		roomScrollDown();
		updateRoom();
		} 
	else 
		alert(msg);
	}

function sendChatMessage(msg)
	{
	$.ajax({
		type: "POST",
		data: {msg: msg},
		dataType: 'text',
		success: messageSent
		});
	}

$(document).ready(function() 
	{ 
	$('#usermsg').keyup(function(e)
		{
		if(e.keyCode==13) 
			{
			sendChatMessage(usermsg.value);
			usermsg.value='';
			}
		});
	updateRoom(); 
	}); 
    

Αφότου η σελίδα έχει “φορτωθεί”, συνδέουμε μία ρουτίνα χειρισμού του keyup event στο στοιχείο usermsg, κι έπειτα ζητάμε μία αρχική ενημέρωση για την κατάσταση του chatroom καλώντας την updateRoom(). Η ρουτίνα χειρισμού του keyup event, με την προϋπόθεση ότι το enter έχει πατηθεί, καλεί την sendChatMessage() πριν καθαρίσει τα περιεχόμενα του πεδίου εισαγωγής.

Ας δούμε πως δουλεύει καθεμία από τις συναρτήσεις:

sendChatMessage(msg): Αποστέλλει το μήνυμα που πληκτρολόγησε ο χρήστης χρησιμοποιώντας μία αίτηση AJAX POST με την παράμετρο msg ως πεδίο. Αν η αίτηση επιτύχει, καλεί την messageSent().

messageSent(msg): Ελέγχει αν απόκριση του server στην παραπάνω αίτηση ήταν κενή. Αν όχι, ένα μήνυμα εμφανίζεται για να ενημερώσει το χρήστη. Αλλιώς, καλείται η updateRoom() για να ανανεώσει τα περιεχόμενα της συζήτησης. Αλλά πρώτα, πρέπει να σιγουρευτούμε ότι το room div έχει κυλήσει ώστε να φαίνονται τα τελευταία του περιεχόμενα.

updateRoom(): Σκοπός της συνάρτησης αυτής είναι να εκτελέσει μία αίτηση AJAX POST, περιμένοντας ένα αντικείμενο JSON ως απάντηση, το οποίο αντιπροσωπεύει την τρέχουσα κατάσταση της συνομιλίας (βλ. παραπάνω). Αν η αίτηση είναι επιτυχής, καλείται η updateEnv() με το αντικείμενο JSON ως παράμετρο, για να οπτικοποιήσει τα δεδομένα.

Η ασύγχρονη φύση της αίτησης, όπως και το γεγονός ότι η συνάρτηση καλείται ως αποτέλεσμα διαφόρων συμβάντων, θα οδηγήσει κάποια στιγμή στην “παράλληλη” εκτέλεση πολλών αιτήσεων. Αν μία δεύτερη αίτηση διαβάσει το curMark πριν η πρώτη προλάβει να το ενημερώσει, θα λάβει λάθος τμήμα της συζήτησης. Επομένως, θα πρέπει να υλοποιήσουμε κάποιο μηχανισμό κλειδώματος ο οποίος να περιβάλλει την αίτηση αλλά και την κλήση της updateEnv(). Αυτό επιτυγχάνεται καλώντας τη setLock() και την clearLock() πριν και μετά την αίτηση αντίστοιχα. Με αυτόν τον τρόπο, αν το κλείδωμα είναι ενεργό, μπορούμε να καθυστερήσουμε την εκτέλεση της συνάρτησης για κάποιο χρονικό διάστημα.

clearLock(): Θέτει την τιμή του lock στο μηδέν, αφήνοντας έτσι άλλες (εκκρεμούσες ή μελλοντικές) αιτήσεις να εκτελεστούν. Επίσης, προγραμματίζει την επόμενη ενημέρωση σε ένα προκαθορισμένο χρονικό διάστημα.

setLock(): Θέτει την τιμή του lock σε 1, εμποδίζοντας την εκτέλεση άλλων αιτήσεων πριν ξαναγίνει μηδέν, και ακυρώνει τυχόν προγραμματισμένες ενημερώσεις.

updateEnv(): Καλείται όταν επιτύχει μία αίτηση ενημέρωσης κατάστασης, με το αντικείμενο JSON ως παράμετρο arr. Με τη σειρά της, καλεί τις showConversation() και showUsers() για να ενημερωθούν τα room και room_panel divs αντίστοιχα.

showConversation(conv): Η παράμετρος conv είναι το κομμάτι του αντικειμένου JSON που αφορά τη συζήτηση. Καθένα από τα στοιχεία της φορμάρεται και προστίθεται στα περιεχόμενα του room div. Αν το τελευταίο είχε κυλήσει ως το τέρμα κάτω, φροντίζουμε να κυλήσει πάλι.

showUsers(): Ομοίως, η παράμετρος u περιέχει το κομμάτι που αφορά τους χρήστες. Αυτή τη φορά, τα περιεχόμενα του room_panel div αντικαθίστανται, επειδή το u περιέχει πλήρη λίστα συνδεδεμένων χρηστών.

roomScrollDown(): Κυλά το room div ως τέρμα κάτω. Αυτό χρειάζεται σε δυο περιστάσεις: (1) όταν ένας χρήστης στέλνει ένα μήνυμα και εκτελείται ενημέρωση και (2) όταν το chat room ανανεώνει την εικόνα του και είχε ήδη κυλίσει ως το τέρμα.

Κάπου εδώ τελειώνουμε. Μπορείτε να δείτε το πρόγραμμα εν δράσει παρακάτω, σε μία demo υλοποίηση. Και να θυμάστε, τα σχόλια είναι πάντα ευπρόσδεκτα!

3 Comments

  1. Καλό φίλε αλλά σε παρακαλούμε θερμός να δημοσιεύσεις και τον κώδικα των συναρτήσεων php ώστε να έχουμε μια πλήρη εικόνα !

    Γίνετε ;

    1. Σ’ ευχαριστώ για το σχόλιό σου.

      Θα ήταν “εκτός θέματος”, καθώς το άρθρο περιγράφει τον κώδικα που αφορά τον client. Όπως λέω ήδη άλλωστε, οι συναρτήσεις αυτές εξαρτώνται από την εκάστοτε εφαρμογή, και πώς υλοποιείται στο backend.

      Με άλλα λόγια εξαρτάται από τον τρόπο που καταχωρείς τους χρήστες σου, και σε τι ΒΔ, από τον τρόπο που έχεις αποφασίσει να τηρείς τα sessions κλπ. Οι επιλογές είναι άπειρες.

      Παρ’ όλα αυτά, αν βρω το χρόνο, δεσμεύομαι να δημοσιεύσω μερικά παραδείγματα σε επόμενα άρθρα. Stay tuned!

Αφήστε μια απάντηση

Η ηλ. διεύθυνση σας δεν δημοσιεύεται. Τα υποχρεωτικά πεδία σημειώνονται με *