Статьи

GMail Chat — Архитектура Gmail на стороне клиента, часть 3

В этом финале серии из трех частей по клиентской архитектуре GMail мы рассмотрим реализацию окна чата, похожего на GMial, которое вы найдете в веб-интерфейсе GMail. Это базовый обзор и реализация, но я надеюсь, что некоторые найдут его полезным.

Эта реализация не является клиентом Ajax с сервером чата, но вместо этого фокусируется на стороне клиента. Давайте начнем с рассмотрения рабочего примера здесь . Откройте эту ссылку в новом окне или вкладке и после загрузки переместите фокус обратно в это окно / вкладку. Через несколько секунд вы услышите звук уведомления Windows, не можете найти Google, переключитесь назад, и вы обнаружите, что изменился заголовок цвета окна чата, а также заголовок. Давайте посмотрим на код, который сделал это возможным. Сначала наша голая HTML-страница:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
</head>
<body onBlur="lostFocus();" onFocus="gotFocus()">
<strong><font color="#FF0000">Gmail</font> Chat Implementation</strong><br />
(take a new tab , or a new window, or minimize this window , and wait 3 sec, and
come back.)
<div id="Layer1" style="position:absolute; left:14px; top:46px; width:246px; height:258px; z-index:1;border: 1px solid #999999;"></div>
<div id="Layer2" style="position:absolute; left:13px; top:46px; width:247px; height:40px; z-index:2; background-color: #73A6FF ; layer-background-color: #FF8A00; border: 1px none #000000;"></div>
<div id="Layer3" style="position:absolute; left:17px; top:200px; width:237px; height:98px; z-index:3"></div>
<div id="Layer4" style="position:absolute; left:21px; top:268px; width:67px; height:26px; z-index:4">Options</div>
<div id="Layer5" style="position:absolute; left:154px; top:265px; width:71px; height:28px; z-index:5">Popout
</div>
<div id="Layer6" style="position:absolute; left:15px; top:89px; width:240px; height:105px; z-index:6">
<p><strong>me</strong>: Hai Anand</p>
<p><strong>Anand</strong>: Hello</p>
</div>
<div id="Layer7" style="position:absolute; left:216px; top:55px; width:41px; height:26px; z-index:7">
<div align="right"><strong><font color="#FFFFFF" size="6">-</font><font color="#FFFFFF" size="4">
X </font></strong></div>
</div>
<div id="Layer8" style="position:absolute; left:19px; top:205px; width:234px; height:53px; z-index:8;border: 1px solid #999999;">
<div align="left">
<textarea name="textarea" style="width:230px;height:50px;" onclick="changeColorBlue();">Hows life </textarea>
</div>
</div>
<div id="Layer9" style="position:absolute; left:20px; top:58px; width:18px; height:17px; z-index:9"><img src="green.png" width="20" /></div>
<div id="Layer10" style="position:absolute; left:53px; top:56px; width:159px; height:18px; z-index:10"><font color="#FFFFFF"><strong>[email protected]...</strong></font></div>
</body>
</html>

Вы увидите, что есть два вызова функций для тега body для двух разных типов событий, событий onBlur и onFocus. Эти два вызова функций проверят, имеет ли текущее окно фокус или потерял фокус, и на основании этого выполняют определенный блок кода JavaScript. Сначала давайте посмотрим на функцию lostFocus (), которая будет вызываться, когда окно браузера теряет фокус, т.е. onBlur:

function lostFocus()
{
document.title = 'Sajith M.R Says...';
state = 'nonfocus';

played = 0 ;

changeColorRed();

alterTitle();
}

function changeColorRed()
{
document.getElementById('Layer2').style.background = '#FF8A00';

}

function alterTitle()
{
if(state =='nonfocus')
{
if ( document.title == 'Gmail Inbox(1)')
{
if(played == 0)
{
soundManager.play('notify');
played = 1;
}
document.title = 'Sajith M.R Says...';
}
else
document.title = 'Gmail Inbox(1)';

setTimeout("alterTitle()",3000);
}
}

 

Первое, что делает функция lostFocus (), это устанавливает для заголовка документа «Sajith MR Says …». Затем он устанавливает состояние var в nofocus, чтобы указать, что окно потеряло фокус, устанавливает воспроизведение в 0, а затем вызывает функцию changeColorRed (). Это просто выбирает DIV с идентификатором Layer2 из DOM и меняет цвет его фона на # FF8A00 через следующую строку:

