
O Backbone.js é um framework Javascript que fornece componentes para melhorar a estrutura de aplicações web. Entre os componentes estão o Router e o History, responsáveis pela criação de rotas e gestão do histórico do browser via Javascript. Além deles componentes, existe a função Backbone.sync, que é utilizada para realizar toda a sincronização com o servidor, através dos métodos de cada componente (apresentados nos artigos anteriores), a API de eventos utilizada para gestão e disparo de eventos, tanto customizados, quanto os eventos definidos no framework. Existem também alguns métodos utilitários, que auxiliam na resolução de pequenos detalhes como, por exemplo, integração com outros frameworks.
Introdução
No primeiro artigo desta série foi apresentado o framework Backbone.js, seus principais conceitos e aspectos, e uma introdução rápida através de um “Hello World”. No segundo artigo da série, foi apresentada a classe Backbone.View, demonstrando sua utilização, templates e a construção de uma View para um exemplo simples de blog. No terceiro artigo, foi apresentada a classe Backbone.Model junto com um simples backend escrito em Sinatra, possibilitando o trabalho com dados dinâmicos no exemplo do blog, e também foi modificada a View para suportar o mecanismo de templates Mustache. Noquarto artigo da série, foi apresentada a classe Backbone.Collection, possibilitando trabalhar com coleções de dados, assim como alguns métodos utilitários da Underscore.js para trabalhar com essas coleções e algumas modificações no backend Sinatra. Neste quinto artigo da série de seis artigos sobre Backbone.js, serão apresentadas as classes Backbone.Router e Backbone.history, assim como a função Backbone.sync, a API de eventos e alguns métodos utilitários, ilustrando cada item com exemplos práticos, a teoria de funcionamento, integração com o backend Sinatra e mudanças no exemplo de blog para agregar as classes apresentadas.
Backbone.Router
O trabalho com rotas em aplicações web é algo trivial e comum em diversos frameworksserver-side. Podemos pegar como exemplo o Ruby on Rails, que define suas rotas no arquivo routes.rb, ou o Zend Framework 2, que define as rotas de acordo com seus módulos nos arquivos module.config.php. De forma simples, uma rota nada mais é do que o mapeamento entre uma URL e alguma ação/método do framework em questão. Isso pode incluir parâmetros da URL e/ou métodos HTTP, de acordo com o necessário. O exemplo abaixo apresenta a definição de duas rotas no arquivo routes.rb de um sistema que utiliza Ruby on Rails.
# routes.rb
match 'products/:id' => 'catalog#view'
match ':controller(/:action(/:id))(.:format)'
A classe Backbone.Router provê métodos para construir essas rotas no lado cliente, e conecta cada rota a ações e eventos definidos via Javascript. Uma rota do lado cliente pode ser definida através do uso de hashes (#pagina, por exemplo) ou com o uso daHistory API, introduzida no HTML5. Backbone.Router utilizará por padrão a History API e, no caso de o browser não suportar essa API, a própria classe irá modificar a forma de tratar as URLs, para que use então o formato de hashes. Assim como as demais classes do framework Backbone, para customizar a Backbone.Router basta utilizar o método extend().
var AppRouter = Backbone.Router.extend({
// router code..
});
Ao customizar esse método, diversos parâmetros podem ser definidos, como, por exemplo o parâmetro routes, que define as rotas que a classe irá tratar e seus devidos mapeamentos para ações. Uma boa prática é evitar o uso de / no início de uma rota.
var AppRouter = Backbone.Router.extend({
// router code..
routes: {
"add" : "callback",
"help" : "helpCallback"
}
});
Além de definir rotas simples, é possível definir rotas que receberão parâmetros dinâmicos através da URL. Um exemplo disso pode ser uma rota que exibe um determinado registro, em que um parâmetro contendo o código identificador do registro é definido na URL, algo como “/registers/1“.
var AppRouter = Backbone.Router.extend({
// router code..
routes: {
"add" : "callback",
"help" : "helpCallback",
"registers/:id" : "registersCallback"
}
});
Existem dois tipos de parâmetros possíveis na definição de uma rota dinâmica:
-
:parametro – Essa notação definirá que existirá somente este parâmetro definido. Para ilustrar, considere um mapeamento do tipo “posts/:id“. Isso significa que a URL mapeará somente o primeiro parâmetro após a “/”, definindo-o ao :id, ou seja, uma URL do tipo /posts/1/2 simplesmente desconsiderará o parâmetro 2. Caso mais de um parâmetro seja necessário, basta defini-lo, algo como posts/:id/:outroid.
-
*parametros – Em contrapartida, com a notação anterior, irá mapear diversos parâmetros de uma URL, desconsiderando seu prefixo. Isso significa que uma definição posts/*ids considerará todos os valores que aparecerem depois de posts/, definindo-os nos callbacks. Por exemplo, posts/1/2 irá ler os valores 1e 2, assim como posts/1/2/3, lerá 1, 2 e 3.
var AppRouter = Backbone.Router.extend({
// router code..
routes: {
"add" : "callback",
"help" : "helpCallback",
"registers/:id" : "registersCallback",
"registers/*ids" : "multipleRegistersCallback"
}
});
Outra maneira de se definir as rotas da classe Backbone.Router é através de seu construtor. Assim como outras classes do Backbone, o Backbone.Router define o método initialize() que obtém um hash como parâmetro. O método route() também pode ser utilizado para criar manualmente uma nova rota. Ele receberá dois argumentos obrigatórios e um opcional, sendo o primeiro argumento a rota que será definida, na mesma sintaxe apresentada anteriormente. O segundo argumento será o nome da ação representada pela rota e será utilizada como identificador do evento da rota, e caso o terceiro parâmetro seja omitido, será mapeado a uma função válida da classe Router. E o terceiro argumento é opcional e pode ser uma função a ser executada quando a rota for acessada.
var AppRouter = Backbone.Router.extend({
initialize: function(options) {
this.route("post/:permalink", "permalink", function(permalink) {});
this.route(/^(.*?)\/open$/, "open");
}
});
Dentro da API de Backbone.Router, também existem alguns eventos definidos. Normalmente, esses eventos disparados conterão o nome da ação correspondente a uma rota. O disparo poderá ser realizado em diversos cenários, como quando o usuário pressionar o botão “Voltar” do browser ou entrar em uma URL válida, ou seja, que corresponde a uma rota. Dessa forma, outros objetos podem escutar os eventos de um Router para serem notificados e realizarem alguma operação. Considere o exemplo abaixo. Ao executar #dispatch, será lançado um evento “route:dispatch” do Router.
var AppRouter = Backbone.Router.extend({
routes: {
"dispatch": "dispatch"
},
dispatch: function() {}
});
router = new AppRouter();
router.on("route:dispatch", function() {});
Agora, para atualizar a URL da página manualmente, o método navigate() pode ser utilizado. Ele irá receber dois parâmetros: o primeiro é a rota a ser exibida na URL, e o segundo é um hash de opções que permite que seja executada a ação da rota (trigger: true) e, também que não seja armazenado no histórico do browser a URL (replace: true).
var AppRouter = Backbone.Router.extend({
//...
newPost: function() {
// ...
this.navigate("posts/add");
}
});
var app = new AppRouter();
// Exibe um novo post, executando a sua lógica de exibição, definida em uma ação
app.navigate("post/1", {trigger: true});
// Redireciona para a página de login, executando a ação, sem gravar no histórico
app.navigate("login", {trigger: true, replace: true});
Backbone.history
A classe Backbone.history fornece um router global para tratar eventos hashchangeou pushState; ele também irá escolher a rota apropriada para um determinado item de histórico e disparar callbacks. Um evento hashchange é associado à definição de rotas através de hashes (#). Essa abordagem dispara rotas para a URL atual, o que não requer o recarregamento da página. Com o surgimento do HTML e da History API, esse trabalho ficou mais transparente ao usuário, tirando a necessidade de definir as URLs com hashes. Dentro dessa nova API, encontra-se o método pushState, responsável por manipular e ativar itens do histórico do navegador, mantendo também uma URL amigável, bom para mecanismos de busca e deixando transparente se a aplicação manipula o histórico via Javascript ou não.
Uma boa prática ao se trabalhar com Backbone.history é a de não instanciá-la diretamente, já que, ao utilizar a classe Backbone.Router, uma referência aBackbone.history já é criada automaticamente. O suporte a pushState é oferecido por padrão no componente Backbone.history, e browsers que não suportam a API utilizarão a abordagem com hashes, no estilo explicado anteriormente. Por outro lado, caso uma URL utilizando hashes seja acessada em um browser que suporta pushState, o componente fará uma atualização transparente na URL.
Uma coisa a notar é que não adianta apenas habilitar ou desabilitar as rotas e o histórico do Backbone, também é necessário fazer algumas modificações no backend para que o servidor consiga retornar ao usuário a página esperada para uma determinada URL. Já para a renderização das páginas em conformidade com mecanismos de busca, uma URL direta deveria trazer o HTML completo da página. Em contraste, para uma aplicação web que não será incluída nos mecanismos de pesquisa, utilizar Views e JavaScript seria uma solução aceitável.
Agora que toda a teoria do Backbone.history já foi apresentada, utilizá-lo é tão simples quanto definir algumas rotas no componente Backbone.Router e executar o método Backbone.history.start(). O método recebe como parâmetro uma hash de opções para configurar o componente. Entre essas opções, pode-se definir a opção {pushState: true}, para garantir que o componente use a API pushState do HTML5.// Definição do Router...
// ...
Backbone.history.start();
// Garante que será utilizado o pushState
Backbone.history.start({pushState: true});
Outra opção que pode ser utilizada é a root, que define qual é o endereço base da aplicação. O método start irá retornar true caso a URL atual seja encontrada na lista de rotas, e false caso contrário. Se o servidor renderizar a página completa, sem a necessidade de disparar a rota root ao iniciar o componente History, basta definir o parâmetro silent: true. No Internet Explorer, o histórico baseado em hashes é definido em um iframe, portanto é necessário iniciar o histórico somente quando toda a árvore DOM já estiver pronta.// Endereço base é "index"
Backbone.history.start({root: "/index"});
// Verifica se o histórico foi iniciado corretamente
if (Backbone.history.start()) {
console.log("Histórico inicializado");
} else {
console.log("Não foi possível inicializar o histórico");
}
// Não dispara a URL "root"
Backbone.history.start({silent: true, root: "/index"});
Backbone.sync
Uma das principais características do Backbone.js é a comunicação remota através de uma API RESTful. Toda operação em que exista a necessidade de ler ou gravar um Model remotamente precisará de uma interface comum para executar as chamadas remotas utilizando corretamente os métodos HTTP, definir no corpo da requisição os parâmetros do Model etc. Apesar de essas chamadas serem executadas tanto porBackbone.Model quanto por Backbone.Collection, existe uma função em comum que sempre será executada por ambos os componentes, essa é a Backbone.sync().
Por padrão, a função sync() executará o método ajax() da biblioteca JavaScript sendo utilizada na aplicação (Zepto ou jQuery), executando uma requisição HTTP com JSON em seu corpo, cabeçalhos HTTP correspondendo à ação em questão e retornando um objeto jqXHR. Seguindo a principal característica do framework Backbone, que é a flexibilidade e a facilidade de extensão, a função sync() também pode ser estendida e customizada. Se, por exemplo, a aplicação utilizar offline storage, sem a necessidade de comunicação com um servidor remoto, o método sync() pode ser customizado para trabalhar com o banco de dados local, ou até, se o servidor suporta apenas transporte por XML, isso também pode ser implementado bastando sobrescrever a função. Ao sobrescrever Backbone.sync(), a assinatura sync(metodo, modelo, [opcoes]) deve ser utilizada, onde:
-
metodo – Corresponde ao método CRUD (“create”, “read”, “update”, “delete”) a ser executado
-
model – O objeto Model a ser gravado ou uma coleção a ser lida
-
opcoes – Argumento opcional, define callbacks de sucesso ou erro, e outras opções de requisição suportadas pela API ajax() do framework Javascript utilizado
O exemplo abaixo ilustra um código simples para estender Backbone.sync().
Backbone.sync = function(method, model, options) {
if (method == 'create') {
console.log('creating a new model...');
} else {
console.log('not creating, doing now a: ' + method);
}
};
O funcionamento padrão de Backbone.sync() pode ser capaz de suprir boa parte dos cenários comuns em aplicações web. Ao ser requisitado para gravar um Model, a função irá definir uma requisição contendo como corpo os atributos do Model serializados como JSON, com um content-type definido para application/json. A requisição retornará como resposta outro JSON, com os atributos já gravados no backend, para serem atualizados no lado cliente da aplicação. Quando uma Collection efetuar uma requisição read, a função Backbone.sync() precisará retornar um array de objetos com atributos que correspondam aos Models gerenciados pela Collection em questão, cabendo também ao backend responder à requisição GET com esses dados. O mapeamento REST padrão funciona da seguinte forma:
-
O create efetuará um POST para o endereço /collection
-
O read efetuará um GET para o endereço /collection[/id]
-
O update efetuará um PUT para o endereço /collection/id
-
O delete efetuará um DELETE para o endereço /collection/id
Além da sobrescrita global apresentada no trecho de código anterior, é possível sobrescrever a função sync() para os componentes mais específicos do framework. Seria possível, por exemplo, adicionar uma função sync() para as classes Backbone.Model e Backbone.Collection, conforme ilustrado no exemplo e seguir.
Backbone.Model.sync = function(method, model, options) {
// just do something...
};
Backbone.Collection.sync = function(method, model, options) {
// only collections...
}
Apesar de esses serem os aspectos principais da função Backbone.sync, ainda existem mais algumas configurações. Um exemplo disso é o atributo emulateHTTP. Ao definir esse atributo como true, a função irá emular requisições PUT e DELETE, ou seja, a requisição construída não utilizará nenhum destes como o método HTTP definido na requisição, utilizará no lugar um método POST, definindo então esses métodos em um atributo de cabeçalho chamado X-HTTP-Method-Override. Esse comportamento é útil para servidores que não oferecem suporte aos cabeçalhos HTTP RESTful. Outro atributo que pode ser configurado é o emulateJSON, que quando definido irá modificar o comportamento de serializar o Model e defini-lo como corpo da requisição HTTP. Em vez de utilizar essa abordagem, o Model será serializado e seu JSON será definido em um parâmetro POST chamado model, e a requisição utilizará o cabeçalho application/x-www-form-urlencoded, o que simula a requisição de um formulário HTML padrão. Esse atributo é útil para servidores que não suportam requisições do tipo application/json. Se ambos os atributos forem definidos como true, o método HTTP que antes era definido no cabeçalho HTTP chamado X-HTTP-Method-Override agora será definido em um parâmetro POST, nesse caso chamado _method.
Backbone.emulateHTTP = true;
Backbone.emulateJSON = true;
// Make a request just to show the behavior
var post = new PostModel(
title: 'Title',
content: 'Content'
);
post.save(););
Eventos
Nos artigos anteriores, foram apresentados diversos eventos disparados pelas classes do framework Backbone, assim como as formas de tratar esses eventos. Além desses eventos já pré-definidos, existe o módulo Events, que permite trabalhar com eventos customizados. Esse módulo é bem flexível, e os eventos não precisam ser pré-definidos para serem tratados ou lançados, e alguns eventos podem ser lançados com alguns argumentos definidos. Se uma aplicação necessita de um dispatcher customizado para tratar diversos eventos específicos da aplicação, o módulo de eventos da Backbone pode ser uma boa solução. Considere o código abaixo.
var object = {};
_.extend(object, Backbone.Events);
object.on("myevent", function() {
console.log("myevent was triggered");
});
object.trigger("myevent");
A situação ilustrada pode ser muito bem tratada por esse código, o objeto em questão ganha alguns novos métodos, como, por exemplo, o método on(), utilizado para vincular uma função de callback a um determinado evento. Caso exista um grande número de eventos na aplicação, uma boa prática é especializar cada um desses eventos através de um prefixo seguido do caractere :, como, por exemplo, users:add e users:edit . O método on() recebe dois parâmetros: o primeiro é o evento a ser escutado, e o segundo é a função de callback. Um terceiro parâmetro opcional pode ser definido, para que o contexto this corresponda ao escopo de classe do objeto, e não o escopo de função dacallback, que é o comportamento padrão.
callback = function(argument) {
console.log("Event triggered with the argument: " + argument);
};
object.on("myevent", callback, this);
Se houver a necessidade de que um callback seja executado para todos os eventos disparados, basta definir como primeiro parâmetro a string all.
object.on("all", globalCallback);
Para remover um callback definido anteriormente a um evento, basta utilizar o método off(), que recebe três parâmetros opcionais:
-
event – O evento anteriormente definido e que será removido
-
callback – A callback existente para o evento
-
context – O contexto definido no callback
object.off("all", globalCallback);
Todos esses métodos são úteis para executar uma determinada função quando um evento for disparado, e disparar o evento é muito simples, basta utilizar o método trigger(), definindo em seu primeiro parâmetro a string representando o evento. Opcionalmente, podem-se definir mais parâmetros nesse método, que serão lançados como argumentos do evento, que aparecerão como argumentos das callbacks definidas no método on().
object.trigger("myevent", "argument");
Utilitários
Outros métodos úteis do Backbone, porém não relacionados com nenhuma das classes já apresentadas até então, envolvem alguns pequenos utilitários para utilizar o framework em si. O primeiro método é o noConflict - seu conceito é simples, retornar o Backbone completo, com seus valores originais. Esse método permite que uma referência local ao framework seja utilizada. Esse cenário é útil, por exemplo, para evitar conflitos em versões diferentes do framework em uma mesma aplicação. Outro método que pode ser utilizado é o Backbone.$ (anteriormente setDomLibrary), que irá dizer ao Backbone qual objeto jQuery, Zepto ou outra variante, utilizar como biblioteca AJAX/DOM. Ou, até, para manter mais de uma versão de jQuery na mesma aplicação web. Acredito que serão raras as vezes em que esses utilitários serão necessários, mas cada caso é um caso, e eles estão presentes no framework para suprir alguma necessidade específica do desenvolvedor.
// noConflict
var localBackbone = Backbone.noConflict();
var model = localBackbone.Model.extend(...);
// $
Backbone.$ = jQuery();
Até agora, todo o conteúdo base do Backbone foi apresentado, abrangendo os principais tópicos da documentação do framework e alguns exemplos para ilustrar o que foi dito. O próximo passo é aplicar esses componentes na aplicação de blog que está sendo desenvolvida desde o primeiro artigo.
Veja a continuação deste artigo aqui: