Em agosto, eu havia participado do Midwest JS localizado em Minneapolis, Minnesota. Como você deve saber, sou um grande fã do desenvolvimento de aplicativos de pilha completa com a pilha JavaScript. Foi exatamente sobre isso que me apresentei na conferência.
Minha sessão teve uma boa participação e muitos desenvolvedores aprenderam a usar o Node.js com o Couchbase para desenvolver uma API RESTful e o Angular como a camada voltada para o cliente.

Conforme prometido, vou revisitar o material que abordei durante a apresentação para que os conceitos e o código possam ser reproduzidos e ampliados.
Daqui para frente, a suposição é que você tem Servidor Couchbase, Node.jse o Estrutura iônica CLI instalado e configurado. O Couchbase será o banco de dados NoSQL, o Node.js alimentará nosso back-end e a estrutura Ionic nos fornecerá um front-end da Web alimentado pelo Angular.
O projeto criado na Midwest JS permitia armazenar informações sobre consoles de videogame e jogos de videogame para vários consoles. Isso demonstrou o uso de CRUD, bem como as relações entre documentos NoSQL e como o Couchbase facilita isso.
Criação do back-end Node.js com Couchbase NoSQL
Antes de iniciarmos o desenvolvimento, precisamos criar um novo projeto Node.js. Na linha de comando, execute o seguinte:
|
1 2 |
npm init -y npm install couchbase express body-parser uuid cors --save |
O comando acima criará um projeto package.json e instalar as dependências do nosso projeto. O arquivo cors nos permitirá usar o Node e o Angular localmente em duas portas diferentes sem receber erros de compartilhamento de recursos entre origens. O pacote uuid nos permitirá gerar cadeias de caracteres exclusivas para uso como chaves de documentos. O pacote analisador de corpo nos permitirá enviar dados JSON em solicitações HTTP. Usaremos o Express e o Couchbase, o que explica os outros dois pacotes.
Criar um app.js em seu projeto. Ele conterá todo o código-fonte do nosso aplicativo Node.js. Como padrão, ele deve se parecer com o seguinte:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
var Couchbase = require("couchbase"); var Express = require("express"); var BodyParser = require("body-parser"); var UUID = require("uuid"); var Cors = require("cors"); var app = Express(); var N1qlQuery = Couchbase.N1qlQuery; app.use(BodyParser.json()); app.use(BodyParser.urlencoded({ extended: true })); app.use(Cors()); var cluster = new Couchbase.Cluster("couchbase://localhost"); var bucket = cluster.openBucket("default", ""); app.get("/consoles", (request, response) => {}); app.post("/console", (request, response) => {}); app.post("/game", (request, response) => {}); app.get("/games", (request, response) => {}); app.get("/game/:id", (request, response) => {}); var server = app.listen(3000, () => { console.log("Listening on port " + server.address().port + "..."); }); |
Observe que importamos cada uma das dependências baixadas, inicializamos e configuramos o Express e nos conectamos a um Bucket em nosso cluster do Couchbase.
Teremos cinco endpoints de API RESTful diferentes para esse aplicativo.
A primeira coisa lógica a fazer seria criar um console de videogame para podermos adicionar jogos a ele. Dê uma olhada no código do endpoint a seguir:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
app.post("/console", (request, response) => { if(!request.body.title) { return response.status(401).send({ "message": "A `title` is required." }); } var id = UUID.v4(); request.body.type = "console"; bucket.insert(id, request.body, (error, result) => { if(error) { return response.status(500).send(error); } response.send(result); }); }); |
Na lógica acima, estamos validando que a título existe em nossa solicitação. Se existir, geraremos um novo ID, atribuiremos um tipo para os dados em nossa solicitação e os insere no Couchbase. A resposta de sucesso ou falha da inserção será retornada ao cliente, que eventualmente será um aplicativo Angular.
Para consultar os consoles de videogame, precisaremos consultar com base no tipo propriedade. Por esse motivo, teremos que usar uma consulta N1QL em vez de uma pesquisa por id.
|
1 2 3 4 5 6 7 8 9 10 11 |
app.get("/consoles", (request, response) => { var statement = "SELECT `" + bucket._name + "`.*, META().id FROM `" + bucket._name + "` WHERE type = 'console'"; var query = N1qlQuery.fromString(statement); query.consistency(N1qlQuery.Consistency.REQUEST_PLUS); bucket.query(query, (error, result) => { if(error) { return response.status(500).send(error); } response.send(result); }); }); |
A consulta N1QL nada mais é do que um simples SELECIONAR que você encontraria no SQL. Depois de executar a consulta, retornamos a resposta para o cliente.
Isso nos leva aos videogames reais. As coisas ficam um pouco mais complexas, mas não mais difíceis.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
app.post("/game", (request, response) => { if(!request.body.title) { return response.status(401).send({ "message": "A `title` is required." }); } else if(!request.body.cid) { return response.status(401).send({ "message": "A `cid` is required." }); } var id = UUID.v4(); request.body.type = "game"; bucket.insert(id, request.body, (error, result) => { if(error) { return response.status(500).send(error); } response.send(result); }); }); |
Na lógica do endpoint acima, planejamos inserir um novo videogame no banco de dados. Isso não é diferente do que vimos ao inserir um novo console de videogame no banco de dados. Estamos definindo um tipo mas também estamos nos certificando de que uma propriedade cid existe. O cid será um ID de console que nos permitirá estabelecer uma relação com nossos dados.
Quando você tem relacionamentos, você tem JUNTAR operações.
|
1 2 3 4 5 6 7 8 9 10 11 |
app.get("/games", (request, response) => { var statement = "SELECT game.title AS game_title, console.title AS console_title FROM `" + bucket._name + "` AS game JOIN `" + bucket._name + "` AS console ON KEYS game.cid WHERE game.type = 'game'"; var query = N1qlQuery.fromString(statement); query.consistency(N1qlQuery.Consistency.REQUEST_PLUS); bucket.query(query, (error, result) => { if(error) { return response.status(500).send(error); } response.send(result); }); }); |
No endpoint acima, estamos fazendo outra consulta N1QL, mas desta vez temos um JUNTAR operação. Não é útil retornar um cid ao consultar jogos de vídeo, portanto JUNTAR e substitua essas informações pelo título do console do outro documento.
Da mesma forma, temos uma consulta semelhante quando tentamos encontrar um videogame específico:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
app.get("/game/:id", (request, response) => { if(!request.params.id) { return response.status(401).send({ "message": "An `id` is required." }); } var statement = "SELECT game.title AS game_title, console.title AS console_title FROM `" + bucket._name + "` AS game JOIN `" + bucket._name + "` AS console ON KEYS game.cid WHERE game.type = 'game' AND META(game).id = $id"; var query = N1qlQuery.fromString(statement); bucket.query(query, { "id": request.params.id }, (error, result) => { if(error) { return response.status(500).send(error); } response.send(result); }); }); |
A alternativa ao uso do N1QL e do JUNTAR seria fazer duas pesquisas com base no ID. Não há nada de errado com essa prática, mas, na minha opinião, é mais fácil deixar que o banco de dados cuide de um JUNTAR em vez de tentar JUNTAR por meio da camada de aplicativos.
Isso nos leva ao front-end do cliente.
Criação da estrutura Ionic com front-end angular
Como mencionado anteriormente, desta vez estamos usando o Ionic Framework, que usa uma variante do Angular. Escolhi esse framework porque estava com preguiça de criar um frontend atraente com o Bootstrap ou o Foundation.
Com a CLI do Ionic Framework disponível, execute o seguinte:
|
1 |
ionic start pwa sidemenu |
O comando acima criará um projeto chamado pwa usando a estrutura Ionic sidemenu modelo.
O modelo básico é útil, mas não tem tudo o que precisamos. Precisamos adicionar algumas páginas ao aplicativo.
Usando os geradores do Ionic Framework, ou manualmente, crie um consoles, jogose jogo página. Cada uma dessas páginas deve ter um arquivo HTML, SCSS e TypeScript e cada diretório de página deve estar no diretório páginas diretório.
Abra o arquivo app/app.component.ts e faça com que ele se pareça com o seguinte:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
import { Component, ViewChild } from '@angular/core'; import { Nav, Platform } from 'ionic-angular'; import { StatusBar } from '@ionic-native/status-bar'; import { SplashScreen } from '@ionic-native/splash-screen'; import { GamesPage } from '../pages/games/games'; import { ConsolesPage } from '../pages/consoles/consoles'; @Component({ templateUrl: 'app.html' }) export class MyApp { @ViewChild(Nav) nav: Nav; rootPage: any = GamesPage; pages: Array<{title: string, component: any}>; constructor(public platform: Platform, public statusBar: StatusBar, public splashScreen: SplashScreen) { this.initializeApp(); // used for an example of ngFor and navigation this.pages = [ { title: 'Games', component: GamesPage }, { title: 'Consoles', component: ConsolesPage } ]; } initializeApp() { this.platform.ready().then(() => { // Okay, so the platform is ready and our plugins are available. // Here you can do any higher level native things you might need. this.statusBar.styleDefault(); this.splashScreen.hide(); }); } openPage(page) { // Reset the content nav to have just this page // we wouldn't want the back button to show in this scenario this.nav.setRoot(page.component); } } |
Observe que importamos Página de jogos e ConsolesPágina, atualizou o páginas e definir a página raiz padrão como Página de jogos. Ao fazer isso, configuramos a navegação e a página padrão quando o aplicativo é iniciado.
Para concluir a configuração, também precisamos alterar o arquivo app/app.module.ts arquivo. Faça com que ele se pareça com o seguinte:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
import { BrowserModule } from '@angular/platform-browser'; import { ErrorHandler, NgModule } from '@angular/core'; import { IonicApp, IonicErrorHandler, IonicModule } from 'ionic-angular'; import { HttpModule } from "@angular/http"; import { MyApp } from './app.component'; import { GamesPage } from '../pages/games/games'; import { GamePage } from '../pages/game/game'; import { ConsolesPage } from '../pages/consoles/consoles'; import { StatusBar } from '@ionic-native/status-bar'; import { SplashScreen } from '@ionic-native/splash-screen'; @NgModule({ declarations: [ MyApp, GamesPage, GamePage, ConsolesPage ], imports: [ BrowserModule, HttpModule, IonicModule.forRoot(MyApp), ], bootstrap: [IonicApp], entryComponents: [ MyApp, GamesPage, GamePage, ConsolesPage ], providers: [ StatusBar, SplashScreen, {provide: ErrorHandler, useClass: IonicErrorHandler} ] }) export class AppModule {} |
Observe que importamos cada uma de nossas novas páginas e as adicionamos à pasta declarações e entryComponents matrizes do @NgModule bloco.
Agora podemos nos concentrar no desenvolvimento do aplicativo e conectá-lo à nossa API.
Abra o arquivo src/pages/games/games.ts e fazer com que ele se pareça com o seguinte. Vamos detalhar o que está acontecendo a seguir.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
import { Component } from '@angular/core'; import { NavController, ModalController } from 'ionic-angular'; import { Http, Headers, RequestOptions } from "@angular/http"; import "rxjs/Rx"; import { GamePage } from "../game/game"; @Component({ selector: 'page-games', templateUrl: 'games.html' }) export class GamesPage { public games: Array<any>; public constructor(public navCtrl: NavController, private http: Http, private modalCtrl: ModalController) { this.games = []; } public ionViewDidEnter() { this.http.get("https://localhost:3000/games") .map(result => result.json()) .subscribe(result => { this.games = result; }); } public create() { let gameModal = this.modalCtrl.create(GamePage); gameModal.onDidDismiss(data => { let headers = new Headers({ "Content-Type": "application/json" }); let options = new RequestOptions({ headers: headers }); this.http.post("https://localhost:3000/game", JSON.stringify(data), options) .subscribe(result => { this.games.push({ "game_title": data.title, "console_title": ""}); }); }); gameModal.present(); } } |
Dentro do Página de jogos temos uma variável pública chamada jogos. Por ser público, ele será acessível via HTML. Ele conterá todos os jogos retornados do aplicativo Node.js.
Quando a página é carregada, queremos consultar nosso endpoint. Nunca é uma boa ideia fazer isso na seção construtor portanto, em vez disso, usamos o método ionViewDidEnter método. Depois de emitir a solicitação, o resultado é transformado em JSON e, em seguida, carregado em nossa variável pública.
Se quisermos criar um novo jogo em nosso banco de dados, as coisas serão um pouco diferentes. Vamos exibir uma caixa de diálogo modal e coletar informações.
|
1 2 3 4 5 6 7 8 9 10 11 12 |
public create() { let gameModal = this.modalCtrl.create(GamePage); gameModal.onDidDismiss(data => { let headers = new Headers({ "Content-Type": "application/json" }); let options = new RequestOptions({ headers: headers }); this.http.post("https://localhost:3000/game", JSON.stringify(data), options) .subscribe(result => { this.games.push({ "game_title": data.title, "console_title": ""}); }); }); gameModal.present(); } |
O criar exibirá nosso método Página do jogo que estará no formato modal. Todos os dados inseridos no formulário no modal serão retornados ao Página de jogos e enviado por meio de uma solicitação HTTP para a API.
Antes de darmos uma olhada em Página do jogoVamos dar uma olhada no HTML que alimenta o Página de jogos.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<ion-header> <ion-navbar> <button ion-button menuToggle> <ion-icon name="menu"></ion-icon> </button> <ion-title>Games</ion-title> <ion-buttons end> <button ion-button icon-only (click)="create()"> <ion-icon name="add"></ion-icon> </button> </ion-buttons> </ion-navbar> </ion-header> <ion-content padding> <ion-list> <button ion-item *ngFor="let game of games"> {{game.game_title}} <span class="item-note">{{game.console_title}}</span> </button> </ion-list> </ion-content> |
No HTML acima, estamos percorrendo nosso arquivo público jogos matriz. Cada objeto da matriz é renderizado na tela dentro de uma lista. O Angular faz todo o trabalho pesado para nós.
Abra o arquivo src/pages/game/game.ts e inclua o seguinte:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
import { Component } from '@angular/core'; import { IonicPage, NavController, NavParams, ViewController } from 'ionic-angular'; import { Http, Headers, RequestOptions } from "@angular/http"; import "rxjs/Rx"; @IonicPage() @Component({ selector: 'page-game', templateUrl: 'game.html', }) export class GamePage { public consoles: Array<any>; public input: any; constructor(public navCtrl: NavController, public navParams: NavParams, public viewCtrl: ViewController, private http: Http) { this.consoles = []; this.input = { "cid": "", "title": "" } } ionViewDidEnter() { this.http.get("https://localhost:3000/consoles") .map(result => result.json()) .subscribe(result => { for(let i = 0; i < result.length; i++) { this.consoles.push(result[i]); } }); } public save() { this.viewCtrl.dismiss(this.input); } } |
Essa lógica modal é semelhante ao que já vimos. Haverá um formulário vinculado a HTML e TypeScript. Quando o ionViewDidEnter acionadores, consultamos as informações do console. Essas informações do console serão eventualmente usadas para uma lista de opções que o usuário pode selecionar.
Quando o usuário seleciona o salvar os dados vinculados no formulário público são passados para o método Página de jogos página.
O HTML que alimenta esse modal, encontrado em src/pages/game/game.html se parece com isso:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<ion-header> <ion-navbar> <ion-title>New Game</ion-title> </ion-navbar> </ion-header> <ion-content padding> <ion-list> <ion-item> <ion-label stacked>Title</ion-label> <ion-input type="text" [(ngModel)]="input.title"></ion-input> </ion-item> <ion-item> <ion-label>Console</ion-label> <ion-select [(ngModel)]="input.cid"> <ion-option *ngFor="let console of consoles" value="{{ console.id }}">{{ console.title }}</ion-option> </ion-select> </ion-item> <ion-item> <button ion-button full (click)="save()">Save</button> </ion-item> </ion-list> </ion-content> |
Temos uma lista simples que compõe nosso formulário. Os elementos do formulário são vinculados à nossa variável TypeScript e as informações do console são percorridas para preencher um arquivo HTML selecionar elemento.
Isso nos leva à página final do front-end do Angular.
Abra o arquivo src/pages/consoles/consoles.ts e inclua o seguinte:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
import { Component } from '@angular/core'; import { NavController, NavParams, AlertController } from 'ionic-angular'; import { Http, Headers, RequestOptions } from "@angular/http"; import "rxjs/Rx"; @Component({ selector: 'page-consoles', templateUrl: 'consoles.html' }) export class ConsolesPage { public consoles: Array<any>; public constructor(public navCtrl: NavController, private http: Http, private alertCtrl: AlertController) { this.consoles = []; } public ionViewDidEnter() { this.http.get("https://localhost:3000/consoles") .map(result => result.json()) .subscribe(result => { this.consoles = result; }); } public create() { let alert = this.alertCtrl.create({ title: 'Add Console', inputs: [ { name: 'title', placeholder: 'Title' }, { name: 'year', placeholder: 'Year' } ], buttons: [ { text: 'Cancel', role: 'cancel', handler: data => { console.log('Cancel clicked'); } }, { text: 'Save', handler: data => { let headers = new Headers({ "Content-Type": "application/json" }); let options = new RequestOptions({ headers: headers }); this.http.post("https://localhost:3000/console", JSON.stringify(data), options) .subscribe(result => { this.consoles.push(data); }, error => {}); } } ] }); alert.present(); } } |
Embora não seja muito diferente do que já vimos, temos um novo recurso. Estamos usando uma caixa de diálogo pop-up para coletar informações sobre o novo console de videogame.
Quando o pop-up é descartado, o seguinte é executado:
|
1 2 3 4 5 6 7 8 |
handler: data => { let headers = new Headers({ "Content-Type": "application/json" }); let options = new RequestOptions({ headers: headers }); this.http.post("https://localhost:3000/console", JSON.stringify(data), options) .subscribe(result => { this.consoles.push(data); }, error => {}); } |
Isso pegará as informações encontradas no formulário e as enviará via HTTP para a nossa API, que, por sua vez, salvará as informações do console no banco de dados.
Incrível, não é?
O HTML encontrado na pasta src/pages/consoles/consoles.html é parecido com o seguinte:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<ion-header> <ion-navbar> <button ion-button menuToggle> <ion-icon name="menu"></ion-icon> </button> <ion-title>Consoles</ion-title> <ion-buttons end> <button ion-button icon-only (click)="create()"> <ion-icon name="add"></ion-icon> </button> </ion-buttons> </ion-navbar> </ion-header> <ion-content> <ion-list> <button ion-item *ngFor="let console of consoles"> {{ console.title }} <span class="item-note" item-right> {{ console.year }} </span> </button> </ion-list> </ion-content> |
Novamente, ele é quase idêntico aos outros arquivos HTML que vimos.
Conclusão
Você acabou de receber uma recapitulação de tudo o que foi abordado no Midwest JS 2017. Vimos como criar uma API do Node.js que se comunica com o Couchbase, nosso banco de dados NoSQL, além de criar um frontend usando o Angular e o Ionic Framework. Esses são apenas alguns componentes de um aplicativo de pilha completa.
Para obter mais informações sobre usando Node.js com o Couchbase, confira o Portal do desenvolvedor do Couchbase. Se você quiser que eu volte ao Midwest JS, avise-me nos comentários.