document.getElementById('Layer2').style.background = '#FF8A00';

After the function exits the next function call is made to alertTitle(), this one is a little bit more involved.It first checks the variable state to detect whether out chat window has focus or not. As the originating function setthe value of state to ‘nofocus’ we will step into the code block inside the if statement. Next we check whether the title of our document is equal to ‘Gmail Inbox(1)’ and since our originating script set the title to ‘Sajith M.R Says…’ we will jump to outside the  if block and execute the else block.

This now sets the document title to  ‘Gmail Inbox(1)’ and following this calls the alertTitle inside a timout function set to 3000.

setTimeout("alterTitle()",3000);

After having waited 300 miliseconds the alertTitle() function is executed again. We again step into the first if statement as the state has not changed. But now, instead of skipping the second if block and jumping to else, we step into the if statement as our document title boolean check now evaluates to true. Inside the if statement we immediately encounter another boolean if statement. This statement checks whether our notification sound has been played or not.

Our played variable is still at 0 so we will execute the code inside the if statement. This calls the play() function of our soundManager script and passes in the notify parameter. Here is the soundManager.js file:

var isIE = navigator.appName.toLowerCase().indexOf('internet explorer')+1;
var isMac = navigator.appVersion.toLowerCase().indexOf('mac')+1;

function SoundManager(container) {
// DHTML-controlled sound via Flash
var self = this;
this.movies = []; // movie references
this.container = container;
this.unsupported = 0; // assumed to be supported
this.defaultName = 'default'; // default movie

this.FlashObject = function(url) {
var me = this;
this.o = null;
this.loaded = false;
this.isLoaded = function() {
if (me.loaded) return true;
if (!me.o) return false;
me.loaded = ((typeof(me.o.readyState)!='undefined' && me.o.readyState == 4) || (typeof(me.o.PercentLoaded)!='undefined' && me.o.PercentLoaded() == 100));
return me.loaded;
}
this.mC = document.createElement('div');
this.mC.className = 'movieContainer';
with (this.mC.style) {
// "hide" flash movie
position = 'absolute';
left = '-256px';
width = '64px';
height = '64px';
}
var html = ['<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,0,0"><param name="movie" value="'+url+'"><param name="quality" value="high"></object>','<embed src="'+url+'" width="1" height="1" quality="high" pluginspage="http://www.macromedia.com/go/getflashplayer" type="application/x-shockwave-flash"></embed>'];
if (navigator.appName.toLowerCase().indexOf('microsoft')+1) {
this.mC.innerHTML = html[0];
this.o = this.mC.getElementsByTagName('object')[0];
} else {
this.mC.innerHTML = html[1];
this.o = this.mC.getElementsByTagName('embed')[0];
}
document.getElementsByTagName('div')[0].appendChild(this.mC);
}

this.addMovie = function(movieName,url) {
self.movies[movieName] = new self.FlashObject(url);
}

this.checkMovie = function(movieName) {
movieName = movieName||self.defaultName;
if (!self.movies[movieName]) {
self.errorHandler('checkMovie','Exception: Could not find movie',arguments);
return false;
} else {
return (self.movies[movieName].isLoaded())?self.movies[movieName]:false;
}
}

this.errorHandler = function(methodName,message,oArguments,e) {
writeDebug('<div class="error">soundManager.'+methodName+'('+self.getArgs(oArguments)+'): '+message+(e?' ('+e.name+' - '+(e.message||e.description||'no description'):'')+'.'+(e?')':'')+'</div>');
}

this.play = function(soundID,loopCount,noDebug,movieName) {
if (self.unsupported) return false;
movie = self.checkMovie(movieName);
if (!movie) return false;
if (typeof(movie.o.TCallLabel)!='undefined') {
try {
self.setVariable(soundID,'loopCount',loopCount||1,movie);
movie.o.TCallLabel('/'+soundID,'start');
if (!noDebug) writeDebug('soundManager.play('+self.getArgs(arguments)+')');
} catch(e) {
self.errorHandler('play','Failed: Flash unsupported / undefined sound ID (check XML)',arguments,e);
}
}
}

this.stop = function(soundID,movieName) {
if (self.unsupported) return false;
movie = self.checkMovie(movieName);
if (!movie) return false;
try {
movie.o.TCallLabel('/'+soundID,'stop');
writeDebug('soundManager.stop('+self.getArgs(arguments)+')');
} catch(e) {
// Something blew up. Not supported?
self.errorHandler('stop','Failed: Flash unsupported / undefined sound ID (check XML)',arguments,e);
}
}

this.getArgs = function(params) {
var x = params?params.length:0;
if (!x) return '';
var result = '';
for (var i=0; i<x; i++) {
result += (i&&i<x?', ':'')+(params[i].toString().toLowerCase().indexOf('object')+1?typeof(params[i]):params[i]);
}
return result
}

this.setVariable = function(soundID,property,value,oMovie) {
// set Flash variables within a specific movie clip
if (!oMovie) return false;
try {
oMovie.o.SetVariable('/'+soundID+':'+property,value);
// writeDebug('soundManager.setVariable('+self.getArgs(arguments)+')');
} catch(e) {
// d'oh
self.errorHandler('setVariable','Failed',arguments,e);
}
}

this.setVariableExec = function(soundID,fromMethodName,oMovie) {
try {
oMovie.o.TCallLabel('/'+soundID,'setVariable');
} catch(e) {
self.errorHandler(fromMethodName||'undefined','Failed',arguments,e);
}
}

this.callMethodExec = function(soundID,fromMethodName,oMovie) {
try {
oMovie.o.TCallLabel('/'+soundID,'callMethod');
} catch(e) {
// Something blew up. Not supported?
self.errorHandler(fromMethodName||'undefined','Failed',arguments,e);
}
}

this.callMethod = function(soundID,methodName,methodParam,movieName) {
movie = self.checkMovie(movieName||self.defaultName);
if (!movie) return false;
self.setVariable(soundID,'jsProperty',methodName,movie);
self.setVariable(soundID,'jsPropertyValue',methodParam,movie);
self.callMethodExec(soundID,methodName,movie);
}

this.setPan = function(soundID,pan,movieName) {
self.callMethod(soundID,'setPan',pan,movieName);
}

this.setVolume = function(soundID,volume,movieName) {
self.callMethod(soundID,'setVolume',volume,movieName);
}

// constructor - create flash objects

if (isIE && isMac) {
this.unsupported = 1;
}

if (!this.unsupported) {
this.addMovie(this.defaultName,'soundcontroller.swf');
// this.addMovie('rc','rubber-chicken-audio.swf');
}

}

