Siguiendo por la senda de los almacenes de perfiles de usuario, habíamos visto anteriormente cómo crear uno con Node.js y Couchbase NoSQL así como un cliente web front-end para él usando Angular. ¿Qué pasaría si quisiéramos llevar esto a una aplicación móvil, que es la norma para todas las aplicaciones a partir de ahora.
Hay muchos frameworks móviles diferentes por ahí y tenemos la suerte de que algunos incluso soportan Angular, que es lo que habíamos utilizado en el ejemplo anterior. Vamos a ver cómo convertir nuestro front-end cliente a móvil usando NativeScript y Angular.
Antes de seguir adelante, se supone que has completado los dos tutoriales anteriores, uno sobre creación del backend de la tienda de perfiles y el otro en creación del front-end web de la tienda de perfiles. También vamos a suponer que su entorno de desarrollo está configurado para el desarrollo móvil, ya sea Android, iOS, o ambos.

El flujo de eventos en esta aplicación coincidirá con lo que vimos en la versión web.
Crear un nuevo proyecto NativeScript con Angular
Asumiendo que tienes el NativeScript CLI instalado y configurado, ejecuta lo siguiente para crear un nuevo proyecto:
|
1 |
tns create profile-project-ns --ng |
En --ng en el comando anterior es importante porque significa que estamos creando un proyecto Angular en lugar de un proyecto NativeScript Core.
Por ahora, el CLI de NativeScript no comparte las capacidades del CLI de Angular de generar componentes. Por esta razón, vamos a tener que crear cada uno de los archivos HTML y TypeScript manualmente.
Si estás en Mac o Linux, ejecuta lo siguiente desde el proyecto NativeScript:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
mkdir -p app/login mkdir -p app/register mkdir -p app/blogs mkdir -p app/blog touch app/login/login.component.html touch app/login/login.component.ts touch app/register/register.component.html touch app/register/register.component.ts touch app/blogs/blogs.component.html touch app/blogs/blogs.component.ts touch app/blog/blog.component.html touch app/blog/blog.component.ts |
Si estás en Windows, simplemente crea esos directorios y archivos manualmente. Si realmente quisieras, podrías copiar estos directorios y archivos de tu proyecto web desde la carpeta tutorial anterior.
Definición de los componentes para representar cada pantalla
Empezando en la misma dirección que la versión web, vamos a centrarnos en el inicio de sesión del usuario. Abra el proyecto app/login/login.component.ts e incluya el siguiente código:
|
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 |
import { Component } from '@angular/core'; import { Http, Headers, RequestOptions } from "@angular/http"; import { Router } from "@angular/router"; import "rxjs/Rx"; @Component({ moduleId: module.id, selector: 'app-login', templateUrl: './login.component.html' }) export class LoginComponent { public input: any; constructor(private http: Http, private router: Router) { this.input = { "email": "", "password": "" }; } public login() { if(this.input.email && this.input.password) { let headers = new Headers({ "content-type": "application/json" }); let options = new RequestOptions({ headers: headers }); this.http.post("https://localhost:3000/login", JSON.stringify(this.input), options) .map(result => result.json()) .subscribe(result => { this.router.navigate(["/blogs"], { "queryParams": result }); }); } } } |
El código anterior es exactamente lo que vimos en la versión web con dos excepciones. He eliminado la referencia CSS ya que no creamos un archivo CSS por componente. También he añadido un moduleId para que las rutas relativas puedan funcionar con nuestro componente. Ambos son elementos relacionados con Angular que no tienen nada que ver con NativeScript.
El HTML es donde las cosas son diferentes. Abra el archivo app/login/login.component.html e incluya el siguiente marcado XML:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<ActionBar title="{N} Profile Store"></ActionBar> <StackLayout class="form"> <StackLayout class="input-field"> <Label text="Email" class="label font-weight-bold m-b-5"></Label> <TextField class="input" [(ngModel)]="input.email" autocapitalizationType="none"></TextField> <StackLayout class="hr-light"></StackLayout> </StackLayout> <StackLayout class="input-field"> <Label text="Password" class="label font-weight-bold m-b-5"></Label> <TextField class="input" secure="true" [(ngModel)]="input.password" autocapitalizationType="none"></TextField> <StackLayout class="hr-light"></StackLayout> </StackLayout> <StackLayout class="input-field"> <Button class="btn btn-primary" text="Login" (tap)="login()"></Button> </StackLayout> <StackLayout class="input-field"> <Label text="Register here." [nsRouterLink]="['/register']" class="text-center"></Label> </StackLayout> </StackLayout> |
NativeScript tiene su propio marcado para la interfaz de usuario. Las mismas reglas se aplican en términos de Angular, pero la creación de componentes de interfaz de usuario es un poco diferente.
Por ejemplo, seguimos utilizando el [(ngModel)] pero en lugar de div etiquetas, tenemos StackLayout tags.
Veamos ahora el componente de registro. Abra el archivo app/register/register.component.ts e incluir el siguiente TypeScript:
|
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 |
import { Component } from '@angular/core'; import { Http, Headers, RequestOptions } from "@angular/http"; import { Router } from "@angular/router"; import "rxjs/Rx"; @Component({ moduleId: module.id, selector: 'app-register', templateUrl: './register.component.html' }) export class RegisterComponent { public input: any; public constructor(private http: Http, private router: Router) { this.input = { "firstname": "", "lastname": "", "email": "", "password": "" }; } public register() { if(this.input.email && this.input.password) { let headers = new Headers({ "content-type": "application/json" }); let options = new RequestOptions({ headers: headers }); this.http.post("https://localhost:3000/account", JSON.stringify(this.input), options) .map(result => result.json()) .subscribe(result => { this.router.navigate(["/login"]); }); } } } |
Una vez más, los únicos cambios que hemos hecho en el código anterior, en comparación con el ejemplo anterior, es en la eliminación de CSS y moduleId Además.
No está nada mal cuando se trata de crear aplicaciones web y móviles multiplataforma, ¿verdad?
El HTML de la interfaz de usuario que alimenta la lógica de TypeScript se encuentra en el archivo app/register/register.component.html y tiene este 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 |
<ActionBar title="{N} Profile Store"></ActionBar> <StackLayout class="form"> <StackLayout class="input-field"> <Label text="First Name" class="label font-weight-bold m-b-5"></Label> <TextField class="input" [(ngModel)]="input.firstname" autocapitalizationType="none"></TextField> <StackLayout class="hr-light"></StackLayout> </StackLayout> <StackLayout class="input-field"> <Label text="Last Name" class="label font-weight-bold m-b-5"></Label> <TextField class="input" [(ngModel)]="input.lastname" autocapitalizationType="none"></TextField> <StackLayout class="hr-light"></StackLayout> </StackLayout> <StackLayout class="input-field"> <Label text="Email" class="label font-weight-bold m-b-5"></Label> <TextField class="input" [(ngModel)]="input.email" autocapitalizationType="none"></TextField> <StackLayout class="hr-light"></StackLayout> </StackLayout> <StackLayout class="input-field"> <Label text="Password" class="label font-weight-bold m-b-5"></Label> <TextField class="input" secure="true" [(ngModel)]="input.password" autocapitalizationType="none"></TextField> <StackLayout class="hr-light"></StackLayout> </StackLayout> <StackLayout class="input-field"> <Button class="btn btn-primary" text="Register" (tap)="register()"></Button> </StackLayout> <StackLayout class="input-field"> <Label text="Login here." [nsRouterLink]="['/login']" class="text-center"></Label> </StackLayout> </StackLayout> |
Los dos últimos componentes no van a ser diferentes de lo que ya estamos experimentando.
Abra el archivo app/blogs/blogs.component.ts e incluya el siguiente código TypeScript:
|
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 |
import { Component, OnInit } from '@angular/core'; import { Http, Headers, RequestOptions } from "@angular/http"; import { Router, ActivatedRoute } from "@angular/router"; import { Location } from "@angular/common"; import "rxjs/Rx"; @Component({ moduleId: module.id, selector: 'app-blogs', templateUrl: './blogs.component.html' }) export class BlogsComponent implements OnInit { private sid: string; public entries: Array<any>; public constructor(private http: Http, private router: Router, private route: ActivatedRoute, private location: Location) { this.entries = []; } public ngOnInit() { this.location.subscribe(() => { let headers = new Headers({ "authorization": "Bearer " + this.sid }); let options = new RequestOptions({ headers: headers }); this.http.get("https://localhost:3000/blogs", options) .map(result => result.json()) .subscribe(result => { this.entries = result; }); }); this.route.queryParams.subscribe(params => { this.sid = params["sid"]; let headers = new Headers({ "authorization": "Bearer " + params["sid"] }); let options = new RequestOptions({ headers: headers }); this.http.get("https://localhost:3000/blogs", options) .map(result => result.json()) .subscribe(result => { this.entries = result; }); }); } public create() { this.router.navigate(["/blog"], { "queryParams": { "sid": this.sid } }); } } |
No hay cambios que ver en lo anterior aparte de los dos mencionados anteriormente, por lo que podemos pasar a nuestro HTML para la página.
Abra el archivo app/blogs/blogs.component.html e incluya el siguiente código HTML:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<ActionBar title="{N} Profile Store"> <ActionItem text="New" ios.position="right" (tap)="create()"></ActionItem> <NavigationButton text="Back"></NavigationButton> </ActionBar> <GridLayout rows="*, auto" columns="*"> <ListView [items]="entries" class="list-group" row="0" col="0"> <ng-template let-entry="item"> <StackLayout class="list-group-item"> <Label text="{{ entry.title }}" class="h2"></Label> <Label text="{{ entry.content }}"></Label> </StackLayout> </ng-template> </ListView> <StackLayout row="1" col="0" padding="10" backgroundColor="#F0F0F0"> <Label text="Logout" [nsRouterLink]="['/login']"></Label> </StackLayout> </GridLayout> |
Finalicemos esta aplicación con el último componente que ofrece nuestra API de tienda de perfiles y el front-end web.
Abra el archivo app/blog/blog.component.ts e incluye esto:
|
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 |
import { Component, OnInit } from '@angular/core'; import { Http, Headers, RequestOptions } from "@angular/http"; import { ActivatedRoute } from "@angular/router"; import { Location } from "@angular/common"; import "rxjs/Rx"; @Component({ moduleId: module.id, selector: 'app-blog', templateUrl: './blog.component.html' }) export class BlogComponent implements OnInit { private sid: string; public input: any; public constructor(private http: Http, private route: ActivatedRoute, private location: Location) { this.input = { "title": "", "content": "" }; } public ngOnInit() { this.route.queryParams.subscribe(params => { this.sid = params["sid"]; }); } public save() { if(this.input.title && this.input.content) { let headers = new Headers({ "content-type": "application/json", "authorization": "Bearer " + this.sid }); let options = new RequestOptions({ headers: headers }); this.http.post("https://localhost:3000/blog", JSON.stringify(this.input), options) .map(result => result.json()) .subscribe(result => { this.location.back(); }); } } } |
Si no ha copiado el archivo CSS, no olvide eliminarlo del directorio @Componente como se ha visto en los componentes anteriores.
La interfaz de usuario HTML que acompaña a este TypeScript se encuentra en el proyecto app/blog/blog.component.html y tiene el siguiente aspecto:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<ActionBar title="{N} Profile Store"> <NavigationButton text="Back"></NavigationButton> </ActionBar> <StackLayout class="form"> <StackLayout class="input-field"> <Label text="Title" class="label font-weight-bold m-b-5"></Label> <TextField class="input" [(ngModel)]="input.title" autocapitalizationType="none"></TextField> <StackLayout class="hr-light"></StackLayout> </StackLayout> <StackLayout class="input-field"> <Label text="Content" class="label font-weight-bold m-b-5"></Label> <TextView class="input" [(ngModel)]="input.content" autocapitalizationType="none"></TextView> <StackLayout class="hr-light"></StackLayout> </StackLayout> <StackLayout class="input-field"> <Button class="btn btn-primary" text="Save" (tap)="save()"></Button> </StackLayout> </StackLayout> |
Ahora mismo puedes estar rascándote la cabeza con todo este marcado XML de NativeScript. Cómo Angular trabaja con la interfaz de usuario no ha cambiado, pero si usted está interesado en aprender sobre el marcado NativeScript, echa un vistazo a su documentación oficial. Familiarícese con el StackLayout, GridLayout y etiquetas de componentes de interfaz de usuario individuales.
La unión hace la fuerza
Hemos creado todos estos componentes Angular para NativeScript, pero no los hemos unido a través del Router Angular.
En la versión web de esta guía, la información sobre la ruta se encontraba en la sección app.module.ts archivo. Si bien podríamos hacer eso, NativeScript lo dividió en un archivo separado.
Abra el archivo app/app.routing.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 |
import { NgModule } from "@angular/core"; import { NativeScriptRouterModule } from "nativescript-angular/router"; import { Routes } from "@angular/router"; import { LoginComponent } from "./login/login.component"; import { RegisterComponent } from "./register/register.component"; import { BlogsComponent } from "./blogs/blogs.component"; import { BlogComponent } from "./blog/blog.component"; const routes: Routes = [ { path: "", redirectTo: "/login", pathMatch: "full" }, { path: "login", component: LoginComponent }, { path: "register", component: RegisterComponent }, { path: "blogs", component: BlogsComponent }, { path: "blog", component: BlogComponent } ]; @NgModule({ imports: [NativeScriptRouterModule.forRoot(routes)], exports: [NativeScriptRouterModule] }) export class AppRoutingModule { } |
Gran parte del código anterior vino con nuestra nueva plantilla de proyecto. Importamos cada uno de nuestros componentes, y creamos una ruta para ellos.
Del mismo modo, los componentes deben importarse en la carpeta del proyecto app/app.module.ts archivo. Abra este archivo 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 |
import { NgModule, NO_ERRORS_SCHEMA } from "@angular/core"; import { NativeScriptModule } from "nativescript-angular/nativescript.module"; import { AppRoutingModule } from "./app.routing"; import { NativeScriptHttpModule } from "nativescript-angular/http"; import { NativeScriptFormsModule } from "nativescript-angular/forms"; import { AppComponent } from "./app.component"; import { LoginComponent } from "./login/login.component"; import { RegisterComponent } from "./register/register.component"; import { BlogsComponent } from "./blogs/blogs.component"; import { BlogComponent } from "./blog/blog.component"; @NgModule({ bootstrap: [ AppComponent ], imports: [ NativeScriptModule, AppRoutingModule, NativeScriptHttpModule, NativeScriptFormsModule ], declarations: [ AppComponent, LoginComponent, RegisterComponent, BlogsComponent, BlogComponent ], providers: [], schemas: [ NO_ERRORS_SCHEMA ] }) export class AppModule { } |
Además de importar cada componente y añadirlo a la carpeta declaraciones también importamos algunos módulos, como el módulo NativeScriptHttpModule y Módulo NativeScriptForms. En Angular puro, se denominan HttpModule y Módulo de formularios.
En teoría, la aplicación está lista para funcionar.
Resolución de problemas de seguridad en el transporte de aplicaciones (ATS) para iOS
Como la API de Node.js y Couchbase se ejecuta localmente, estamos usando HTTP en lugar de HTTPS. iOS arrojará errores si intentamos acceder a recursos HTTP.
Esto se puede solucionar fácilmente añadiendo una política ATS.
Abra el archivo app/App_Resources/iOS/Info.plist y añade lo siguiente junto al otro XML:
|
1 2 3 4 |
<dict> <key>NSAllowsArbitraryLoads</key> <true /> </dict> |
Lo anterior esencialmente listas blancas todos los puntos finales HTTP. No es seguro para producción, pero sí para pruebas.
Más información sobre ATS en aplicaciones NativeScript se puede leer en un artículo anterior que escribí titulado, Solucionar problemas de seguridad en el transporte de aplicaciones de iOS 9 en NativeScript.
Conclusión
Usted acaba de ver lo fácil que era tomar su cliente web front-end y convertirlo en móvil usando NativeScript y Angular. El ejemplo de la tienda de perfiles de usuario se convirtió rápidamente en un ejemplo de pila completa utilizando la pila de JavaScript. Teníamos un Node.js con Servidor Couchbase backend, Angular web front-end y NativeScript mobile front-end.
El siguiente paso, u opción, sería utilizar los componentes de Couchbase Mobile en lugar de llamadas HTTP contra la API.
Hola,
He intentado ejecutar este código pero POST no funciona. Cuando intento registrarme obtengo el siguiente error. De en línea me doy cuenta de que https://localhost no funciona en android sdk. Yo uso la dirección IP tanto 10.0.2.2 y 127.0.0.1 pero sigue recibiendo el mismo error.
JS: ERROR Respuesta con estado: 200 para URL: null