Site menu Node.js: um humilde exemplo
e-mail icon
Site menu

Node.js: um humilde exemplo

e-mail icon

Este artigo é o contraponto prático do texto NodeJS: ferramenta e filosofia. Descrevo aqui o funcionamento do sistema de comentários e chat que implementei para meu site.

A (minha) história

Eu uso Node no trabalho, mas num contexto completamente diferente do "normal" (passa loooonge de serviço Web). Assim, resolvi usar meu próprio site como cobaia para treinar o uso típico do Node e das tecnologias correlatas.

Como meu site já é PHP, é semi-estático (uso PHP basicamente para gerar cabeçalhos e rodapés) e não vejo valor em convertê-lo para outra linguagem de template (meu tempo é mais bem aplicado escrevendo mais artigos e quiçá outros livros), um serviço Node foi desenvolvido para suprir alguns recursos extras:

O serviço Node tem de rodar completamente independente do PHP, e o site tem de continuar funcionando mesmo que o Node quebre, ou se o Node estiver em manutenção. Ou mesmo se eu enjoar e tirar o Node do ar :) Assim, o serviço Node roda numa porta diferente do servidor Web normal. Poderia ser até num servidor separado, mas aí eu teria de comprar outro certificado.

(Aqui entra uma diferença importante: se uma página PHP quebrar, não acontece nada porque o contexto de execução se restringe àquela página. Mesmo que seja um defeito do PHP, o máximo que pode acontecer é quebrar um subprocesso do Apache, que vai ser imediatamente reposto. Já o Node.js é um processo monolítico, e precisa ser mantido vivo por um script ou pelo nodemon).

Arquitetura

Como o servidor Node é separado do servidor PHP, a arquitetura dos serviços implementados tem três componentes principais, em vez de dois:

  1. O servidor Node;
  2. Uma página "trampolim", servida pelo Node e carregada como um iframe pelas páginas-clientes PHP;
  3. O código do lado cliente, que roda no contexto da página PHP.

O trampolim é uma página HTML completa, mas de conteúdo imaterial. Seu conteúdo relevante é o código Javascript que transfere dados entre a página PHP e o servidor Node. Se meu site Web fosse inteiramente servido pelo Node, ou talvez algum proxy no servidor para o PHP repassasse determinadas conexões ao Node, seria possível eliminar o trampolim. Mas preferi manter as coisas explícitas.

A base da comunicação entre cada página PHP e o trampolim é o evento DOM message. Este é o único recurso oficialmente disponível para trocar informação entre um iframe e a página onde ele reside, por razões de segurança.

Ovo ou galinha

O maior problema do lado cliente é coordenar o início das atividades, o que só pode ser feito quando todos os componentes estão devidamente carregados e funcionando. E isso pode acontecer em qualquer sequência (o iframe de trampolim pode estar pronto antes dos módulos ou vice-versa). A conexão Socket.io tem sua própria dinâmica.

Para abstrair esta confusão, o módulo io.js tem quatro estados, que são transmitidos aos módulos conforme acontecem:

Todos os estados são comunicados aos módulos apenas uma vez, exceto connected que pode ser mandado várias vezes, se a conexão Socket.io cair e voltar, para o caso do módulo usar este evento. O módulo de chat reconecta automaticamente nesta situação, porque o lado servidor do chat "esquece" completamente o usuário numa eventual desconexão.

Como é esperado, o módulo io.js coordena corretamente o caso em que um módulo registra-se muito tarde (e.g. quando o estado geral já é "ready"). A única exigência é que os módulos sejam incluídos em cada página após o io.js, aproveitando a única garantia que o browser oferece — o código Javascript é executado de cima para baixo, e apenas quando todos os scripts foram carregados.

Um problema semelhante acontece com os tratadores de eventos de Socket.io dos módulos.

Primeiro, cada mensagem Socket.io tem um nome e ela só é entregue se houver um tratador de evento com aquele nome. Como nós adiamos a conexão do Socket.io até o momento em que ela é necessária, o registro dos tratadores também tem de ser adiado até ocorrer a conexão; mas o módulo não deve tomar conhecimento desta complexidade.

Segundo, o registro do tratador de evento deve ocorrer dentro da página do iframe, onde o Socket.io é realmente carregado. A página PHP tem de mandar para o trampolim a lista de eventos que interessam ao lado cliente.

Socket.io ou não Socket.io