function SoundManagerNull() {
// Null object for unsupported case
this.movies = []; // movie references
this.container = null;
this.unsupported = 1;
this.FlashObject = function(url) {}
this.addMovie = function(name,url) {}
this.play = function(movieName,soundID) {
return false;
}
this.defaultName = 'default';
}

function writeDebug(msg) {
var o = document.getElementById('debugContainer');
if (!o) return false;
var d = document.createElement('div');
d.innerHTML = msg;
o.appendChild(d);
}

var soundManager = null;

function soundManagerInit() {
soundManager = new SoundManager();
}

Include this in the <head> section of your HTML document as follows:

<script type="text/javascript" src="script/soundmanager.js"></script>

After the sound manager completed it’s job and played the mp3 notify sound it completes it’s function by setting played to 1 and setting the document’s title back to Sajith M.R Says…’. When the window/tab regains focus the gotFocus() script is executed:

function gotFocus()
{
document.title = 'Gmail Inbox(1)';

state = 'focus';

played = 0 ;
}

This script is very simple and has only three steps, set the document title to ‘Gmail Inbox(1)’, sets state to focus and played back to 0. That is almost it, there is only one small script to look at. If you look at the HTML for the textarea you will notice a function call that is triggered by the onclick event:

<textarea name="textarea" style="width:230px;height:50px;" onclick="changeColorBlue();">Hows life </textarea>

This is a very simple script with one goal, to turn the Layer2’s color back to ‘#73A6FF’:

function changeColorBlue()
{
document.getElementById('Layer2').style.background = '#73A6FF';
}

And that’s it! A very simple but working example of the client side aspects of the GMail chat interface. You can download all the source code from here and do not hesitate to visit the original post with your comments and suggestion.This then concludes the 3 part series on the GMail client side architecture. I hope you enjoyed it and learnt something from it.

Original Author

Original article written by Sajith.M.R