quarta-feira, 6 de abril de 2016

MEAN Stack Tutorial

Introdução


O objetivo deste tutorial é mostrar o desenvolvimento de uma aplicação utilizando o MEAN, acrônimo para MongoDB, Express, AngularJS e Node.js, ou seja, uma aplicação utilizando todo esse conjunto de tecnologias. O código da aplicação esta disponível aqui.

Uma das grandes vantagens do MEAN é a possibilidade de se trabalhar com uma única linguagem de programação de ponta a ponta, do back ao front-end, neste caso, a linguagem Javascript, possibilitando àqueles que conhecem Javascript, maior facilidade para se tornar um desenvolvedor Full Stack, com o apoio de uma grande comunidade de desenvolvedores.

MongoDB é um banco de dados NoSQL, ou seja, não utiliza o modelo relacional tradicional dos bancos de dados mais conhecidos. Ao invés disso, ele trabalha com o conceito de documentos, que podem ser pensados como tabelas no modelo tradicional e estes documentos são armazenados no formato JSON (Javascript Object Notation), ou seja, no MEAN, até o banco de dados trabalha com Javascript.

Express é um framework para desenvolvimento de aplicações web com Node.js.

AngularJS é um framework muito poderoso para criação de SPAs (Single Page Applications), através de suas diretivas, que estendem o HTML, arquiteturas MVC (Model View Controller) e MVVM (Model View View Model),   two way data binding, entre outras caracteristicas.

Node.js é um ambiente de execução javascript, que executa do lado do servidor através da V8 Engine do Chrome. Utiliza um modelo dirigido a eventos e não bloqueante de I/O. Além disso, o Node.js possui um sistema de gerenciamento de pacotes de bibliotecas open source, o NPM (Node Package Manager) que é um dos maiores do mundo.

A aplicação que vamos desenvolver neste tutorial é um CRUD simples de séries de TV. Teremos a capacidade de ver uma lista das séries adicionadas, incluir uma nova série, apagar ou editar uma série existente e, ao clicar em uma das séries presentes na lista, veremos um detalhamento da mesma, onde haverá um trailer embutido através de um iframe.

Lista de séries

Detalhe série

Nova série



Atualiza série
Apaga série

Desenvolvimento


Back-end

Vamos começar o desenvolvimento pelo back-end (nossa API RESTful)

O primeiro passo é instalar o Node.js
https://nodejs.org/en/

Depois o MongoDB:
https://www.mongodb.org/downloads#production

Depois de instalado, precisamos rodar o processo mongod, que é responsável por manipular as requisições de dados que nossa aplicação irá fazer.
Abra um terminal e digite: sudo mongod

Agora vamos instalar o express-generator que será responsável por gerar a estrutura da nossa aplicação.
npm install -g express-generator

Para criar o projeto digite:
express series-app
cd series-app

Este comando irá criar a seguinte estrutura de pastas:
        series-app
app.js
bin/
package.json
public/
routes/
views/

app.js - É a "porta de entrada" da nossa aplicação, onde importamos os diversos módulos utilizados pela aplicação, configuramos rotas, abrimos a conexão com o banco, etc.

bin - Este diretório contem scripts úteis para nossa aplicação, por padrão ele já vem com o script www, que é por onde nosso servidor é iniciado através da importação do arquivo app.js.

package.json - Este arquivo contem informações sobre o projeto, bem como as informações de dependências (módulos) utilizados.

public - Neste diretório ficam os recursos estáticos que o servidor disponibiliza para os clientes, Javascript, CSS, imagens, templates, etc.

routes - Neste diretório é onde reside o arquivo index.js, responsável por disponibilizar as rotas da nossa API, o controller do servivor.

views - Como o nome diz, aqui é onde vamos colocar nossas views, como por exemplo, serie.html para mostrar o detalhe de uma série, adiciona.html, onde vamos incluir uma série, etc.

Hora de instalar as dependências presentes do arquivo package.json, para isto digite:
npm install
Isto irá criar uma pasta chamada node_modules onde ficarão todos os módulos baixados.

Precisamos adicionar alguns módulos adicionais, são eles o mongoose e o ejs.
npm install mongoose --save
npm install ejs --save
O flag --save no final do comando indica que a dependência instalada deverá ser salva no arquivo package.json

Mongoose é uma biblioteca utilizada em aplicações Node.js para mapear objetos para o MongoDB utilizando o conceito de modelagem através de Schemas, tornando muito mais fácil a interação com o banco.

Vamos abrir o arquivo app.js e fazer as seguintes modificações:

Importar o mongoose:
var mongoose = require('mongoose');

Abaixo da linha:
app.set('views', path.join(__dirname, 'views'));
Incluir:
app.set('view engine', 'html');
app.engine('html', require('ejs').renderFile);
Onde definimos a view engine a ser utilizada, neste caso html.

No final do arquivo, antes de: module.exports = app;
Incluir:
mongoose.connect('mongodb://localhost/series'); //(conectamos ao banco)

Precisamos também, mover a pasta views, para dentro da pasta public.

Agora, precisamos criar o nosso model Serie, que será mapeado para o MongoDB através do mongoose.
Crie a pasta models:
mkdir models

Dentro da pasta models crie o arquivo Series.js, que terá o seguinte conteúdo:

var mongoose = require('mongoose');

var SerieSchema = mongoose.Schema({
titulo: String,
genero: String,
trailerURL: String
});

module.exports = mongoose.model('Serie', SerieSchema);

Nós definimos um Schema para nosso modelo Serie que tem os atributos titulo, genero e trailerURL, todos do tipo String.
Atribuímos o model criado a module.exports para que possamos reutilizá-lo em outros arquivos através do método require.
No arquivo app.js inclua o seguinte código:
require('./models/Series');

É hora de criar os endpoints da nossa API, nossas rotas, o controller responsável por receber requisições HTTP e enviar as respostas para a aplicação cliente no formato JSON.

Abra o arquivo index.js na pasta routes, o mesmo deve conter o seguinte código:

var express         = require('express');
var router = express.Router();
var mongoose         = require('mongoose');
var Serie         = mongoose.model('Serie');

/* GET home page. */
router.get('/', function(req, res, next) {
  res.render('index', { title: 'Express' });
});

router.get('/series', function(req, res, next) {
Serie.find(function(err, series) {
if(err)
return next(err);

res.json(series);
});
});

router.post('/series', function(req, res, next) {
var serie = new Serie(req.body);

if(serie.trailerURL && serie.trailerURL != '')
serie.trailerURL = serie.trailerURL.replace('watch?v=', 'embed/');

serie.save(function(err, serie) {
if(err)
return next(err);

res.json(serie);
});
});

router.get('/series/:serie_id', function(req, res, next) {

var query = Serie.findById(req.params.serie_id);

query.exec(function(err, serie) {
if(err)
next(err);
if(!serie)
return next(new Error("Não foi possível encontrar a série"));

res.json(serie);
});
});

router.delete('/series/:serie_id', function(req, res, next) {

var query = Serie.findById(req.params.serie_id);

query.remove(function(err, serie) {
if(err)
return next(err);
res.json({mensagem: 'deletado com sucesso'});
});
});

router.put('/series/:serie_id', function(req, res, next) {

var query = Serie.findById(req.params.serie_id);

query.exec(function(err, serie) {
if(err)
return next(err);
if(!serie)
return next(new Error("Não foi possível encontrar a série"));
serie.titulo = req.body.titulo;
serie.genero = req.body.genero;
serie.trailerURL = req.body.trailerURL.replace('watch?v=', 'embed/');
serie.save();
res.json({mensagem: 'atualizado com sucesso'});
});
});

module.exports = router;

Temos duas rotas '/series', uma para receber requisições GET, que retorna todas as séries do banco de dados em formato JSON e outra para requisições POST, para salvar uma série no banco de dados.

O método find de Model retorna um conjunto de documentos através do callback recebido como parâmetro.

O método res.json(param) envia uma resposta no formato JSON do objeto passado como parâmetro.

O método save de Model salva o documento no banco de dados.

A rota '/series/:serie_id' aparece em três momentos manipulando as seguintes requisições HTTP:

GET:  retorna uma série com determinado id (parâmetro :serie_id)

O método findById de Model procura por um documento único baseado em seu id e retorna uma query.
O método exec de Query executa a query e retorna os resultados através do callback passado como parâmetro.

DELETE: deleta uma série com determinado id (parâmetro :serie_id)

O método remove de Query remove um documento do banco de dados.

PUT: atualiza uma série com determinado id (parâmetro :serie_id)

Front-end

É hora de desenvolvermos nosso front-end, o cliente que irá consumir a API que acabamos de criar do lado servidor.
Vamos utilizar para isso o AngularJS e Bootstrap para o CSS.

Na pasta public adicione o arquivo index.html com o seguinte conteúdo:

<!DOCTYPE html>
<html>
<head>
<title>Séries MEAN App</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet">
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.3.10/angular.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.10/angular-ui-router.js"></script>
<script src="/javascripts/app.js"></script>
</head>
<body ng-app="seriesApp">
<div class="container">
<div class="col-md-8 col-md-offset-2">
<div class="page-header">
<h1>Séries MEAN App</h1>
</div>
<ui-view></ui-view>
</div>
</div>
</body>
</html>


A diretiva ng-app presente na tag body indica que este será o contexto da nossa aplicação AngularJS.
A diretiva <ui-view> indica onde serão renderizados os nossos templates (views)

Na pasta javascripts, inclua um arquivo com nome de app.js.
Adicione a linha:
var app = angular.module('seriesApp', ['ui.router']);
Criamos um módulo de nome seriesApp injetando o módulo ui.router como dependência.

Agora vamos adicionar nosso service "series" que tem como dependência o service $http para fazer requisições HTTP para nossa API:

app.factory('series', ['$http', function($http) {

var service = {
series: []
};

service.getSeries = function() {
return $http.get('/series').success(function(data) {
angular.copy(data, service.series);
});
};

service.getSerie = function(id) {
return $http.get('/series/' + id).then(function(res) {
return res.data;
});
};

service.adicionaSerie = function(serie) {
return $http.post('/series', serie).success(function(data) {
service.series.push(data);
});
};

service.apagaSerie = function(id) {
return $http.delete('/series/'+id);
};

service.atualizaSerie = function(serie) {
return $http.put('/series/'+serie._id, serie);
};

return service;

}]);

Adicione o controller MainCtrl:

app.controller('MainCtrl', ['$scope', '$location', 'series', function($scope, $location, series) {

$scope.series = series.series;

$scope.adicionaSerie = function() {
if(!$scope.titulo || $scope.titulo === '')
return;

series.adicionaSerie({
titulo: $scope.titulo,
genero: $scope.genero,
trailerURL: $scope.trailerURL
});

$location.path('/home');

};


}]);

Este controller recebe como dependências $scope, $location e o service recém criado, series.
Ele é responsável por carregar a lista de séries do banco de dados através do service series colocando-as no $scope para que esta lista seja exibida na nossa view, e também, adicionar uma nova série através do método adicionaSerie.

Vamos agora adicionar o controller SeriesCtrl:

app.controller('SeriesCtrl', ['$scope', '$location', 'series', 'serie', function($scope, $location, series, serie) {

$scope.serie = serie;

$scope.apagaSerie = function() {
series.apagaSerie($scope.serie._id).success(function(data) {
$location.path('/home');
});
};

$scope.atualizaSerie = function() {
series.atualizaSerie($scope.serie).success(function(data) {
console.log(data.message);
$location.path('/home');
});
};

}]);

Este controller é responsável por apagar/atualizar uma série baseado na série carregada em $scope injetada como dependência.

Criaremos também uma diretiva para exibição de um vídeo do youtube (o trailer da série) embutido em um iframe:

app.directive('youtube', function() {
return {
restrict: 'E',
scope: {
src: '='
},
templateUrl: 'views/youtube.html'
}
});

Adicione o arquivo youtube.html na pasta views com o seguinte conteúdo:

<div class="embed-responsive embed-responsive-16by9">
  <iframe class="embed-responsive-item" src="{{ src | trusted }}"></iframe>
</div>

Note que no atributo src é utilizado o filtro 'trusted'

app.filter('trusted', function ($sce) {
  return function(url) {
    return $sce.trustAsResourceUrl(url);
  };
});

Vamos agora criar os estados da nossa aplicação utilizando $stateProvider de ui-router:

app.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) {
$stateProvider.state('home', {
url: '/home',
templateUrl: 'views/home.html',
controller: 'MainCtrl',
resolve: {
seriePromisse: ['series', function(series) {
return series.getSeries();
}]
}
}).state('series', {
url: '/series/{id}',
templateUrl: 'views/serie.html',
controller: 'SeriesCtrl',
resolve: {
serie: ['$stateParams', 'series', function($stateParams, series) {
return series.getSerie($stateParams.id);
}]
}
}).state('adiciona', {
url: '/adiciona',
templateUrl: 'views/adiciona.html',
controller: 'MainCtrl'
}).state('delete', {
url: '/delete/{id}',
templateUrl: 'views/apaga.html',
controller: 'SeriesCtrl',
resolve: {
serie: ['$stateParams', 'series', function($stateParams, series) {
return series.getSerie($stateParams.id);
}]
}
}).state('update', {
url: '/update/{id}',
templateUrl: 'views/atualiza.html',
controller: 'SeriesCtrl',
resolve: {
serie: ['$stateParams', 'series', function($stateParams, series) {
return series.getSerie($stateParams.id);
}]
}
});

$urlRouterProvider.otherwise('home');

}]);

Em seguida, criaremos nossas views. Na pasta views, adicione os seguintes arquivos:

adiciona.html

<form ng-submit="adicionaSerie()" style="margin-top:30px;">
<h3>Adiciona nova série</h3>
<div class="form-group">
<input type="text" class="form-control" placeholder="Titulo" ng-model="titulo" required />
</div>
<div class="form-group">
<input type="text" class="form-control" placeholder="Gênero" ng-model="genero" />
</div>
<div class="form-group">
<input type="text" class="form-control" placeholder="URL Trailer" ng-model="trailerURL" />
</div>
<button type="submit" class="btn btn-primary">Adiciona</button>
<a href="#/home">
<button class=" btn btn-warning">Cancela</button>
</a>
</form>

apaga.html

<form role="form">
  <p>Tem certeza que deseja apagar esta série: {{ serie.titulo }} ?</p>
  <div class="form-group">
    <input type="submit" class="btn btn-danger" ng-click="apagaSerie()" value="Sim"/>
    <a href="#/home">
    <button type="button" class="btn btn-default">Não</button>
    </a>
  </div>
</form>

atualiza.html

<form ng-submit="atualizaSerie()" style="margin-top:30px;">
<h3>Atualiza série</h3>
<div class="form-group">
<input type="text" class="form-control" name="titulo" ng-model="serie.titulo" />
</div>
<div class="form-group">
<input type="text" class="form-control" name="genero" ng-model="serie.genero" />
</div>
<div class="form-group">
<input type="text" class="form-control" name="trailerURL" ng-model="serie.trailerURL" />
</div>
<button type="submit" class="btn btn-primary">Atualiza</button>
<a href="#/home">
<button class=" btn btn-warning">Cancela</button>
</a>
</form>

home.html

<table class="table">
<tbody>
<tr ng-repeat="serie in series">
<td>
<a href="#/series/{{serie._id}}">
<span style="font-size:25px;">{{serie.titulo}}</span>
</a>
</td>
<td>
<a class="btn btn-danger" href="#/delete/{{serie._id}}">  
 Apagar
</a>
</td>
<td>
<a class="btn btn-warning" href="#/update/{{serie._id}}">
 Atualizar
</a>
</td>
</tr>
</tbody>
</table>

<div style="margin-top:10px;">
<a href="#/adiciona">
<button class="btn btn-primary">Adicionar Série</button>
</a>
</div>

serie.html

<div>
<div class="row">
<div class="col-md-10">Título: {{serie.titulo}}</div>
<div class="col-md-2">
<a href="#/home">
<button class=" btn btn-primary">Séries</button>
</a>
</div>
</div>
<div class="row">
<div class="col-md-12">Gênero: {{serie.genero}}</div>
</div>
<youtube ng-show="serie.trailerURL && serie.trailerURL != ''" src="serie.trailerURL"></youtube>
<div style="margin-top:15px;">
</div>
</div>

Abra o terminal, vá até o diretório series-app e digite: npm start.
No navegador digite: http://localhost:3000.
Nossa aplicação está pronta para ser utilizada.

Espero que este post tenha sido útil.
Abraço.

Referências


https://docs.angularjs.org
http://expressjs.com/
http://mongoosejs.com/
https://nodejs.org
https://thinkster.io/mean-stack-tutorial#introduction
https://www.youtube.com/watch?v=kHV7gOHvNdk
https://www.youtube.com/watch?v=OhPFgqHz68o&nohtml5=False




4 comentários: