O Backbone.js é um framework Javascript que fornece componentes para melhorar a estrutura de aplicações web. Dentre os componentes encontram-se o Router e o History, responsáveis pela criação de rotas e gestão do histórico do browser via Javascript. Além destes componentes, existe a função Backbone.sync que é a função 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 da série 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.

# 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 da History API, introduzida no HTML5. Backbone.Router utilizará por padrão a History API, e no caso do browser não suportar esta 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 este 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 também é 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,  onde 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:

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 estes 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 também 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({
  //...
  novoPost: 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 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 (#). Esta 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, este trabalho ficou mais transparente ao usuário, tirando a necessidade de definir as URLs com hashes. Dentro desta 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, utilizar ele é 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. Dentre 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. Caso o servidor renderize 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, necessitará 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 facilidade de extensão, a função sync() também pode ser extendida 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() poderia ser customizado para trabalhar com o banco de dados local, ou até, se o servidor suporta apenas transporte por XML, isso também poderia 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 extender 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:

  • 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, também é 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 à 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 este 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 estes 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 definí-lo como corpo da requisição HTTP. Ao invés 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, neste 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 destes eventos já pré-definidos, também existe o módulo Events, que permite trabalhar com eventos customizados. Este 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 este 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 destes 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 da callback, 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:

object.off("all", globalCallback);

Todos estes 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 pode-se definir mais parâmetros neste método, que serão lançados como argumentos do evento, estes argumentos 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 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.

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

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

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, este 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 onde 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.

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

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.

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();
}

Se você acompanhou os artigos anteriores vai notar que existe uma chamada à this.header, não apresentada em nenhum destes 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.

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);
    }
  });
}

Ambos os métodos precisam de um atributo em comum: o header. Este atributo 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 este atributo.

initialize: function() {
  if (!this.header) {
    this.header = new HeaderView();
    $('#content').before(this.header.render().el);
  }
}

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.

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;
        }
    }
});

Todo esse código não irá funcionar se não inicializarmos o Router e o suporte à History API.

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

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.

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

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, onde um evento customizado form:hide indicará essa remoção. A atribuição do listener deste evento ficará no método showForm.

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

Agora lá no formulário PostFormView será necessário lançar este evento, assim que o Post for gravado, isso ficará à cargo do método postSaved.

postSaved: function() {
  window.alert('Post gravado com sucesso!');
  this.trigger("form:hide");
}

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

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();
}

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

events: {
  "click .remove-button": "removePost",
  "click .view-button": "showPost"
}

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.

showPost: function(e) {
  router.navigate($(e.currentTarget).attr('href'), {trigger: true});
  e.preventDefault();
}

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

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});
      }
    });
  }
}

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

var PostReaded = Backbone.Model.extend({
  localStorage: new Backbone.LocalStorage("PostReaded"),
  defaults: {
    id: 1,
    post_id: ''
  }
});

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 Router criado até então.

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

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.

isReaded: function() {
  var lastPost = new PostReaded();
  lastPost.fetch();
  return lastPost.get('post_id') == this.get('id');
}

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.

if (this.model.isReaded()) {
  this.$el.find('h2').addClass('readed');
}

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:

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;
}

O resultado final do blog desenvolvido ao longo destes 5 artigos é uma página funcional, com suporte a 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.

Poderá também incluir uma nova postagem.

Página inicial do blog.

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

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

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

Post inserido. Formulário escondido.

Pressionar o mouse no título de uma postagem permite a sua visualização, com suporte às 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. Listagem de postagens a partir do botão voltar do browser.

Uma postagem pode ser removida.

Confirmação de remoção de uma postagem. 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. 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.

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 o 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 outros. 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.

Por favor, não copie este artigo na íntegra, se gostaria de referenciar escreva com suas próprias palavras e referencie o link original. Obrigado!