Por Fabrício Tavares / 2013-02-20

Conceitos básicos do Ember.js


Post desatualizado

Atenção! Esse post está desatualizado, e utiliza a versão v.1.0.0-rc.1.


Obs. 1: Esse post assume um conhecimento básico de JavaScript.

Obs. 2: Decidi não traduzir vários termos em inglês para o português por serem conceitos fundamentais ao Ember. Quando possível, utilizo de termos em português. Entretanto, não se preocupe, pois todos os conceitos estão explicados. Qualquer dúvida quanto a isso, coloque nos comentários.

Obs. 3: Este não pretende ser um guia completo, apenas uma introdução. Trabalharei conceitos mais avançados em futuros posts.

O que é o Ember.js?


Do próprio site:

A framework for creating ambitious web applications.

Em uma tradução livre: Um framework para criação de grandes aplicações web.

Vou falar sobre os conceitos básicos necessários para a criação de um micro aplicativo (lista de pessoas e seus frameworks favoritos), que será construído ao longo do texto. A versão final está aqui.

Por quê utilizar o Ember?


Se você já teve a experiência de criar uma página web, com várias requisições ajax e modificações da interface de usuário, sabe que, em pouco tempo, o código vira um espaguete. Extremamente complicado de manter, principalmente quando há mais de uma pessoa trabalhando no projeto.

Portanto, se há a necessidade de ações CRUD - create (criar), read (ler), update (alterar) e delete (remover) - na página, e você deseja melhorar a performance, evitando recarregamento a cada ação, o padrão de arquitetura MVC, adotado pelo Ember, vai facilitar sua vida.

É claro que é possível ter código bem estruturado sem a utilização de um framework. Entretanto, quando a sua base de código começar a crescer e crescer, e quando começar projetos novos, vai perceber que estará escrevendo as mesmas funções para cada uma das aplicações.

"Trivial choices are the enemy" - Yehuda Katz, Ember Core Team

Ember se apóia bastante no paradigma convention over configuration. Quase sempre o que é necessário para a aplicação será gerado automaticamente, em memória, sem precisar, explicitamente, instanciar classe alguma.

Isso não significa que não seja possível configurá-lo para ficar exatamente da forma como te agrada. Apenas elimina as decisões triviais que toda aplicação requer ao longo do seu desenvolvimento.

Padrão de arquitetura MVC


MVC significa Model View Controller. Não vou entrar em detalhes sobre a história desse padrão, nem sua evolução ao longo dos anos. Falarei apenas dos conceitos necessários para entender e construir uma aplicação com Ember.

MVC no Ember é como MVC em aplicações desktop. Portanto, se você já utiliza algum framework MVC no servidor, como Django, Zend ou Ruby on Rails, deixe de lado por um momento os conceitos que você já sabe.

Application


Para criar uma aplicação, primeiramente é necessário instanciar a classe Ember.Application.

window.App = Ember.Application.create();

App será o namespace do aplicativo. Aqui usei a palavra App, mas pode ser qualquer outra. A convenção é que o nome comece com letra maiúscula.

Todas as classes serão instanciadas sob esse namespace.

Templates


Toda a interface de usuário residirá nos templates. O Ember utiliza, por padrão, o Handlebars. É possível utilizar outro template, mas, a não ser que você saiba bem o que está fazendo, não recomendo. Ganhamos várias coisas ao utilizar o Handlebars, sem precisar preocupar com coisas triviais.

O template default do aplicativo é chamado application, e é criado da seguinte forma:

<script type="text/x-handlebars" data-template-name="application">
Esse é o template padrão.

{{ outlet }}
</script>

Esse é o menor aplicativo possível, criado com Ember. Muita coisa está acontecendo nos "bastidores". Vejamos:

  1. o aplicativo é inicializado;
  2. o template application é renderizado;
  3. automaticamente é criado um route, que reflete o estado do aplicativo na URL.

O {{ outlet }} serve como um espaço reservado para outros templates.

