En agosto participé en Medio Oeste JS ubicado en Minneapolis, Minnesota. Como ya sabrás, soy un gran fan del desarrollo de aplicaciones completas con JavaScript. Esto es exactamente lo que había presentado en la conferencia.
Mi sesión fue muy concurrida y muchos desarrolladores aprendieron a utilizar Node.js con Couchbase para desarrollar una API RESTful, y Angular como capa de cara al cliente.
Como prometí, voy a revisar el material que repasé durante la presentación para que los conceptos y el código puedan reproducirse y ampliarse.
De cara al futuro, se supone que tienes Servidor Couchbase, Node.jsy el Marco iónico CLI instalado y configurado. Couchbase será la base de datos NoSQL, Node.js impulsará nuestro backend, y Ionic Framework nos dará un frontend web impulsado por Angular.
El proyecto creado en Midwest JS permitía almacenar información sobre videoconsolas y videojuegos para varias consolas. Esto demostró el uso de CRUD así como las relaciones entre documentos NoSQL y cómo Couchbase lo hace fácil.
Creación de Node.js con Couchbase NoSQL Backend
Antes de comenzar el desarrollo, necesitamos crear un nuevo proyecto Node.js. Desde la línea de comandos, ejecuta lo siguiente:
1 2 |
npm init -y npm instale couchbase express cuerpo-analizador uuid cors --guardar |
El comando anterior creará un proyecto paquete.json e instalar las dependencias de nuestro proyecto. El cors
nos permitirá usar Node y Angular localmente en dos puertos diferentes sin obtener errores de compartición de recursos entre orígenes. El uuid
nos permitirá generar cadenas únicas para utilizarlas como claves de documento. La dirección body-parser
nos permitirá enviar datos JSON en peticiones HTTP. Usaremos Express y Couchbase que explican los otros dos paquetes.
Crear un app.js dentro de tu proyecto. Contendrá todo el código fuente de nuestra aplicación Node.js. Como ejemplo, debería tener el siguiente aspecto:
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 = requiere("couchbase"); var Express = requiere("express"); var BodyParser = requiere("body-parser"); var UUID = requiere("uuid"); var Cors = requiere("cors"); var aplicación = Express(); var N1qlQuery = Couchbase.N1qlQuery; aplicación.utilice(BodyParser.json()); aplicación.utilice(BodyParser.urlencoded({ ampliado: verdadero })); aplicación.utilice(Cors()); var grupo = nuevo Couchbase.Grupo("couchbase://localhost"); var cubo = grupo.openBucket("por defecto", ""); aplicación.consiga("/consolas", (solicitar, respuesta) => {}); aplicación.Correo electrónico:("/consola", (solicitar, respuesta) => {}); aplicación.Correo electrónico:("/juego", (solicitar, respuesta) => {}); aplicación.consiga("/juegos", (solicitar, respuesta) => {}); aplicación.consiga("/juego/:id", (solicitar, respuesta) => {}); var servidor = aplicación.escuche(3000, () => { consola.registro("Escuchando en el puerto " + servidor.dirección().puerto + "..."); }); |
Observa que hemos importado cada una de las dependencias descargadas, inicializado y configurado Express, y conectado a un Bucket en nuestro cluster Couchbase.
Tendremos cinco puntos finales de API RESTful diferentes para esta aplicación.
Lo primero lógico sería crear una videoconsola para poder añadirle juegos. Echa un vistazo al siguiente código endpoint:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
aplicación.Correo electrónico:("/consola", (solicitar, respuesta) => { si(!solicitar.cuerpo.título) { devolver respuesta.estado(401).enviar({ "mensaje": "Se requiere un "título"." }); } var id = UUID.v4(); solicitar.cuerpo.tipo = "consola"; cubo.insertar(id, solicitar.cuerpo, (error, resultado) => { si(error) { devolver respuesta.estado(500).enviar(error); } respuesta.enviar(resultado); }); }); |
En la lógica anterior, estamos validando que a título
existe en nuestra solicitud. Si es así, generaremos un nuevo id, asignaremos un tipo
a los datos de nuestra petición, e insertarlos en Couchbase. La respuesta de éxito o fracaso de la inserción será devuelta al cliente, que finalmente será una aplicación Angular.
Para buscar consolas de videojuegos, tendremos que hacer una consulta basada en la variable tipo
propiedad. Por esta razón, tendremos que utilizar una consulta N1QL en lugar de una búsqueda por id.
1 2 3 4 5 6 7 8 9 10 11 |
aplicación.consiga("/consolas", (solicitar, respuesta) => { var declaración = "SELECT `" + cubo.Nombre + "`.*, META().id FROM `" + cubo.Nombre + "WHERE type = 'console'"; var consulta = N1qlQuery.fromString(declaración); consulta.coherencia(N1qlQuery.Coherencia.SOLICITUD_PLUS); cubo.consulta(consulta, (error, resultado) => { si(error) { devolver respuesta.estado(500).enviar(error); } respuesta.enviar(resultado); }); }); |
La consulta N1QL no es más que un simple SELECCIONE
que encontraríamos en SQL. Después de ejecutar la consulta, devolveríamos la respuesta al cliente.
Esto nos lleva a los videojuegos propiamente dichos. Las cosas se ponen un poco más complejas, pero no más difíciles.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
aplicación.Correo electrónico:("/juego", (solicitar, respuesta) => { si(!solicitar.cuerpo.título) { devolver respuesta.estado(401).enviar({ "mensaje": "Se requiere un "título"." }); } si no si(!solicitar.cuerpo.cid) { devolver respuesta.estado(401).enviar({ "mensaje": "Se requiere un `cid`". }); } var id = UUID.v4(); solicitar.cuerpo.tipo = "juego"; cubo.insertar(id, solicitar.cuerpo, (error, resultado) => { si(error) { devolver respuesta.estado(500).enviar(error); } respuesta.enviar(resultado); }); }); |
En la lógica del endpoint anterior, planeamos insertar un nuevo videojuego en la base de datos. Esto no es diferente de lo que vimos al insertar una nueva consola de videojuegos en la base de datos. Estamos definiendo un tipo
pero también nos aseguramos de que cid
existe. En cid
será un id de consola que nos permitirá establecer una relación con nuestros datos.
Cuando tienes relaciones, tienes ÚNASE A
operaciones.
1 2 3 4 5 6 7 8 9 10 11 |
aplicación.consiga("/juegos", (solicitar, respuesta) => { var declaración = "SELECT game.title AS game_title, console.title AS console_title FROM `" + cubo.Nombre + "` AS game JOIN `" + cubo.Nombre + "` AS console ON KEYS game.cid WHERE game.type = 'game'"; var consulta = N1qlQuery.fromString(declaración); consulta.coherencia(N1qlQuery.Coherencia.SOLICITUD_PLUS); cubo.consulta(consulta, (error, resultado) => { si(error) { devolver respuesta.estado(500).enviar(error); } respuesta.enviar(resultado); }); }); |
En el endpoint anterior, estamos haciendo otra consulta N1QL, pero esta vez tenemos una consulta ÚNASE A
operación. No es útil devolver un cid
cuando se buscan videojuegos, así que ÚNASE A
y sustituye esa información por el título de la consola del otro documento.
Lo mismo ocurre cuando buscamos un videojuego concreto:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
aplicación.consiga("/juego/:id", (solicitar, respuesta) => { si(!solicitar.parámetros.id) { devolver respuesta.estado(401).enviar({ "mensaje": "Se requiere un `id`". }); } var declaración = "SELECT game.title AS game_title, console.title AS console_title FROM `" + cubo.Nombre + "` AS game JOIN `" + cubo.Nombre + "` AS console ON KEYS game.cid WHERE game.type = 'game' AND META(game).id = $id"; var consulta = N1qlQuery.fromString(declaración); cubo.consulta(consulta, { "id": solicitar.parámetros.id }, (error, resultado) => { si(error) { devolver respuesta.estado(500).enviar(error); } respuesta.enviar(resultado); }); }); |
La alternativa al uso de N1QL y ÚNASE A
sería hacer dos búsquedas basadas en el id. No hay nada malo en esta práctica, pero en mi opinión es más fácil dejar que la base de datos se encargue de un ÚNASE A
en lugar de intentar ÚNASE A
a través de la capa de aplicación.
Esto nos lleva al frontend del cliente.
Creación de Ionic Framework con Angular Frontend
Como se mencionó anteriormente, esta vez estamos utilizando Ionic Framework que utiliza un sabor de Angular. Elegí esto porque me sentía demasiado perezoso para crear un frontend atractivo con Bootstrap o Foundation.
Con la CLI de Ionic Framework disponible, ejecute lo siguiente:
1 |
iónico iniciar pwa sidemenu |
El comando anterior creará un proyecto llamado pwa utilizando Ionic Framework sidemenu
plantilla.
La plantilla base es útil, pero no tiene todo lo que necesitamos. Tenemos que añadir algunas páginas a la aplicación.
Utilizando los generadores de Ionic Framework, o manualmente, cree un archivo consolas, juegosy juego página. Cada una de estas páginas debe tener un archivo HTML, SCSS y TypeScript y cada directorio de página debe estar en el directorio páginas directorio.
Abra el archivo app/app.component.ts y que tenga el siguiente aspecto:
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 |
importar { Componente, VerNiño } de @angular/core; importar { Navegación, Plataforma } de iónico-angular; importar { Barra de estado } de @ionic-native/status-bar; importar { SplashScreen } de @ionic-native/splash-screen; importar { JuegosPágina } de '../páginas/juegos/juegos'; importar { ConsolasPágina } de '../páginas/consolas/consolas'; @Componente({ templateUrl: app.html }) exportar clase MyApp { @VerNiño(Navegación) nav: Navegación; rootPage: cualquier = JuegosPágina; páginas: Matriz<{título: cadena, componente: cualquier}>; constructor(público plataforma: Plataforma, público statusBar: Barra de estado, público splashScreen: SplashScreen) { este.inicializarApp(); // utilizado para un ejemplo de ngFor y navegación este.páginas = [ { título: Juegos, componente: JuegosPágina }, { título: Consolas, componente: ConsolasPágina } ]; } inicializarApp() { este.plataforma.listo().entonces(() => { // Bien, la plataforma está lista y nuestros plugins están disponibles. // Aquí puedes hacer cualquier cosa nativa de nivel superior que necesites. este.statusBar.styleDefault(); este.splashScreen.ocultar(); }); } abrirPágina(página) { // Restablecer el contenido nav para tener sólo esta página // en este caso no queremos que se muestre el botón de retroceso este.nav.setRoot(página.componente); } } |
Observe que hemos importado JuegosPágina
y ConsolasPágina
actualizó el páginas
y establecer la página raíz por defecto como JuegosPágina
. De este modo habremos configurado la navegación y la página predeterminada cuando se inicie la aplicación.
Para completar la configuración, también tenemos que modificar el proyecto de app/app.module.ts archivo. Que tenga el siguiente aspecto:
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 |
importar { BrowserModule } de @angular/platform-browser; importar { ErrorHandler, NgModule } de @angular/core; importar { IonicApp, IonicErrorHandler, IonicModule } de iónico-angular; importar { HttpModule } de "@angular/http"; importar { MyApp } de './app.component'; importar { JuegosPágina } de '../páginas/juegos/juegos'; importar { Página de juego } de '../páginas/juego/juego'; importar { ConsolasPágina } de '../páginas/consolas/consolas'; importar { Barra de estado } de @ionic-native/status-bar; importar { SplashScreen } de @ionic-native/splash-screen; @NgModule({ declaraciones: [ MyApp, JuegosPágina, Página de juego, ConsolasPágina ], importaciones: [ BrowserModule, HttpModule, IonicModule.forRoot(MyApp), ], arranque: [IonicApp], entryComponents: [ MyApp, JuegosPágina, Página de juego, ConsolasPágina ], proveedores: [ Barra de estado, SplashScreen, {proporcionar: ErrorHandler, useClass: IonicErrorHandler} ] }) exportar clase AppModule {} |
Observe que hemos importado cada una de nuestras nuevas páginas y las hemos añadido a la carpeta declaraciones
y entryComponents
matrices del @NgModule
bloque.
Ahora podemos centrarnos en el desarrollo de la aplicación y en conectarla a nuestra API.
Abra el archivo src/pages/games/games.ts y hacer que se vea como lo siguiente. Vamos a desglosar lo que sucede a continuación.
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 |
importar { Componente } de @angular/core; importar { NavController, ModalController } de iónico-angular; importar { Http, Cabeceras, RequestOptions } de "@angular/http"; importar "rxjs/Rx"; importar { Página de juego } de "../juego/juego"; @Componente({ selector: página-juegos, templateUrl: juegos.html }) exportar clase JuegosPágina { público juegos: Matriz<cualquier>; público constructor(público navCtrl: NavController, privado http: Http, privado modalCtrl: ModalController) { este.juegos = []; } público ionViewDidEnter() { este.http.consiga("http://localhost:3000/games") .mapa(resultado => resultado.json()) .suscríbase a(resultado => { este.juegos = resultado; }); } público crear() { deje gameModal = este.modalCtrl.crear(Página de juego); gameModal.onDidDismiss(datos => { deje cabeceras = nuevo Cabeceras({ "Tipo de contenido": "application/json" }); deje opciones = nuevo RequestOptions({ cabeceras: cabeceras }); este.http.Correo electrónico:("http://localhost:3000/game", JSON.stringify(datos), opciones) .suscríbase a(resultado => { este.juegos.pulse({ "game_title": datos.título, "console_title": ""}); }); }); gameModal.presente(); } } |
Dentro del JuegosPágina
tenemos una variable pública llamada juegos
. Como es público, será accesible a través del HTML. Contendrá todos los juegos devueltos desde la aplicación Node.js.
Cuando se carga la página, queremos consultar nuestro endpoint. Nunca es una buena idea hacer esto en el archivo constructor
por lo que en su lugar utilizaremos el método ionViewDidEnter
método. Después de emitir la solicitud, el resultado se transforma en JSON y luego se carga en nuestra variable pública.
Si queremos crear un nuevo juego en nuestra base de datos, las cosas son un poco diferentes. Vamos a mostrar un cuadro de diálogo modal y recoger la entrada.
1 2 3 4 5 6 7 8 9 10 11 12 |
público crear() { deje gameModal = este.modalCtrl.crear(Página de juego); gameModal.onDidDismiss(datos => { deje cabeceras = nuevo Cabeceras({ "Tipo de contenido": "application/json" }); deje opciones = nuevo RequestOptions({ cabeceras: cabeceras }); este.http.Correo electrónico:("http://localhost:3000/game", JSON.stringify(datos), opciones) .suscríbase a(resultado => { este.juegos.pulse({ "game_title": datos.título, "console_title": ""}); }); }); gameModal.presente(); } |
En crear
mostrará nuestro método Página de juego
que estará en formato modal. Cualquier dato introducido en el formulario en el modal será devuelto al JuegosPágina
y se envía mediante una solicitud HTTP a la API.
Antes de echar un vistazo a Página de juego
veamos el HTML que potencia JuegosPágina
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<ion-cabecera> <ion-barra de navegación> <botón ion-botón menuToggle> <ion-icono nombre="menú"></ion-icono> </botón> <ion-título>Juegos</ion-título> <ion-botones fin> <botón ion-botón icono-sólo (haga clic en)="create()"> <ion-icono nombre="añadir"></ion-icono> </botón> </ion-botones> </ion-barra de navegación> </ion-cabecera> <ion-contenido acolchado> <ion-lista> <botón ion-artículo *ngFor="el juego de los juegos"> {{juego.título_juego}} <span clase="artículo-nota">{{juego.título_consola}}</span> </botón> </ion-lista> </ion-contenido> |
En el HTML anterior, hacemos un bucle a través de nuestro código público juegos
array. Cada objeto de la matriz se muestra en la pantalla dentro de una lista. Angular hace todo el trabajo pesado por nosotros.
Abra el archivo src/pages/game/game.ts e incluya lo siguiente:
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 |
importar { Componente } de @angular/core; importar { IonicPage, NavController, NavParams, ViewController } de iónico-angular; importar { Http, Cabeceras, RequestOptions } de "@angular/http"; importar "rxjs/Rx"; @IonicPage() @Componente({ selector: página-juego, templateUrl: juego.html, }) exportar clase Página de juego { público consolas: Matriz<cualquier>; público entrada: cualquier; constructor(público navCtrl: NavController, público navParams: NavParams, público viewCtrl: ViewController, privado http: Http) { este.consolas = []; este.entrada = { "cid": "", "título: "" } } ionViewDidEnter() { este.http.consiga("http://localhost:3000/consoles") .mapa(resultado => resultado.json()) .suscríbase a(resultado => { para(deje i = 0; i < resultado.longitud; i++) { este.consolas.pulse(resultado[i]); } }); } público guardar() { este.viewCtrl.desestimar(este.entrada); } } |
Esta lógica modal es similar a lo que ya hemos visto. Habrá un formulario vinculado a HTML y TypeScript. Cuando el ionViewDidEnter
se consulta la información de la consola. Esta información de la consola se utilizará finalmente para una lista de radio que el usuario puede seleccionar.
Cuando el usuario selecciona el guardar
los datos vinculados en el formulario público se pasan al método anterior JuegosPágina
página.
El HTML que alimenta este modal, que se encuentra en src/pages/game/game.html se ve así:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<ion-cabecera> <ion-barra de navegación> <ion-título>Nuevo Juego</ion-título> </ion-barra de navegación> </ion-cabecera> <ion-contenido acolchado> <ion-lista> <ion-artículo> <ion-etiqueta apilado>Título</ion-etiqueta> <ion-entrada tipo="texto" [(ngModel)]="entrada.titulo"></ion-entrada> </ion-artículo> <ion-artículo> <ion-etiqueta>Consola</ion-etiqueta> <ion-seleccione [(ngModel)]="entrada.cid"> <ion-opción *ngFor="la consola de las consolas" valor="{{ console.id }}">{{ consola.título }}</ion-opción> </ion-seleccione> </ion-artículo> <ion-artículo> <botón ion-botón completo (haga clic en)="save()">Guardar</botón> </ion-artículo> </ion-lista> </ion-contenido> |
Tenemos una lista simple que compone nuestro formulario. Los elementos del formulario están ligados a nuestra variable TypeScript y la información de la consola se repite para rellenar un HTML seleccione
elemento.
Esto nos lleva a la página final del frontend Angular.
Abra el archivo src/pages/consolas/consolas.ts e incluya lo siguiente:
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 |
importar { Componente } de @angular/core; importar { NavController, NavParams, AlertController } de iónico-angular; importar { Http, Cabeceras, RequestOptions } de "@angular/http"; importar "rxjs/Rx"; @Componente({ selector: página-consolas, templateUrl: consolas.html }) exportar clase ConsolasPágina { público consolas: Matriz<cualquier>; público constructor(público navCtrl: NavController, privado http: Http, privado alertCtrl: AlertController) { este.consolas = []; } público ionViewDidEnter() { este.http.consiga("http://localhost:3000/consoles") .mapa(resultado => resultado.json()) .suscríbase a(resultado => { este.consolas = resultado; }); } público crear() { deje alerta = este.alertCtrl.crear({ título: Añadir consola, entradas: [ { nombre: título, marcador de posición: Título }, { nombre: año, marcador de posición: Año } ], botones: [ { texto: Cancelar, papel: cancelar, manipulador: datos => { consola.registro("Cancelar clic); } }, { texto: Guardar, manipulador: datos => { deje cabeceras = nuevo Cabeceras({ "Tipo de contenido": "application/json" }); deje opciones = nuevo RequestOptions({ cabeceras: cabeceras }); este.http.Correo electrónico:("http://localhost:3000/console", JSON.stringify(datos), opciones) .suscríbase a(resultado => { este.consolas.pulse(datos); }, error => {}); } } ] }); alerta.presente(); } } |
Aunque no es muy diferente de lo que ya hemos visto, tenemos una nueva característica. Estamos utilizando un cuadro de diálogo emergente para recoger información sobre nuevas consolas de videojuegos.
Cuando se cierra la ventana emergente, se ejecuta lo siguiente:
1 2 3 4 5 6 7 8 |
manipulador: datos => { deje cabeceras = nuevo Cabeceras({ "Tipo de contenido": "application/json" }); deje opciones = nuevo RequestOptions({ cabeceras: cabeceras }); este.http.Correo electrónico:("http://localhost:3000/console", JSON.stringify(datos), opciones) .suscríbase a(resultado => { este.consolas.pulse(datos); }, error => {}); } |
Esto tomará la información encontrada en el formulario y la enviará vía HTTP a nuestra API que a su vez guardará la información de la consola en la base de datos.
Impresionante, ¿verdad?
El HTML que se encuentra en el proyecto src/pages/consolas/consolas.html tiene el siguiente aspecto:
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-cabecera> <ion-barra de navegación> <botón ion-botón menuToggle> <ion-icono nombre="menú"></ion-icono> </botón> <ion-título>Consolas</ion-título> <ion-botones fin> <botón ion-botón icono-sólo (haga clic en)="create()"> <ion-icono nombre="añadir"></ion-icono> </botón> </ion-botones> </ion-barra de navegación> </ion-cabecera> <ion-contenido> <ion-lista> <botón ion-artículo *ngFor="la consola de las consolas"> {{ consola.título }} <span clase="artículo-nota" artículo-derecha> {{ consola.año }} </span> </botón> </ion-lista> </ion-contenido> |
De nuevo, es casi idéntico a los otros archivos HTML que hemos visto.
Conclusión
Acabas de recibir un resumen de todo lo que repasé en Midwest JS 2017. Vimos cómo crear una API Node.js que se comunica con Couchbase, nuestra base de datos NoSQL, así como crear un frontend usando Angular y Ionic Framework. Estos son sólo algunos componentes de una aplicación de pila completa.
Para más información usando Node.js con Couchbase, consulte la Portal para desarrolladores de Couchbase. Si quieres que vuelva a Midwest JS, házmelo saber en los comentarios.