Back-End

26 ago, 2013

Série Backbone.js – Parte 05: Router, Histórico, Backbone.sync, Eventos, e mais

Publicidade

Backbone.js

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. No quarto 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 frameworks server-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.

[ruby]
# routes.rb
match ‘products/:id’ => ‘catalog#view’
match ‘:controller(/:action(/:id))(.:format)’
[/ruby]

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 da History 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().

[javascript]
var AppRouter = Backbone.Router.extend({
// router code..
});
[/javascript]

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.

[javascript]
var AppRouter = Backbone.Router.extend({
// router code..
routes: {
"add" : "callback",
"help" : "helpCallback"
}
});
[/javascript]

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“.

[javascript]
var AppRouter = Backbone.Router.extend({
// router code..
routes: {
"add" : "callback",
"help" : "helpCallback",
"registers/:id" : "registersCallback"
}
});
[/javascript]

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 12, assim como posts/1/2/3, lerá 12 e 3.

[javascript]
var AppRouter = Backbone.Router.extend({
// router code..
routes: {
"add" : "callback",
"help" : "helpCallback",
"registers/:id" : "registersCallback",
"registers/*ids" : "multipleRegistersCallback"
}
});
[/javascript]

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.

[javascript]
var AppRouter = Backbone.Router.extend({
initialize: function(options) {
this.route("post/:permalink", "permalink", function(permalink) {});
this.route(/^(.*?)\/open$/, "open");
}
});
[/javascript]

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.

[javascript]
var AppRouter = Backbone.Router.extend({
routes: {
"dispatch": "dispatch"
},
dispatch: function() {}
});
router = new AppRouter();
router.on("route:dispatch", function() {});
[/javascript]

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).

[javascript]
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});
[/javascript]

Backbone.history

A classe Backbone.history fornece um router global para tratar eventos hashchange ou 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 a Backbone.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.

[javascript]
// Definição do Router…
// …
Backbone.history.start();
// Garante que será utilizado o pushState
Backbone.history.start({pushState: true});
[/javascript]

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.

[javascript]
// 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"});
[/javascript]

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 por Backbone.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().

[javascript]
Backbone.sync = function(method, model, options) {
if (method == ‘create’) {
console.log(‘creating a new model…’);
} else {
console.log(‘not creating, doing now a: ‘ + method);
}
};
[/javascript]

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:

  • create efetuará um POST para o endereço /collection
  • read efetuará um GET para o endereço /collection[/id]
  • update efetuará um PUT para o endereço /collection/id
  • 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.ModelBackbone.Collection, conforme ilustrado no exemplo e seguir.

[javascript]
Backbone.Model.sync = function(method, model, options) {
// just do something…
};

Backbone.Collection.sync = function(method, model, options) {
// only collections…
}
[/javascript]

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 PUTDELETE, 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.

[javascript]
Backbone.emulateHTTP = true;
Backbone.emulateJSON = true;
// Make a request just to show the behavior
var post = new PostModel(
title: ‘Title’,
content: ‘Content’
);
post.save();
[/javascript]

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.

[javascript]
var object = {};
_.extend(object, Backbone.Events);
object.on("myevent", function() {
console.log("myevent was triggered");
});
object.trigger("myevent");
[/javascript]

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 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 da callback, que é o comportamento padrão.

[javascript]
callback = function(argument) {
console.log("Event triggered with the argument: " + argument);
};
object.on("myevent", callback, this);
[/javascript]

Se houver a necessidade de que um callback seja executado para todos os eventos disparados, basta definir como primeiro parâmetro a string all.

[javascript]
object.on("all", globalCallback);
[/javascript]

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

[javascript]
object.off("all", globalCallback);
[/javascript]

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().

[javascript]
object.trigger("myevent", "argument");
[/javascript]

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.

[javascript]
// noConflict
var localBackbone = Backbone.noConflict();
var model = localBackbone.Model.extend(…);
// $
Backbone.$ = jQuery();
[/javascript]

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.

Incrementando e finalizando o blog

Existem diversos passos para melhorar o blog desenvolvido nos artigos anteriores, alguns deles sendo:

  • Adicionar rotas essenciais para o leitor do blog, com suporte para histórico
  • Trabalhar com a API de eventos
  • Criar um simples armazenamento offline com o link do último artigo visualizado pelo usuário
  • Fornecer um CSS mínimo para melhorar um pouco a cara da aplicação

O código do backend será bastante similar ao existente nos artigos anteriores, a única diferença é no delete, já que para que o Backbone.js considere que a exclusão foi executada com sucesso, chamando assim o callback success – é necessário retornar o Model excluído como resposta da requisição. Dessa forma, o DELETE fica conforme o código abaixo.

[ruby]
delete ‘/posts/:id’ do
post = Post.find params[:id]
Post.destroy params[:id]
post.to_json
end
[/ruby]

Se preferir, todo o código-fonte está disponível no GitHub, o link encontra-se ao final do artigo. O arquivo index.html também precisará de algumas mudanças. O cabeçalho do blog foi convertido para um template, e algumas novas bibliotecas foram adicionadas – esste código completo também está disponível no GitHub. Prosseguindo agora para as melhorias, vamos começar pelas rotas, partiremos para uma abordagem simples, na qual o blog terá a rota principal que listará os posts, e uma rota para exibir uma postagem, seguindo o formato “post/:id”. Poderíamos avaliar e incluir novas rotas, mas isso fica como lição de casa para você, leitor.

O primeiro passo é inicializar o Backbone.Router e definir as rotas mencionadas, isso tudo será colocado no arquivo AppRouter.js.

[javascript]
var AppRouter = Backbone.Router.extend({
routes: {
"": "listAction",
"post/:id": "viewAction"
}
};
[/javascript]

Note que a rota padrão aponta para o método listAction, e a rota que exibe uma postagem para o método viewAction. O primeiro método será responsável por obter a lista de postagens e exibir para o usuário.

[javascript]
listAction: function() {
$(‘#content’).html(”);

this.header.showControls();

this.appView = new AppView();

Posts.bind(‘add’, this.appView.addPost);
Posts.bind(‘sync’, this.appView.render);

Posts.fetch();
}
[/javascript]

Se você acompanhou os artigos anteriores, vai notar que existe uma chamada à this.header, não apresentada em nenhum desses artigos. Não se preocupe, logo chegaremos lá, esse método se refere ao cabeçalho da aplicação. O método viewAction irá obter uma postagem por seu id e exibir somente ela ao usuário. Antes disso, ele irá esconder o menu e, caso aberto, o formulário de nova postagem. Note que é feita uma requisição para obter o Model, poderíamos obter diretamente da Collection, mas isso foi feito para ilustrar o callback de sucesso.

[javascript]
viewAction: function(id) {
this.header.hideControls();
this.header.hideForm();
$(‘#content’).html(”);
var post = new PostModel({
id: id
});
post.fetch({
success: function(postFetched) {
var postView = new PostView({
model: postFetched
});
postView.render();
$(‘#content’).html(postView.el);
}
});
}
[/javascript]

Ambos os métodos precisam de um atributo em comum: o header. Ele será uma View para representar o topo do blog, contendo o seu nome, o botão “Adicionar” e o formulário (quando acionado). O método initialize() do Router ficará responsável por preencher esse atributo.

[javascript]
initialize: function() {
if (!this.header) {
this.header = new HeaderView();
$(‘#content’).before(this.header.render().el);
}
}
[/javascript]

A classe HeaderView ainda não foi implementada, seu principal objetivo é representar o topo do site e controlar o menu e o formulário de inserção de uma nova postagem, representado pela classe PostFormView. A classe HeaderView ficará no arquivo views/HeaderView.js.

[javascript]
var HeaderView = Backbone.View.extend({
tagName: ‘header’,
className: ‘site-header’,
template: $(‘#header’).html(),
events: {
‘click #new-post’: ‘addButtonClick’
},
initialize: function() {
_.bindAll(this, ‘render’, ‘addButtonClick’, ‘showForm’, ‘hideForm’, ‘showControls’, ‘hideControls’);
},
render: function() {
var viewContent = Mustache.to_html(this.template);
this.$el.html(viewContent);
return this;
},
hideControls: function() {
this.$el.find(‘.toolbar’).hide();
},
showControls: function() {
this.$el.find(‘.toolbar’).show();
},
hideForm: function() {
if (this.form != null) {
this.form.remove();
this.form = null;
}
}
});
[/javascript]

Todo esse código não irá funcionar se não inicializarmos o Router e o History, logo após a declaração da classe AppRouter.

[javascript]
var router = new AppRouter();
Backbone.history.start({pushState: true});
[/javascript]

A próxima etapa agora é trabalhar com a API de eventos. Temos várias formas de implementar o formulário para a adição de uma nova postagem, uma delas é a partir de uma rota. Para este exemplo, vamos utilizar a API de eventos do Backbone. Se analisar a classe HeaderView implementada até então, já é feito o uso da API de eventos para o listener do clique do link de “Nova Postagem”. Esse listener é representado pelo método addButtonClick.

[javascript]
addButtonClick: function(e) {
if (this.form == null) {
this.showForm();
} else {
this.hideForm();
}
e.preventDefault();
}
[/javascript]

Verificamos se já existe o form aberto no Header; caso exista, ele será escondido e removido, dando uma ação de toggle ao botão “Adicionar Postagem”. Agora, quando o usuário inserir com sucesso uma nova postagem, o formulário também deverá ser escondido e removido. Para que o HeaderView saiba que o formulário deverá ser removido, será utilizada a API de eventos, na qual um evento customizado form:hide indicará essa remoção. A atribuição do listener desse evento ficará no método showForm.

[javascript]
showForm: function() {
this.form = new PostFormView();
this.form.on("form:hide", this.hideForm, this);
this.form.render();
this.$el.append(this.form.el);
}
[/javascript]

A classe PostFormView continua a mesma implementada na parte 4 da série. Ela precisará de uma alteração, pois será necessário lançar o evento form:hide assim que o Post for gravado. Isso ficará a cargo do método postSaved.

[javascript]
postSaved: function() {
window.alert(‘Post gravado com sucesso!’);
this.trigger("form:hide");
}
[/javascript]

O método savePost deverá então chamar esse método assim que uma postagem for inserida.

[javascript]
savePost: function(e) {
e.preventDefault();

this.model = new PostModel();

var title = this.titleInput.val();
var text = this.textInput.val();

this.model.set({
title: title,
text: text
});

Posts.create(this.model, {
wait: true,
success: this.postSaved
});
Posts.sort();
}
[/javascript]

Agora, como que o Backbone saberá que ao clicar no link de uma postagem ele deverá interceptar e utilizar sua API de rotas para exibi-la? Podemos definir isso via hash ou utilizando os métodos do Router. A abordagem escolhida foi a segunda, na classe PostView, já implementada no artigo anterior, vamos interceptar alguns eventos dos links disponíveis.

[javascript]
events: {
"click .remove-button": "removePost",
"click .view-button": "showPost"
}
[/javascript]

Quando o link de exibir a postagem for pressionado, a API de rotas deverá ser chamada de acordo com o link que se deseja exibir.

[javascript]
showPost: function(e) {
router.navigate($(e.currentTarget).attr(‘href’), {trigger: true});
e.preventDefault();
}
[/javascript]

Se o link pressionado for o de exclusão, o usuário deverá confirmar, e assim que a postagem for removida, o usuário será redirecionado para a página principal do blog. Aqui que fará sentido a alteração no DELETE do backend, apresentada no início da implementação, pois o Backbone só chamará a callback success quando o servidor responder com algum JSON referente ao modelo excluído. O método removePost da classe PostView ficará da seguinte forma.

[javascript]
removePost: function(e) {
e.preventDefault();
if (window.confirm(‘Are you sure to remove this post?’)) {
this.model.destroy({
wait: true,
success: function(model, response, options) {
window.alert(‘Post excluído com sucesso!’);
router.navigate("/", {trigger: true});
}
});
}
}
[/javascript]

O blog está quase finalizado, a próxima etapa agora é a de fornecer uma cor diferenciada para a postagem que o usuário leu por último. Dessa forma, ele irá saber qual a última postagem do blog que ele visualizou e poderá continuar lendo as demais interessantíssimas postagens. Para fazer isso, será customizado o Backbone.sync de um Model específico para que o mesmo utilize offline storage, um recurso novo introduzido pelas APIs do HTML5. Essa funcionalidade poderia ser implementada utilizando puramente esse recurso, porém vamos utilizar uma biblioteca de offline storage já disponibilizada pela comunidade, ela se chama Backbone.localStorage.

Primeiramente, vamos criar um novo Model representando uma postagem recentemente lida, ele estará na pasta models e no arquivo PostReaded.js.

[javascript]
var PostReaded = Backbone.Model.extend({
localStorage: new Backbone.LocalStorage("PostReaded"),
defaults: {
id: 1,
post_id: ”
}
});
[/javascript]

Agora, quando o usuário clicar em uma postagem, devemos inserir um registro referente ao PostReaded, portanto o seguinte código é adicionado ao método viewAction do AppRouter criado até então.

[javascript]
var lastPost = new PostReaded();
lastPost.fetch();
lastPost.set({"post_id":id});
lastPost.save();
[/javascript]

Agora, basta verificar se é o último post lido e modificar a cor do título. Primeiramente, é adicionado o método isReaded ao PostModel, ele irá obter o PostReaded atual e verificar se a postagem é a última lida pelo usuário.

[javascript]
isReaded: function() {
var lastPost = new PostReaded();
lastPost.fetch();

return lastPost.get(‘post_id’) == this.get(‘id’);
}
[/javascript]

Por último, no PostView, é adicionada a classe CSS readed caso o método isReaded retorne true. Portanto, o seguinte código é adicionado ao método render.

[javascript]
if (this.model.isReaded()) {
this.$el.find(‘h2’).addClass(‘readed’);
}
[/javascript]

Dessa forma, a última melhoria que falta é a de deixar o blog com uma cara mais bonita. Um pequeno CSS já ajuda nisso, apesar de que o trabalho de um designer profissional seria indispensável em uma aplicação real. Portanto, basta criar o arquivo public/css/blog.css com o seguinte conteúdo:

[css]
body {
font-family: Arial, Helvetica, sans-serifs;
}

.site-header {
border-bottom: 1px #ccc solid;
}

h1 {
font-size: 32px;
}

h1, h2, h2 a {
color: #034C7C;
}

.toolbar a {
color: #999;
}

h2 {
float: left;
font-size: 28px;
}

.readed a {
color: #FF8500;
}

form {
margin-bottom: 10px;
}

.remove-button {
color: red;
display: inline-block;
font-size: 12px;
margin-top: 34px;
padding-left: 10px;
}

.post-content {
clear: both;
}

.clear {
clear: both;
}
[/css]

O resultado final do blog desenvolvido ao longo destes cinco artigos é uma página funcional, com suporte para histórico e dados dinâmicos, usufruindo de muitos dos componentes do Backbone.js, assim como de um backend escrito em Sinatra. Ao abrir o blog, o usuário poderá ver todas as postagens cadastradas (via rotas).

Página inicial do blog.
Página inicial do blog.

Poderá também incluir uma nova postagem.

Formulário de Inclusão de uma nova postagem
Formulário de Inclusão de uma nova postagem.

O formulário de inclusão de postagem faz a validação dos campos obrigatórios.

Campos obrigatórios não preenchidos
Campos obrigatórios não preenchidos
Erro de Validação
Erro de Validação

A inclusão de uma postagem exibe uma mensagem de sucesso e esconde o formulário (via eventos).

Post inserido.
Post inserido.
Formulário escondido.
Formulário escondido.

Pressionar o mouse no título de uma postagem permite a sua visualização, com suporte para ações padrão de voltar/avançar do browser (via rotas).

Visualização de postagem, a sua rota aparece na barra de endereços
Visualização de postagem, a sua rota aparece na barra de endereços
Listagem de postagens a partir do botão voltar do browser
Listagem de postagens a partir do botão voltar do browser

Uma postagem pode ser removida.

Confirmação de remoção de uma postagem.
Confirmação de remoção de uma postagem.
Postagem removida com sucesso.
Postagem removida com sucesso.

E a última postagem lida (clicada) pelo usuário é destacada (via offline storage).

A postagem do meio é a última lida (clicada) pelo usuário.
A postagem do meio é a última lida (clicada) pelo usuário.
Informações do offline storage do Google Chrome
Informações do offline storage do Google Chrome

Note que não precisamos incluir versões diferenciadas do framework, por isso não foi implementado nada com as funções utilitárias. Outro ponto é que o backend não suporta as rotas definidas no Router, ou seja, ao dar o F5 quando uma postagem estiver sendo exibida, o Sinatra apresentará uma página de erro.

Página de erro do Sinatra
Página de erro do Sinatra

Código-fonte

O código-fonte de todos os artigos desta séria sobre Backbone.js encontra-se no repositório backbone-tutorial-series do meu GitHub. Se você tiver interesse em dar continuidade nesse projeto simples de blog, fique à vontade, faltam muitos recursos e tenho curiosidade em ver o que pode resultar desse simples projeto.

Conclusões

Nestes cinco artigos, foram apresentadas as principais classes e funções do framework Backbone.js. O principal objetivo até este artigo era apresentar o Backbone conforme os tópicos de sua documentação. No próximo artigo, o foco será mudado, ele terá uma abordagem totalmente prática, focando no desenvolvimento de uma outra aplicação do início ao fim. Todos os conceitos apresentados até aqui serão aplicados, e a novidade será a utilização de diversas outras bibliotecas JavaScript para estruturar melhor uma aplicação com Backbone, além de algumas funcionalidades comuns a aplicações web como, por exemplo, autenticação, paginação, entre outras. Também será construído um outro backend, dessa vez usando PHP.

Se você, leitor, possui alguma sugestão do que incluir no próximo artigo, podendo ser uma funcionalidade específica, uso de alguma biblioteca/framework específico, deixe sua sugestão aqui nos comentários. Avaliarei os itens mais pedidos por vocês e farei de tudo para incluir no próximo artigo.

Para ressaltar: o próximo artigo será totalmente prático, portanto é altamente recomendável o acompanhamento dos 5 primeiros artigos para entender com mais facilidade tudo que será passado.

Tópicos já previstos:

  • Require.js
  • Twitter Bootstrap
  • Autenticação
  • Paginação
  • Backend em PHP
  • Versão mobile via Apache Cordova

Referências

Para a construção deste artigo a documentação do Backbone.js foi utilizada, em conjunto com alguns vídeos do curso de Backbone.js da CodeSchool. Também foi utilizada a documentação do Sinatra, e a documentação do ActiveRecord.

Até o próximo artigo.