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.
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).
Como o servidor Node é separado do servidor PHP, a arquitetura dos serviços implementados tem três componentes principais, em vez de dois:
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.
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.
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.
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); }); }); }
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;
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 () {};
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, }); });
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 }); });
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(); });
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", {});