O Socket.io é incrivelmente fácil e prático de usar. Inicialmente todos os serviços usavam Socket.io como veículo. Todos os problemas típicos de comunicação remota, como reconexão, retransmissão de mensagens perdidas, conversão de mensagens de/para JSON, etc. são automaticamente tratados pelo Socket.io.

O problema é que cada página carregada mantinha uma conexão aberta com o servidor, o que acumula bem rápido.

Assim, todos os serviços exceto o chat foram migrados para AJAX. O módulo cliente io.js abstrai parciamente a diferença entre Socket.io e AJAX, de modo que seja fácil para os módulos usarem um ou outro.

Do ponto de vista do módulo-cliente, a única grande diferença ao usar Socket.io é poder receber mensagens não solicitadas (que não são resposta a uma requisição), e apenas o chat precisa deste recurso hoje, para receber as falas dos demais usuários.

O io.js implementa os métodos send() e ajax() para os módulos usarem. Os parâmetros das duas são os mesmos:

// io.js

epxio.send = function (cmd, payload) {
	payload.page = epxio.page;
	data = JSON.stringify({"emit": cmd, "payload": payload});
	epxio.iframe.contentWindow.postMessage(data,
			epxio.origin);
};

epxio.ajax = function (cmd, payload) {
	payload.page = epxio.page;
	data = JSON.stringify({"ajax": cmd, "payload": payload});
	epxio.iframe.contentWindow.postMessage(data,
			epxio.origin);
};

Como se viu, a requisição é codificada e repassada para o iframe de trampolim. Vejamos agora o que acontece no trampolim:

// main.html (trampolim)

window.addEventListener('message', receiver, false);

function receiver(e)
{
	if (e.origin == origin) {
		var data = JSON.parse(e.data);
		if (data.ajax !== undefined) {
			// manda via AJAX
			ajax(data.ajax, data.payload);
			return;
		}
		if (data.emit === "_cb") {
			// chamada especial para adicionar tratador
			link_msg(data.payload.id);
		} else if (data.emit === "_socketio") {
			// chamada especial para ativar Socket.io
			ioconnect();
		} else {
			// manda via Socket.io
			if (socket === null) {
				return;
			}
			socket.emit(data.emit, data.payload);
		}
	} else {
		// console.log("index: origem ruim");
	}
}

function ajax(method, params)
{
   	var request;
	var sparams = JSON.stringify({"method": method,
					"params": params});

	request = new window.XMLHttpRequest();

	request.onreadystatechange = function () {
		if (request.readyState == 4 &&
				request.status == 200) {
			var res = JSON.parse(request.responseText);
			var method = res.method;
			var params = res.params;
			send_back(method, params);
		}
	}
	request.open('POST', origin + ':34549/ajax', true);
	request.setRequestHeader('Content-type', 'application/json');
	request.send(sparams);
}

A checagem da origem (origin) no tratador de evento não é apenas por segurança; ela é necessária porque o tratador de evento recebe mensagens de todos os iframe's da página. E geralmente há vários deles: Google AdSense, Facebook, etc. e obviamente nós só queremos (ou podemos) interpretar as mensagens cujo formato nós conhecemos.

Quando o servidor manda a resposta, seja via Socket.io ou AJAX, ela é tratada pela função send_back() do trampolim:

// trampolim main.html

function send_back(type, msg)
{
	msg.type = type;
	parent.postMessage(JSON.stringify(msg), origin);
}

e finalmente recebida pela página-cliente através do seu próprio tratador do evento message:

// io.js

var h = function (e) {
	if (e.origin !== epxio.origin) {
		// origem estranha
		return;
	}
	var d = JSON.parse(e.data);

	if (! d.type) {
		return;
	}
	if (d.type === "ready") {
		// mensagem especial que significa que o trampolim
		// está funcionando
		epxio.ready = true;
		epxio._modules_ready();
		epxio._update_cbs();
	} else if (epxio.cbs[d.type]) {
		// mensagem normal enviada pelo send_back()
		// do trampolim
		epxio.cbs[d.type](d);
	} else {
		// mensagem sem tratador registrado
	}
}

window.addEventListener('message', h, false);

A mensagem de retorno só é interpretada se houver um tratador para ela, na lista epxio.cbs. Os módulos adicionam tratadores a esta lista no processo de inicialização.

Esta é a mecânica básica de como uma mensagem vai e volta, usando Socket.IO ou AJAX como transporte para o servidor, através do mecanismo do trampolim.

Acho que já deu pra notar que é tudo assíncrono. O fato do cliente fazer uma requisição não dá qualquer garantia de que virá uma resposta. Qualquer elo da cadeia pode romper, mas na prática o problema mais comum vai ser um servidor Node parado.

No geral a minha implementação é "ingênua" e confia que a resposta virá, mas também não deixa a página inconsistente caso ela não venha. Sistemas mais críticos teriam de implementar algum mecanismo de timeout.

O servidor Node

Obviamente, para as chamadas AJAX ou Socket.io serem atendidas, é preciso haver um servidor no outro lado da linha. Assim como o cliente, o servidor é implementado de forma modular, com uma base que distribui mensagens para os módulos interessados.

Em primeiro lugar, o código de inicialização do servidor:

var express = require('express');
var compress = require('compression')();
var bodyParser = require('body-parser');
var app = express();
app.use(bodyParser.json());
var fs = require('fs');
var mongodb = require('mongodb');
var debug = require('debug')('main');

var srv = require('http').createServer(app);

app.get('/main.html', function(req, res){
	res.sendFile(__dirname + '/main.html');
});

srv.listen(34549, function() {
	debug('listening on *:34549');
});

var io = require('socket.io').listen(srv);
var MongoClient = mongodb.MongoClient;

MongoClient.connect("mongodb://localhost:27017/banco", {
		db: {w: 0, native_parser: false },
		server: {
			socketOptions: {connectTimeoutMS: 500},
			poolSize: 5,
			auto_reconnect: true
		},
		replSet: {},
		mongos: {}
}, function (err, db) {
	if (err) {
		console.log("server: MongoDB conn failed");
	} else {
		debug("MongoDB connected");
		init_listeners(db, io);
		main(db);
	}
});

Seguindo a API assíncrona do Node, as funções init_listeners() e main() são invocadas apenas se o banco de dados MongoDB for iniciado com sucesso. Se houver algum problema com o banco, nenhuma requisição será atendida, muito embora o servidor permaneça "rodando".

Continuando:

var chato = require("./chato.js");
var comment = require("./comment.js");
var listeners = [ chato, comment ];
var listeners_ajax = {};

// Quando o MongoDB conecta, os módulos são iniciados e recebem
// o objeto do banco de dados. Os modulos também podem adicionar
// tratadores à lista listeners_ajax.

function init_listeners(database)
{
	for (var x = 0; x < listeners.length; ++x) {
		listeners[x].init(database, io, listeners_ajax);
	}
}

// Notifica os módulos conhecidos quando uma conexão Socket.io
// foi levantada, e o cliente identificou-se. (O cliente só
// levanta esta conexão quando necessário, então esta função
// só é chamada se o cliente requisitou chat.)

function distrib_connect(sk, page, ip)
{
	for (var x = 0; x < listeners.length; ++x) {
		listeners[x].connection(sk, page, ip);
	}
}

// Tratador de requisições AJAX

app.post('/ajax', function (req, res) {
	// Algumas checagens omitidas, mas necessárias para
	// que o servidor seja minimamente seguro

	if (typeof req.body !== 'object') {
		debug("Requisição AJAX não é JSON");
		return;
	}

	var method = req.body.method;
	var params = req.body.params;
	var ip = req.connection.remoteAddress;

	// os módulos já adicionaram seus tratadores à lista
	// listeners_ajax durante a inicialização... desde que o
	// MongoDB tenha iniciado com sucesso.

	listeners_ajax[method](ip, params, res);
});

A função main() do servidor tem a responsabilidade de lidar com as conexões do Socket.io.

function main(db)
{
	io.on('connection', function (socket) {
		var ip = socket.request.connection.remoteAddress;
		var id = socket.id;

		debug('connected ' + ip);

		socket.on('disconnect', function () {
			debug('disconnected ' + ip);
		});

		// O cliente identifica-se mandando o nome da
		// página, e só então os módulos são notificados
		// desta conexão, pois nem todo serviço se aplica
		// a toda página.

		socket.on('page', function (page) {
			distrib_connect(socket, "" + page.page, ip);
		});
	});
}

Servidor de chat

Assim como acontece no lado cliente, também no servidor o Socket.io pede o registro de um tratador para cada tipo de mensagem. O módulo de chat é quem usa Socket.io, então apenas ele faz este registro quando uma conexão é recebida.

Este registro tem de ser feito para cada nova conexão. Por conta disso, a função connection() que recepciona uma conexão nova contém praticamente todo o corpo do módulo.

O código do módulo de chat foi muito editado, pois eu faço inúmeras verificações, inclusive limitação de spam e flood, que ficariam chatas e extensas demais neste artigo. Vou colocar apenas os "highlights" que ilustram como o Socket.io é utilizado.

// Módulo de chat, lado servidor

var db = null;
var io = null;
var debug = require('debug')('chato');

// Invocado quando o MongoDB do servidor está ok

function init(pdb, pio)
{
	debug("init");
	db = pdb;
	io = pio;
}

var rooms = {};

// Invocado quando o cliente conecta Socket.io e identifica
// a página. O chamador manda o socket sk referente a aquele
// cliente. Nós utilizamos o nome da página para criar "canais"
// separados, um por página.

function connection(sk, page, ip)
{
	var id = sk.id;

	if (! rooms[page]) {
		rooms[page] = {};
	}

	rooms[page][id] = {"present": false, "nick": null};

	sk.on('disconnect', function () {
		...
		delete rooms[page][id];
	});

	// Tratador do evento chat_enter

	sk.on('chat_enter', function (msg) {
		var payload;
		...
		if (error) {
			payload = {'error': error, 'msg': error_msg};
		} else {
			payload = {'error': 0, 'msg': error_msg,
					'nick': nick,
					'nicks': nick_list(page)};
		}

		// retorna o nick aceito e a lista dos participantes
		sk.emit('chat_enter_cb', payload);

		// adiciona o usuário à sala
		if (! error) {
			rooms[page][id].present = true;
			rooms[page][id].nick = nick;
			// Ingressa no grupo de broadcast do Socket.io
			sk.join("chato" + page);
		}
	});

	// Tratador do evento chat_leave

	sk.on('chat_leave', function (msg) {
		...
		rooms[page][id].present = false;
		...
		// Deixa o grupo de broadcast do Socket.io
		sk.leave("chato" + page);
	});

	// Tratador do evento chat_talk (quando o cliente fala)

	sk.on('chat_talk', function (msg) {
		...
		var text = ("" + msg.text).substr(0, 300);
		...
		bcast(page, rooms[page][id].nick, text, null, null);
	});
}

// Manda uma mensagem para todos os participantes da página,
// e inclui listas de novos usuários (plus) e usuários que
// saíram da sala (minus)

function bcast(page, nick, text, plus, minus)
{
	io.to("chato" + page).emit('chat_text',
		{"nick": nick,
		"text": text,
		"plus": plus,
		"minus": minus});
}

// O servidor principal só enxerga as funções exportadas assim:

exports.init = init;
exports.connection = connection;

Servidor de comentários

O módulo servidor de comentários tem arquitetura consideravelmente diferente, porque ele não mantém estado na memória; simplesmente reage às requisições do cliente e lida com o banco de dados.

Incidentalmente, este código ilustra um pouco da API do MongoDB.

// Módulo de comentários, lado servidor

var db = null;
var debug = require('debug')('comment');
var ObjectID = require('mongodb').ObjectID;

// Invocado pelo servidor quando o MongoDB estiver funcionando

function init(pdb, pio, ajax)
{
	debug("init");
	db = pdb;

	// Cria a tabela de comentários no banco

	pdb.collection('comments', {}, function (err, comments) {
		if (err) {
			console.log("comment: idx error " + err);
			return;
		}
		comments.createIndex({page: 1}, {unique: false},
			function (err, result) {
				if (err) {
					console.log("error");
				}
			}
		);
	});

	// Registra os tratadores de requisições AJAX
	// Diferente do Socket.io e do chat, neste caso o registro
	// só precisa ser feito uma vez.

	ajax['comment_post'] = comment_post;
	ajax['comment_fetch'] = comment_fetch;
}

// Trata um comentário remetido pelo cliente

function comment_post(ip, msg, response)
{
	... 
	var nick = "" + msg.nick;
	var text = "" + msg.text;
	var page = "" + msg.page;
	...

	db.collection('comments', {}, function (err, comments) {
		if (err) {
			console.log("error getting collection");
			return;
		}
		var record = {'page': page, 'sid': "", 'ip': ip,
			'nick': nick, 'text': text,
			'timestamp': new Date(), 'approved': -1};
		comments.insert(record, function (err, result) {
			if (err) {
				console.log("Error " + err);
			}
			...

			// Responde a requisição AJAX
			response.send({
				method: 'comment_post_cb',
				params: {'error': 0,
					'id': record._id}});
		});
	});
}

// Trata a requisição da lista de comentários para uma página

function comment_fetch(ip, msg, response)
{
	...

	var start = parseInt(msg.start);
	var volume = parseInt(msg.volume);
	var page = "" + msg.page;

	var query = { "page": page, "approved": 1 };

	db.collection('comments', {}, function (err, comments) {
		if (err) {
			console.log("error getting collection");
			return;
		}
		debug(query);
		comments.find(query)
				.skip(start)
				.limit(volume + 1)
				.sort({"timestamp": -1})
				.toArray(function (err, result) {
			if (err) {
				debug("Error getting comments");
				return;
			}
			var crop = result;
			var more = crop.length > volume;
			crop = wash(crop, false);

			// A resposta AJAX é feita desta forma:

			response.send({
				method: 'comment_fetch_cb',
				params: {"comments": crop,
					"more": more}});
		});
	});
}

exports.init = init;
// Este módulo não se interessa por conexões Socket.io
exports.connection = function () {};

Módulo de chat, lado cliente

Tendo visto como o chat é implementado do lado servidor, resta ver como ele é feito no lado cliente.

Diferente do lado servidor, a API do Socket.io nunca é utilizada diretamente, pois o módulo io.js tinha abstraído a mesma. (Seria fácil converter este chat para funcionar com AJAX, a maior mudança seria a necessidade de requisitar periodicamente as falas de outros usuários.)

// cliente chato2.js

epxio.chato_create = function () {
	var self = {};

	self.nick = null;
	self.loggedin = false;
	self.nicks = [];
	self.log = [];

	// Usuário tocou na aba
	self.tab_toggle = function () {
		// solicita conexão Socket.IO apenas neste caso
		// (se o usuário tocou na aba, provavelmente quer usar
		// o chat)
		epxio.socketio();
		... Ergue ou baixa a aba usando CSS ...
	};

	// Usuário pressionou o botão de login
	self.login = function (nick) {
		$("#chatologin").hide();
		self.nick = nick;
		epxio.send("chat_enter", {"nick": nick});
	};

	// Tratador da resposta à requisição "chat_enter"
	self.chat_enter_cb = function (d) {
		if (d.error) {
			// Login falhou
			self.more_text("Login error: " + d.msg);
			self.loggedin = false;
			... Manipulações CSS ...
			return;
		}
		self.nick = d.nick;
		self.loggedin = true;
		... Manipulações CSS ...
		self.handle_nicks(d.nicks, null, null);
	};

	// Tratador de recepção de texto do servidor de chat
	self.chat_text = function (d) {
		... CSS da mensagem conforme @nick está nela ...
		self.more_text(d.text);
		self.handle_nicks(null, d.plus, d.minus);
	};

	// Mostra mais texto na tela, lidando com scroll etc.
	self.more_text = function (html) {
		self.log.push(html);
		...
		var t = "";
		for (var i = 0; i < self.log.length; ++i) {
			t += self.log[i] + "<br>";
		}
		...
		$("#chatotext").html(t);
		...
	};

	// Lida com as mudanças na lista de nicks
	self.handle_nicks = function (initial, added, removed) {
		...
	};

	// Invocado quando o sistema migra para o estado "Enabled"
	self.enabled = function () {
		$("#chatotab").mousedown(function () {
			self.tab_toggle();
		});
		... adiciona outros tratadores de eventos de UI ...
	};

	// Invocado quando o sistema migra para o estado "Ready"
	self.ready = function () {
		// Faz a aba aparecer na tela
		$("#chatotab").attr("class", "chato_tab_down");

		// Adiciona os tratadores de mensagens Socket.io
		// O trampolim "adia" o registro destes tratadores
		// até o momento da conexão realmente existir, então
		// não precisamos nos preocupar com isto
		epxio.add_handler({
			"chat_enter_cb": function (d) {
				self.chat_enter_cb(d);
			},
			"chat_text": function (d) {
				self.chat_text(d);
			}
		});
	};

	// Invocado quando a conexão Socket.io foi (re-)estabelecida
	self.connected = function () {
		if (self.loggedin) {
			// re-login automático
			...
			epxio.send("chat_enter", {"nick": self.nick});
		}
	};

	return self;
};

// Invocado quando a página terminou de carregar
$(function () {
	var chato = epxio.chato_create();

	// Adiciona os tratadores de estado do sistema

	epxio.add_module("chato", {
		"enabled": chato.enabled,
		"ready": chato.ready,
		"connected": chato.connected,
	});
});

Módulo de comentários, lado cliente

O módulo de comentários é mais simples que o de chat, porque não precisa reter estado; ele basicamente faz requisições na medida em que o usuário faz alguma coisa (carrega uma página ou manda um comentário).

epxio.comment_create = function () {
	var self = {};

	// Invocado quando o usuário faz um comentário
	self.post = function () {
		var nick = $("#comment-nick").val();
		var text = $("#comment-text").val();
		...
		epxio.ajax("comment_post", {nick: nick, text: text});
		...
	}

	// Resposta do servidor ao comentário mandado acima
	// Basicamente mostra um alert() com a resposta.
	self.post_cb = function (d) {
		if (! d.error) {
			alert("The post has been received.");
		} else {
			alert(d.msg);
		}
	};

	// Obtém comentários para esta página.
	// Como os módulos io.js e main.html registram automaticamente
	// o nome da página, não precisamos fazer isto nós mesmos
	self.fetch = function (d) {
		epxio.ajax("comment_fetch", {start: 0, volume: 999});
	};

	// Resposta à requisição acima
	self.fetch_cb = function (d) {
		var root = $('#comment-list');
		var comments = d.comments;
		var h = "<hr>";
		for (var i = 0; i — comments.length; ++i) {
			var comment = comments[i];
			... gera HTML com o conteúdo do comentário ...
		}
		root.html(h);
	};

	// Chamado quando o sistema vai para o estado "enabled"
	self.enabled = function () {
		// Registra os tratadores de eventos vindos do
		// servidor
		// Como nós usamos AJAX, estes tratadores serão
		// invocados apenas se fizermos alguma requisição
		epxio.add_handler({
			"comment_post_cb": function (d) {
				self.post_cb(d);
			},
			"comment_fetch_cb": function (d) {
				self.fetch_cb(d);
			}
		});
	};

	// Chamado quando o sistema vai para o estado "ready"
	self.ready = function () {
		// Registra tratador do botão "submeter evento"
		$("#comment-submit").click(function () {
			self.post();
		});

		// Mostra seção de comentários
		$('#comment-main').show();

		// Agenda solicitação dos comentários já existentes
		// Só pode ser feito no estado "Ready" porque o
		// servidor só está disponível neste estado
		setTimeout(function () {
			self.fetch();
		}, 0);
	};

	return self;
};

// Invocado quando a página terminou de carregar
$(function () {
	var comment = epxio.comment_create();
	epxio.comment_object = comment;

	// Registra os tratadores do estado do sistema
	epxio.add_module("comment", {
		"enabled": comment.enabled,
		"ready": comment.ready
	});
});

O módulo cliente io.js, partes restantes

Segue a listagem comentada do módulo io.js, do que não foi exibido em partes anteriores. Basicamente o que faltou mostrar é a burocracia relativa ao registro de módulos e dos tratadores de evento.

var epxio = {};

epxio.enabled = false;
epxio.modules = {};
epxio.cbs = {};
epxio.ready = false;

// Adiciona módulo (chat, comentário, etc.) ao sistema.
// Invocado pelo próprio módulo.

epxio.add_module = function (name, m) {
	console.log("io: add_module " + name);
	epxio.modules[name] = m;

	// Se por acaso o sistema já está no estado "enabled"
	// ou "ready", inicia o módulo agora mesmo
	epxio._modules_enabled();
	epxio._modules_ready();
};

// Adiciona tratadores de evento.
// Invocado pelo próprio módulo.

epxio.add_handler = function (mft) {
	for (var id in mft) {
		epxio.cbs[id] = mft[id];
	}

	// Atualiza os tratadores junto ao trampolim
	epxio._update_cbs();
};

// Inicialização do sistema do lado cliente

epxio.init = function ()
{
	// Verifica em primeiro lugar se iframe é suportado

	var iframe = document.getElementById('animator');

	if (iframe) {
		if (! iframe.contentWindow) {
			iframe = null;
		} else if (! iframe.contentWindow.postMessage) {
			iframe = null;
		}
	}

	if (! iframe) {
		return;
	}

	// Muda para estado "enabled"

	epxio.iframe = iframe;
	epxio.enabled = true;
	epxio._modules_enabled();

	var page = location.pathname;
	var url = location.protocol + "//" + location.hostname;
	epxio.myorigin = url;
	epxio.origin = url + ":34549";
	epxio.page = page;

	... tratador de mensagem do trampolim, já abordada ...

	// Trata o evento de conexão do Socket.io
	// (só disparado se algum módulo faz uso de Socket.io)

	epxio.add_handler({
		"connect": function (d) {
			epxio.send("page", {"page": epxio.page});
			epxio._modules_connected();
		}
	});
};

... métodos send() e ajax(), já abordados ...

// Notifica os módulos de que o sistema está em estado "enabled"
// Se o sistema está neste estado

epxio._modules_enabled = function () {
	if (! epxio.enabled) {
		return;
	}
	for (var id in epxio.modules) {
		if (epxio.modules[id].enabled) {
			if (! epxio.modules[id].is_enabled) {
				epxio.modules[id].enabled();
				epxio.modules[id].is_enabled = true;
			}
		}
	}
};

// Notifica os módulos de que o sistema está em estado "ready"
// Se o sistema está neste estado

epxio._modules_ready = function () {
	if (! epxio.ready) {
		return;
	}
	for (var id in epxio.modules) {
		if (! epxio.modules[id].is_ready) {
			epxio.modules[id].ready();
			epxio.modules[id].is_ready = true;
		}
	}
};

// Notifica os módulos da conexão Socket.io

epxio._modules_connected = function () {
	for (var id in epxio.modules) {
		if (epxio.modules[id].connected) {
			epxio.modules[id].connected();
		}
	}
};

// Atualiza os tratadores de evento registrados pelos módulos
// junto ao trampolim

epxio._update_cbs = function () {
	if (! epxio.ready) {
		return;
	}
	for (var id in epxio.cbs) {
		if (id !== "connect" &&
				! epxio.cbs[id].registered) {
			// Manda uma mensagem especial 
			// ao trampolim para registrar que nós estamos
			// interessados // em tratar a mensagem do
			// tipo "id"
			epxio.send("_cb", {"id": id});
			epxio.cbs[id].registered = true;
		}
	}
};

// Método chamado pelos módulos que precisam do Socket.IO para
// funcionar, como o chat

epxio.socketio = function () {
	// Manda uma mensagem especial ao trampolim requisitando
	// a iniciação do Socket.io
	epxio.send("_socketio", {});
};

// Invocado quando a página terminou de carregar

$(function () {
	epxio.init();
});

O trampolim main.html: partes restantes

Também falta exibir algum código do trampolim. É o código mais burocrático de todo o sistema pois só faz intermediar mensagens.

var origin = location.protocol + "//" + window.location.hostname;
var secure = location.protocol === "https";

var socket = null;
var socket_linkqueue = [];

// Invocado quando o cliente requisita Socket.io

function ioconnect()
{
	if (socket !== null) {
		return;
	}
	socket = io.connect(origin + ':34549', {secure: secure});

	// Conecta este evento logo, pois ele pode ocorrer antes do
	// cliente atingir o estado "ready"

	socket.on('connect', function () {
		send_back("connect", {});
	});

	// Registra todos os tratadores de mensagem que estavam
	// pendentes. Infelizmente o Socket.io não tem um mecanismo
	// genérico para tratar "qualquer" mensagem.

	while (socket_linkqueue.length > 0) {
		var type = socket_linkqueue.shift();
		link_msg(type);
	}
}

// Registra um tratador de mensagem junto ao Socket.io

function link_msg(type) {
	if (socket === null) {
		// Socket.io não conectado ainda, adia o registro

		if (socket_linkqueue.indexOf(type) < 0) {
			socket_linkqueue.push(type);
		}
		return;
	}

	socket.on(type, function (msg) {
		// Manda código via evento 'message', que consegue
		// furar a barreira entre iframe e página principal
		send_back(type, msg);
	});
}

... aqui há código que já tinha sido explanado antes ...

// Invocado assim que a página de trampolim é carregada.
// É desta forma que o cliente (io.js) sabe que pode migrar
// para o estado "ready".

send_back("ready", {});

e-mail icon