Ao estender o controller abaixo, tornamos disponível alguns dados para o template application.

App.ApplicationController = Ember.ObjectController.extend({
    nome: 'Eddard',
    sobrenome: 'Stark'
});
<script type="text/x-handlebars" data-template-name="application">
Olá, {{ nome }} {{ sobrenome }}!

{{ outlet }}
</script>

O resultado HTML será:

<div id="ember151" class="ember-view">
    Olá, Eddard Stark!
</div>

Não se preocupe com os atributos HTML id e class por enquanto.

Router


O router é responsável pela manutenção do estado do aplicativo. Como estamos construindo um aplicativo web, que roda em um navegador, seria ótimo se o estado ativo fosse refletido na URL.

Bem, o Ember faz isso automaticamente.

Vamos criar uma nova rota para listar as pessoas do nosso app.

App.Router.map(function(){
    this.resource('people');
});
<script type="text/x-handlebars" data-template-name="people">
<h1>Pessoas</h1>
</script>

Isso criará automaticamente a seguinte URL:

http://dominio.com/#/people

Ao ser acessada, o template people será renderizado e adicionado no outlet do template application.

Além da manutenção dos estados, o router é também responsável por atribuir aos controllers a representação de um ou mais models, e lidar com eventos.

Resources e routes

Há duas formas de se criar um novo route:

App.Router.map(function(){
    // Cria um route e um novo namespace
    this.resource('people');

    // Cria um route no namespace que o route está aninhado
    this.route('new');
});

Ao criar um novo resource, será criado também um novo namespace; quando é criado um route, o namespace é o do route "pai". Isso vai afetar a forma como se declara os nomes dos controllers, views e templates. Ao criar os routes como no código acima, temos o seguinte:

URL Nome do route Controller Route Template
/ index App.IndexController App.IndexRoute index
/people people App.PeopleController App.PeopleRoute people
/new new App.NewController App.NewRoute new

A medida que a complexidade da aplicação aumenta, será necessário ter routes aninhados. Eles podem ser criados da seguinte forma:

App.Router.map(function(){
    this.resource('people', function() {
        this.route('new');
    });
});

E os nomes ficam assim:

URL Nome do route Controller Route Template
/ index App.IndexController App.IndexRoute index
N/A people App.PeopleController App.PeopleRoute people
/people people.index App.PeopleIndexController App.PeopleIndexRoute people/index
/people/new people.new App.PeopleNewController App.PeopleNewRoute people/new

Veja que, ao aninhar o route new ao resource people, automaticamente foi criado um novo route, o index. Todo resource que tiver routes aninhados, terá um route index.

Dynamic segments

Ao acessar um determinado estado da aplicação a partir de uma URL, o route utiliza a informação da URL para determinar o model correspondente, e carregar esse model para o controller.

A versão final do router do nosso aplicativo será:

App.Router.map(function(){
    this.resource("people", function() {
        this.resource('person', { path: ':person_id' });
        this.route('new');
    });
});

Repare que o valor da property path, no resource person, possui um :, seguido do nome do resource junto com um _id. Isso faz com que, ao acessar a URL /people/3, o router automaticamente busque pelo model App.Person com id 3.

Caso haja interesse em ter uma URL com nome diferente do nome do route, como, por exemplo, o nome "pessoas" para o route people, é só adicionar o valor à property path.

App.Router.map(function(){
    this.resource("people", { path: '/pessoas' }function() {
        this.resource('person', { path: ':person_id' });
        this.route('new');
    });
});

Os nomes ficam assim:

URL Nome do route Controller Route Template
/ index App.IndexController App.IndexRoute index
N/A people App.PeopleController App.PeopleRoute people
/people people.index App.PeopleIndexController App.PeopleIndexRoute people/index
/people/:id person App.PersonController App.PersonRoute person
/people/new people.new App.PeopleNewController App.PeopleNewRoute people/new

Models


Os models são responsáveis pelo controle dos dados da aplicação. São completamente independentes da interface do usuário, apesar de serem requisitados por ela. Ao sofrer atualização, o model notifica os observers, que traduz isso para a UI.

No Ember, os models são javascript objects ligeiramente modificados (para suportar bindings e outras funcionalidades).

Definindo um Model

App.Person = DS.Model.extend({
    firstName: DS.attr('string'),
    lastName: DS.attr('string'),
    fullName: function() {
        return this.get('firstName') + ' ' + this.get('lastName');
    }.property('firstName', 'lastName')
});

Estou utilizando o Ember Data, uma biblioteca responsável por carregar dados do servidor, fazer alterações no navegador, e salvar as alterações de volta ao servidor. Esta biblioteca ainda não está mesclada ao Ember, mas o será em breve.

As properties firstName e lastName são carregadas diretamente do servidor através de alguma REST API, e serão persistidas ao servidor após as alterações. A property fullName é uma computed property, que tem seu valor determinado a partir de alguma função. No código acima, ela é composta do nome e sobrenome (firstName e lastName).

Toda vez que qualquer uma das properties do model App.Person for alterada, a computed property também será.

Definindo uma Store

A Store é o repositório que contém todos os models já carregados, além de ser responsável pelo carregamento dos models que ainda não foram carregados.

App.Store = DS.Store.extend({
    revision: 11,
    adapter: DS.RESTAdapter.create();
});

A property revision é o número de revisão da API, utilizada para a notificação de alterações que podem quebrar código já existente. O adapter é responsável pela comunicação com o servidor.

O Ember Data possui dois adapters padrões: DS.FixtureAdapter e o DS.RESTAdapter. O primeiro é utilizado para dados hard coded (extremamente útil para prototipagem de uma aplicação, já que independe do backend), enquanto que o segundo é usado quando se tem um servidor REST.

Para esse aplicativo, vamos usar uma REST API criada em node.js, que persiste os dados apenas no processo. Essa REST API possui apenas um resource:


GET http://api.dadospublicos.org/people
GET http://api.dadospublicos.org/people/:id
POST http://api.dadospublicos.org/people
DELETE http://api.dadospublicos.org/people/:id

Para utilizá-la na aplicação, vamos configurar a sua url no adapter.

App.RESTSerializer = DS.RESTSerializer.extend({
    init: function() {
        this._super();

        this.map('App.Person', {
            frameworks: {embedded: 'always'}
        });

        this.configure('plurals', {
            person: 'people'
        });
    }
});

App.RESTAdapter = DS.RESTAdapter.extend({
    url: 'http://api.dadospublicos.org',
    bulkCommit: false,
    serializer: App.RESTSerializer.create()
});

App.Store = DS.Store.extend({
    revision: 11,
    adapter: App.RESTAdapter.create();
});

Controllers


Um controller é responsável por representar um model para um template e por armazenar properties que não serão salvas no servidor.

Para representar um único model, utiliza-se o Ember.ObjectController. Quando é necessário representar uma matriz (array) de models, utiliza-se o Ember.ArrayController.

É no route que se determina qual será o model que será representado no controller. Para carregar o model App.Person no controller App.PersonController, utilizamos a property model, como mostrado abaixo.

App.PeopleRoute = Ember.Route.extend({
    model: function() {
        return App.Person.find();
    }
});

Adicionar pessoa à lista

O controller App.PeopleNewController será o responsável por adicionar novas pessoas à lista. Precisamos, então, configurar seu model. Como o route people.new é para a criação de uma nova pessoa, não há model algum sendo carregado.

App.PeopleNewRoute = Ember.Route.extend({
    model: function() {
        // Model vazio
        return null;
    },

    setupController: function(controller) {
        controller.startEditing();
    },

    exit: function() {
        this._super();
        this.controllerFor('people.new').stopEditing();
    }
});

Toda a manipulação do model é feita no controller.

App.PeopleNewController = Ember.ObjectController.extend({
    startEditing: function() {
        this.transaction = this.get('store').transaction();
        this.set('content', this.transaction.createRecord(App.Person, {}));
    },

    stopEditing: function() {
        if (this.transaction) {
            this.transaction.rollback();
            this.transaction = null;
        }
    },

    save: function() {
        console.log( "return", this.validate() );
        if (this.validate()) {
            this.transaction.commit();
            this.transaction = null;
        } else {
            alert('Nome inválido');
        }
    },

    validate: function() {
        var firstName, regex;

        regex = /^[A-ZÄÁÀËÉÈÍÌÖÓÒÚÙÑÇa-zäáàëéèíìöóòúùñç][A-ZÄÁÀËÉÈÍÌÖÓÒÚÙÑÇa-zäáàëéèíìöóòúùñç ]{1,70}[A-ZÄÁÀËÉÈÍÌÖÓÒÚÙÑÇa-zäáàëéèíìöóòúùñç]$/;
        firstName = this.get('content').get('firstName');
        firstNameOk = regex.exec(firstName);

        if (firstNameOk) {
            return true;
        } else {
            return false;
        }
    },

    transitionAfterSave: function() {
        if (this.get('content.id')) {
            this.transitionToRoute('people.index');
        }
    }.observes('content.id'),

    cancel: function() {
        this.stopEditing();
        this.transitionToRoute('people.index');
    },

    addFramework: function() {
        this.get('content.frameworks').createRecord();
    },

    removeFramework: function(framework) {
        framework.deleteRecord();
    }
});

E no template temos:


<script type="text/x-handlebars" data-template-name="people/new">
<form {{action save on="submit"}}>

<div>
    {{view Ember.TextField placeholder="nome" valueBinding="firstName"}}
    {{nameValidation firstName}}
</div>
<div>
    {{ view Ember.TextField placeholder="sobrenome" valueBinding="lastName"}}
</div>
<div>
    {{ view Ember.TextField placeholder="twitter" valueBinding="twitter"}}
</div>

{{#each frameworks}}
    <div>
        {{view Ember.TextField placeholder="framework" valueBinding="name"}}
        <button {{action removeFramework this}}>-</button>
    </div>
{{/each}}

<div><button {{action addFramework}}>Adicionar framework</button></div>
<div>
    {{submitButton "Salvar"}}
    <button {{action "cancel"}}>Cancelar</button>
</div>
</form>
</script>

Views


A responsabilidade da view é traduzir os eventos primitivos do navegador, e.g. click, key press, em eventos que tenham algum significado para a sua aplicação.

As views são utilizadas quando há necessidade de componentes reutilizáveis, como inputs, e.g. Ember.TextField, ou quando há eventos complexos.

No nosso aplicativo, não precisamos alterar comportamento de view alguma. Apesar disso, as seguintes views foram geradas automaticamente:

App.ApplicationView = Ember.View.extend();
App.PeopleView = Ember.View.extend();
App.PeopleIndexView = Ember.View.extend();
App.PeopleNewView = Ember.View.extend();
App.PersonView = Ember.View.extend();

Juntando tudo


Todo o código pode ser baixado aqui e o app pode ser visto aqui.

Coisas importantes que não foram vistas aqui


Existem várias áreas extremamente importantes para o desenvolvimento de um aplicativo web, como:

  1. depuração de erros;
  2. testes;
  3. organização de arquivos;
  4. implementação em servidores.

O objetivo desse post é somente fazer com que você coloque a mão na massa e conheça esse excelente framework javascript. Em futuros posts cobrirei outros tópicos.

Dicas:

  1. Utilize a versão não "minificada" do Ember durante o desenvolvimento.
  2. Mantenha o Chrome Developer Tools sempre aberto. Caso não saiba como utilizá-lo, corra atrás. É uma ferramenta indispensável para a criação de aplicações javascript.

comments powered by Disqus