Back-End

9 ago, 2012

Série Backbone.js: Parte 03 – Model

Publicidade

O Backbone.js é um framework Javascript que fornece componentes para melhorar a estrutura de aplicações web. Dentre os componentes, encontra-se o Model, responsável por representar os dados de uma aplicação, conter regras de negócio, incluindo validações, conversões, controle de acessos e definir os aspectos de persistência.

Introdução

No primeiro artigo desta série, foi apresentado o framework Backbone.js, seus principais conceitos e aspectos e foi feita 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 e a construção de uma View para um simples blog.

Neste terceiro artigo, será apresentada a classe Backbone.Model, com exemplos práticos, integração com backend e utilização na classe Backbone.View.

Backbone.Model

Conforme dito no segundo artigo, apesar de muitos desenvolvedores não acharem o Backbone um framework completamente MVC, a documentação oficial faz uma analogia, comparando os componentes com o framework Ruby on Rails.

Seguindo essa premissa, a classe Backbone.Model representa a camada Model da aplicação, que, segundo a documentação oficial, é a fundação de aplicações Javascript, onde estarão presentes os dados dinâmicos, assim como a lógica que os envolve, como conversões, validações, propriedades computadas e controle de acesso.

Continuando a ideia de desenvolver um simples blog, um Model que representa uma postagem pode ser definido conforme o código a seguir:

var PostModel = Backbone.Model.extend({});

A primeira premissa a se destacar na classe Backbone.Model, é que ela não possui funcionalidades para trabalhar com várias linhas de dados. Em outras palavras, diferente do Model do Ruby on Rails, o Model do Backbone só permitirá trabalhar com uma instância por vez e não fornecerá métodos como all() do ActiveRecord::Base. Para trabalhar com coleções de Models, outra classe é utilizada, e a mesma será apresentada no próximo artigo.

O Backbone.Model também possui o método initialize(), que é o construtor padrão das classes do framework Backbone, porém, ao contrário das demais classes, é raro modificar o comportamento padrão da classe Backbone.Model, que receberá um hash contendo os atributos que serão definidos no Model. Para o PostModel, os atributos serão: título e texto. O código abaixo demonstra a utilização do construtor:

var post = new PostModel({
title: 'Primeiro Post',
text: 'Conteúdo do post'
});

Para obter o valor de um atributo, utiliza-se o método get(), passando como parâmetro o atributo em questão. Para definir um novo valor a um atributo é utilizado o método set(), que recebe como parâmetro um hash contendo o identificador do atributo a ser definido e seu novo valor. O código abaixo exemplifica o uso destes métodos:

console.log(post.get('title')); // Primeiro Post
console.log(post.get('text')); // Conteúdo do post
post.set({text: "Novo conteúdo");
console.log(post.get('text'); // Novo conteúdo

Os atributos de um Model, definidos por set() ou pelo initialize(), são armazenados internamente em um hash interno da classe Backbone.Model chamado attributes. Uma boa prática ao se trabalhar com a Backbone.Model é a de não utilizar diretamente o attributes e sim os métodos get() e set().

Outro método para obter o valor de um atributo é o escape(), que funciona de maneira similar ao get(), com a diferença de retornar uma versão com tratamento de tags HTML, para prevenir XSS.

post.set({
title: '<h1>HTML</h1>',
text: '<h2><script>alert("xss");</script></h2>'
});
console.log(post.escape('text'));
// imprime:
// &lt;h2&gt;&lt;script&gt;alert(&quot;xss&quot;);&lt;/script&gt;&lt;/h2&gt;

O método has() verifica se existe o atributo informado no objeto e se o mesmo não está null ou undefined. O unset() irá remover um atributo do objeto e o clear() irá limpar todos os atributos.

if (post.has('title')) {
post.unset('title');
}
console.log(post.get('title')); // undefined
post.clear();
console.log(post.get('text')); // undefined

É possível notar que os atributos de um Model são definidos dinamicamente no framework Backbone, porém com o atributo defaults, é possível preencher o Model com atributos e valores padronizados, que só serão sobrescritos caso novos valores sejam definidos no construtor da classe ou via o método set(). O exemplo abaixo demonstra isso.

var PostModel = Backbone.Model.extend({
defaults: {
title: "",
text: ""
}
});
var post = new PostModel();
console.log(post.get('title')); //
var post = new Post({title: 'Meu Post'});
console.log(post.get('title')); // Meu Post
console.log(post.get('text'); //

Para converter um objeto Backbone.Model para notação JSON é utilizado o método toJSON(). Seu funcionamento é simples: ele irá utilizar o método _.clone() da biblioteca Underscore.js, e retornar um array contendo os atributos e valores do objeto. Esse método pode ser utilizado para persistência de dados, serialização, utilização em templates, entre outros. O exemplo abaixo demonstra o funcionamento deste método utilizando o método stringify(), da API Javascript.

var post = new PostModel({title: "Primeiro Post"});
post.set({text: "conteudo"});
alert(JSON.stringify(post)); // {"title":"Primeiro Post","text":"conteudo"}
alert(post.toJSON());

A classe Backbone.Model possui também os atributos id, idAttribute e cid. O primeiro atributo é o identificador único do Model e é utilizado para obter um determinado Model de uma coleção, assim como para construir URLs para o Model. O segundo atributo é utilizado quando o atributo id não é o identificador único de um Model. Ao definir idAttribute, o atributo customizado definido será mapeado internamente para o atributo id. E o terceiro atributo, cid (client id) é uma propriedade especial utilizada para identificar a primeira instância de Models, útil principalmente para casos onde os Models não estão gravados ainda no servidor, e não possuem um valor real para o atributo id, porém precisam estar presentes na parte visual. O atributo cid conterá valores no formato: c1, c2, c3, etc. O código abaixo demonstra a utilização de cada atributo:

console.log(post.id); // undefined

var PostModel = Backbone.Model.extend({
idAttribute: 'timestamp'
});

var post = new PostModel({timestamp: '201201'});
console.log('Id: ' + post.id); // Id: 201201
console.log('Timestamp: ' + post.get('timestamp')); // Timestamp: 201201
console.log('CID: ' + post.cid); // CID: c4

Esses são os conceitos, atributos e métodos básicos para se trabalhar com a classe Backbone.Model. A partir desta etapa serão abordados os conceitos de persistência de dados e a PostView criada no artigo anterior será modificada para interagir com a classe PostModel.

Backend

Para as próximas etapas deste artigo, é necessário criar uma API RESTful para utilizar em conjunto com os métodos de persistência e sincronização do Backbone. Neste artigo foi criada uma API simples utilizando o Sinatra – um framework Ruby que agiliza bastante a criação de APIs RESTful. O primeiro passo é criar um banco de dados utilizando SQLite:

sqlite3 data.db
sqlite> create table posts(id integer not null primary key autoincrement, title varchar(200), text varchar(255));

Com o banco de dados criado, é necessário instalar o Sinatra junto com o driver sqlite3 para Ruby:

sudo gem install sinatra sqlite3 active_record

A próxima etapa é criar o arquivo posts.rb e incluir o seguinte código:

require 'sinatra'
require 'json'
require 'active_record'

# O JSON não deve conter um elemento ROOT, apenas os atributos ActiveRecord::Base.include_root_in_json = false class Post < ActiveRecord::Base end Post.establish_connection( :adapter => "sqlite3", :database => "data.db" )

# Apresenta a página contendo o código HTML e Javascript
get '/' do
File.read(File.join('public', 'index.html'))
end

# Endpoint GET para obter a última postagem do banco de dados
get '/posts' do
content_type :json
Post.last.to_json
end

# Endpoint POST para criar uma nova postagem
post '/posts' do
data = JSON.parse request.body.read

post = Post.new
post.title = data['title']
post.text = data['text']

post.save
end

# Endpoint PUT para atualizar uma postagem existente
put '/posts/:id' do
data = JSON.parse request.body.read

post = Post.find params[:id]
post.title = data['title']
post.text = data['text']

post.save
end

# Endpoint DELETE para remover uma postagem existente
delete '/posts/:id' do
Post.destroy params[:id]
end

O primeiro parâmetro configura o ActiveRecord para não incluir um elemento pai ao se fazer a conversão para JSON, ou seja, apenas os atributos da classe estarão presentes. É definido, então, o Model Post, os parâmetros para conexão de banco de dados, uma rota padrão que irá apenas retornar o arquivo index.html da pasta public, uma rota GET /posts que irá retornar a última postagem do banco de dados, uma rota POST para criação de uma nova postagem, uma rota PUT para atualização de uma postagem e, por último, uma rota DELETE para exclusão de uma postagem. Quem está acostumado a trabalhar com APIs RESTful irá notar que a rota GET /posts está inconsistente, principalmente por ela retornar apenas uma postagem, o que deveria ser feito em uma rota GET /posts/:id. Essa abordagem foi utilizada apenas para simplificar este artigo e a utilização no framework Backbone, porém no próximo artigo isso será modificado.

A próxima etapa é criar o arquivo index.html e incluí-lo na pasta public. Este arquivo irá conter o código HTML básico e o código Javascript com Backbone.js. Um exemplo simples desse arquivo é apresentado abaixo:

<!doctype html>
<html>
<head>
<title>Último Post</title>
<meta charset="UTF-8" />
<script src="../lib/jquery-min.js"></script>
<script src="../lib/underscore-min.js"></script>
<script src="../lib/backbone-min.js"></script>
<script src="Models.js"></script>
</head>
<body>
</body>
</html>

No arquivo Models.js encontra-se a classe PostModel, apresentada a seguir:

var PostModel = Backbone.Model.extend({
defaults: {
title: "",
text: ""
}
});

Para iniciar o servidor, execute o seguinte comando:

ruby products.rb
== Sinatra has taken the stage ...
>> Listening on 0.0.0.0:4567

Não esqueça de incluir na pasta public/lib os frameworks: jQuery, Underscore.js e Backbone.js.

Configuração do Endpoint

Ao se definir uma classe que herda do Backbone.Model, existem dois atributos necessários para configuração do endpoint RESTful do servidor: o primeiro é o urlRoot e o segundo o url. O atributo urlRoot representa o prefixo da URL. Quando não é utilizada a classe Backbone.Collection (mais sobre isso no próximo artigo), este prefixo é utilizado para gerar as URLs baseadas no atributo id do Model. O segundo atributo url, contém o endereço completo onde o recurso do Model é encontrado no servidor. A partir dele é possível definir URLs localizadas em outros servidores, caso necessário. Este atributo será construído dinamicamente, caso não seja definido explicitamente, podendo ser a partir do atributo url da classe Backbone.Collection. Ou, caso não seja usada a classe Backbone.Collection, a partir do atributo urlRoot da classe Backbone.Model. As URLs geradas podem, então, ter o formato: /[urlRoot]/id ou /[collection.id]/id.

Para a classe PostModel, o atributo urlRoot deverá ser configurado da seguinte forma:

var PostModel = Backbone.Model.extend({
urlRoot: 'posts',
defaults: {
title: "",
text: ""
}
});

Dessa forma, as URLs geradas serão:

  • GET: /posts ou /posts/id
  • POST: /posts
  • PUT: /posts/id
  • DELETE: /posts/id

Obtendo a última postagem

Com o servidor devidamente iniciado e a classe PostModel configurada, é possível obter a última postagem através do método fetch():

var post = new PostModel();
post.fetch();

Este método irá limpar o estado atual da instância de PostModel e sincronizar seus dados com os dados do servidor, utilizando o método Backbone.sync, que será explicado em um artigo futuro. Ele utilizará o primeiro endereço apresentado, /posts, e poderá executar dois callbacks: success executado quando o servidor retornar uma resposta e não apresentar erros e error caso ocorra algum erro. Ambas são apresentadas abaixo:

post.fetch({
success: function(model, response) {
console.log(model.get('title'));
},
error: function(model, response) {
window.alert('Ocorreu um erro');
}
});

Ao testar esse código, será impresso no console Javascript do browser o título da última postagem existente no banco de dados.

Gravando uma postagem

Para gravar uma postagem, utiliza-se o método save(). Internamente, assim como no caso do fetch(), ele utilizará o método Backbone.sync. Baseando-se no método isNew(), será executada uma requisição POST para a criação de um novo recurso no servidor ou um PUT para a atualização de um recurso já existente. isNew() considerará um objeto como um novo recurso ou não, caso o atributo id esteja vazio. O código abaixo grava uma nova postagem no servidor:

var post = new PostModel({
title: "First Post",
text: "Text of the post"
});
post.save();

Para atualizar uma postagem existente, o seguinte código é utilizado:

var post = new PostModel({
id: 1,
title: "First Post",
text: "Text of the post"
});
post.save();

O método save() recebe os parâmetros opcionais: attributes e options. O primeiro parâmetro define quais os atributos que serão atualizados no servidor, porém, ao enviar para o servidor, o recurso como um todo estará presente. Para ilustrar isso, considere o seguinte exemplo:

var post = new PostModel();
// Obtém a última postagem
post.fetch({
success: function(model, response) {
// Será atualizado somente o atributo "title"
post.save({
title: "Atualizar o titulo"
});
}
});

Quando o método save() faz a requisição, todos os valores do objeto são enviados, mas internamente na classe Backbone.Model serão considerados atualizados somente os atributos definidos no método save(). O segundo parâmetro, options, pode configurar diversos comportamentos, dentre eles os callbacks success e error. A principal diferença com relação aos callbacks do método fetch() é que caso ocorra algum erro de validação no PostModel, o callback error será executado. Mais sobre isso a seguir.

var post = new PostModel({
id: 1,
title: "First Post",
text: "Text of the post"
});
post.save(
null,
{
success: function (model, response) {
console.log(model.get('title'));
},
error: function (model, response) {
window.alert('Ocorreu um erro');
}
}
);

Removendo uma postagem

Para remover um recurso no servidor, utiliza-se o método destroy(). Este método também executará internamente o método Backbone.sync e fará uma requisição utilizando o método HTTP DELETE, definindo na URL o atributo id do Model. Caso o método isNew() retorne verdadeiro, o método destroy() retornará false e não fará a requisição. O destroy() recebe como parâmetro uma hash opcional, que também aceita as callbacks success e error, assim como o parâmetro wait: true, útil ao se utilizar o Model com a classe Backbone.Collection. Ao utilizar o wait: true, o Model em questão só será removido da coleção quando houver uma resposta do servidor indicando sucesso na remoção. O método abaixo faz a remoção de uma postagem:

var post = new PostModel({
id: 1
});
post.destroy({
success: function(model, response) {
console.log('Postagem removida com sucesso');
},
error: function(model, response) {
window.alert('Ocorreu um erro');
}
});

Eventos

A classe Backbone.Model trabalha com diversos eventos, permitindo sempre manter o estado de Backbone.Model atualizado nas demais partes da aplicação. Métodos como clear(), unset() e set() irão disparar o evento “change”, assim como os métodos fetch() e save(). Os método save() e destroy() irão disparar, além do “change”, um evento “sync” assim que a execução de uma requisição tiver uma resposta do servidor confirmando as operações de gravação e remoção, consecutivamente. O exemplo abaixo demonstra algum destes eventos:

var post = new PostModel();
post.on('change', function() {
console.log('O evento change foi disparado');
});
post.on('sync', function() {
console.log('O evento sync foi disparado');
});
post.set({
title: "Um titulo",
text: "Um texto "
}); // Dispara change
post.fetch(); // Dispara change e sync

Os métodos save() e destroy() recebem também o parâmetro wait: true, conforme mencionado na seção de remoção de uma postagem. No método destroy() este atributo é utilizado ao se trabalhar com a classe Backbone.Collection e evita que um Model seja removido de uma coleção até que o servidor confirme a operação. No caso do método save(), este atributo garantirá que os atributos do Model só sejam atualizados caso o servidor confirme a gravação.

É possível disparar manualmente um evento “change” com o método change(). Os atributos modificados no último evento “change” ficam armazenados no atributo changedAttributes, no formato de hash. Para verificar se um atributo foi alterado no último evento “change” o método hasChanged() é utilizado.

var post = new PostModel();
post.on('change', function() {
if (post.hasChanged('title'))
console.log('o atributo titulo foi alterado');
else
console.log('o atributo titulo não foi alterado');
});
post.set({
title: 'Evento manual',
text: 'Texto'
}, {
silent: true
});
post.change();

Para obter os valores anteriores aos do evento “change”, utiliza-se o método previous(), que recebe como parâmetro o atributo a ser obtido. O método previousAttributes() também pode ser utilizado, o mesmo irá retornar um hash com todos os atributos anteriores à modificação.

var post = new PostModel({
title: 'Valor antigo',
text: 'Texto antigo'
});

post.on('change', function() {
if (post.hasChanged('title')) {
console.log('o atributo titulo foi alterado');
console.log(post.previous('title'));
console.log(post.get('title'));
}
});
post.set({title: 'Novo valor'});
post.change();

Validação de dados

A classe Backbone.Model suporta validação de dados antes de enviá-los ao servidor. Essa validação é definida no método validate(), que inicialmente não possui implementação. Este método será chamado pelo Backbone antes de executar os métodos set() ou save(). Caso os dados estejam válidos, o método não deve retornar nada – caso contrário, um erro deve ser retornado, que pode ser desde uma string, representando o erro, ou um objeto de erro completo. Ao encontrar um erro, a execução de set() e save() será interrompida imediatamente, e um evento error será disparado pelo Backbone. O código abaixo faz a validação do preenchimento dos atributos title e text do PostModel:

var PostModel = Backbone.Model.extend({
urlRoot: 'posts',
defaults: {
title: "",
text: ""
},
validate: function(attrs) {
if (attrs.title == '')
return 'O título é obrigatório';
if (attrs.text == '')
return 'O texto é obrigatório'
}
});

var post = new PostModel();
post.on('error', function(model, error) {
alert(error);
});
post.save();

Para verificar se o Model está com dados válidos, o método isValid() também pode ser utilizado. Ele é muito utilizado ao se trabalhar com entradas de dados. O exemplo abaixo utiliza o método isValid() para exibir uma mensagem informativa ao usuário:

var post = new PostModel();
post.on('error', function(model, error) {
if (!model.isValid())
alert('Erro de validação: ' + error);
else
alert(error);
});
post.save();

Model + View

No artigo anterior foi criada uma View simples para exibir o conteúdo de uma postagem. Primeiramente, a View será alterada para utilizar o mecanismo de template Mustache.js. Faça o download da última versão e o inclua na pasta lib. Uma boa prática ao desenvolver aplicações com Backbone é a de definir o template fora do código-fonte Javascript. O arquivo public/index.html contém o seguinte código:

<!doctype html>
<html>
<head>
<title>Último Post</title>
<meta charset="UTF-8" />
<script src="../lib/jquery-min.js"></script>
<script src="../lib/underscore-min.js"></script>
<script src="../lib/backbone-min.js"></script>
<script src="Models.js"></script>
</head>
<body>
</body>
</html>

A principal diferença entre o Mustache.js e o mecanismo de template do Underscore.js são os coringas para inclusão de conteúdo dinâmico, onde no Underscore é utilizado uma notação similar ao do ERB <%= coringa %> e no Mustache.js {{coringa}}. Neste template foi incluído um botão para direcionar o usuário para a página public/add.html, que contém um formulário para adicionar uma nova postagem. Antes de ir para essa página, é necessário alterar a classe PostView:

var PostView = Backbone.View.extend({
tagName: 'article',
className: 'page-posts',
events: {
"click #remove-button": "removePost"
},

initialize: function() {
_.bindAll(this, 'render', 'removePost', 'refresh');

this.template = $('#post-template').html();

this.model = new PostModel();

this.model.on("change", this.render);
this.model.on("destroy", this.refresh);
this.model.fetch();
},

render: function() {
console.log("Rendering...");
var rendered = Mustache.to_html(this.template, this.model.toJSON());
this.$el.html(rendered);
$('body').append(this.el);
},

removePost: function() {
this.model.destroy();
},

refresh: function() {
this.model.clear({silent: true});
this.model.fetch();
}
});

Algumas mudanças foram necessárias para utilizar o framework Mustache.js. Primeiramente, o atributo template é criado no construtor da classe, pois neste momento o DOM já estará carregado e o template será encontrado. A segunda mudança é no método render(), onde o método to_html() é utilizado, criando o HTML a partir do template e dos atributos dinâmicos definidos no PostModel – obtidos a partir do método toJSON(). A terceira mudança é a adição do trecho _.bindAll(this, ‘render’, ‘removePost’, ‘refresh’). Esse código irá permitir a utilização de this referenciando para a instância de PostView nos métodos definidos.

Ao executar a aplicação, o código desenvolvido renderizará a seguinte tela:

No código criado são adicionadas callbacks para os eventos “change” e “destroy”. Nesta View, o evento “change” será disparado no método fetch(), e seria disparado também no método clear(), caso não fosse definido o atributo silent: true. Tratando este evento com o método render(), fica garantido que as alterações do Model serão refletidas na View. Para testar este recurso, abra o console Javascript do navegador e execute o seguinte código:

postView.model.set({title: "Alteração de Título"});

Ao executá-lo, a View será atualizada automaticamente, renderizando a página a seguir:

Por último, ao remover uma postagem a partir do método removePost(), o callback do evento “destroy” será executado e ele irá limpar o estado do Model e irá sincronizá-lo novamente com o servidor, obtendo a última postagem existente.

A segunda página é a public/add.html, que permitirá incluir uma nova postagem:

<!doctype html>
<html>
<head>
<title>Nova Postagem</title>
<meta charset="UTF-8" />
<script src="lib/jquery-min.js"></script>
<script src="lib/underscore-min.js"></script>
<script src="lib/backbone-min.js"></script>
<script src="lib/mustache.js"></script>
<script src="js/models/PostModel.js"></script>
<script src="js/views/PostFormView.js"></script>
</head>
<body>
<script type="text/template" id="post-form">
<h2>Adicionar Post</h2>
<p><label>Title: <input type="text" id="post-title" /></label></p>
<p><label>Text: <textarea id="post-text"></textarea></label></p>
<p><input type="submit" value="Salvar" /></p>
</script>
<script>
var postView = new PostFormView();
</script>
</body>
</html>

A classe PostFormView é definida no arquivo js/views/PostFormView.js:

var PostFormView = Backbone.View.extend({
tagName: 'form',
className: 'page-form',
id: 'post-form',
attributes: {
action: 'posts',
method: 'POST'
},
events: {
"submit" : "savePost"
},

initialize: function(model) {
_.bindAll(this, 'render', 'savePost', 'goToIndex');

this.template = $('#post-form').html();
this.model = new PostModel();

this.model.on("error", this.showError);
this.model.on("sync", this.goToIndex);
this.render();
},

render: function() {
var rendered = Mustache.to_html(this.template);
this.$el.html(rendered);

this.titleInput = this.$el.find('#post-title');
this.textInput = this.$el.find('#post-text');

$('body').append(this.el);
},

savePost: function(e) {
e.preventDefault();

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

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

if (this.model.isValid())
this.model.save();
},

showError:function(model, error) {
window.alert('Ocorreu um erro, motivo: ' + error);
},

goToIndex: function() {
window.location = 'index.html';
}
});

Esse código irá adicionar callbacks para os eventos “error” e “sync”, assim como a View irá tratar o evento “submit” do formulário, obter os valores dos campos de texto, verificar se os dados são válidos e criar um novo recurso no servidor. Caso ocorra algum erro de validação, um alert será exibido ao usuário. No evento “sync”, que será chamado quando o servidor retornar para o Backbone que a postagem foi criada, o usuário será redirecionado para a página contendo a última postagem.

E execução de add.html renderizará o seguinte formulário:

Caso algum dos campos não seja preenchido, será exibida uma mensagem similar à apresentada a seguir:

Se os dados forem válidos, o Backbone irá gravar os dados no servidor e a página public/index.html será exibida, contendo a postagem recém inserida:

Código-fonte

O código-fonte de todos os artigos desta séria sobre Backbone.js encontram-se, também, no repositório backbone-tutorial-series do meu GitHub.

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.

Como foi possível notar em todo o artigo, não foi construída uma listagem das postagens. Foi ressaltado logo no início que a classe Backbone.Model corresponde a uma instância apenas e não fornece métodos para trabalhar com coleções de dados.

No próximo artigo, será apresentada a classe Backbone.Collection, com todos os métodos fornecidos para trabalhar com conjuntos de dados, criação da listagem das postagens do simples blog proposto, e também será alterado o backend desenvolvido com Sinatra, para deixá-lo com uma API RESTful mais consistente.