- ¿Qué es MEAN?
- Angular
- TypeScript:
- Developer Experience de Angular:
- Instalación y Configuración de Entorno
- Angular Material
- Estructura de los Archivos
- Componentes
- Modelos
- Angular Moment
- Formularios en Angular
- Ocultar Elementos (ngIf)
- Repetir Elementos (ngFor)
- Pasar datos a un componente
- Routing
- Child Routing
- Services
- Obtener parámetros de la url
- NodeJS
- Deploy
- Recursos Complementarios
- Enlaces de Interés
MEAN es uno de los Stacks tecnológicos más utilizados para crear Single Page Applications:
- MongoDb para la base de datos.
- Express como framework backend corriendo en Node.js.
- Angular para el frontend.
- Node.js es nuestro servidor web, encargado de escuchar las peticiones de los usuarios y responderle con los datos que requieran.
- Configurar sus rutas
- Proteger los recursos
- Servir archivos estáticos
Estas tecnologías son muy sencillas de combinar: todo está escrito en JavaScript, incluso la base de datos guarda colecciones con objetos en JSON.
- Es un framework para Frontend.
- Tiene la opción actualmente de correrlo desde el Backend y poder reutilizar el código.
- Nos permite organizar los archivos que vamos a tener y de separar las responsabilidades de cada archivo. Nos permite separar en componentes reutilizables nuestra aplicación.
- Este framework, de lo que más se va a tratar es de componentes. Los componentes son unidades visuales reutilizables y con cierto comportamiento definido.
- Otra cosa que nos va a solucionar Angular, es el routing, es decir, el sistema de ruteo.
- Angular nos va a dar también los services. son maneras de conectarnos con el Backend, desde ahí vamos a pedir ciertos datos que necesita el Frontend para mostrar en la pantalla.
- Angular ofrece un sistema de templates. Cada componente va a tener asociado un template o vista que va a ser la estructura HTML que tenga.
- Desde Angular es posible que podamos definir nuestras propias etiquetas para definir los componentes reutilizables.
- Otra cosa que nos permite Angular son los modules o módulos. Son librerías enteras con componentes y directivas ya armadas o preestablecidas. Un ejemplo es Material Angular que es la que vamos a estar utilizando para nuestro proyecto del curso.
- CLI = Command Line Interface o Interfaz de Línea de Comandos.
- Google está detrás de Angular. Son los desarrolladores de Google quienes le dan soporte a esta librería Open Source.
- Angular es muy adaptado y tiene una comunidad muy grande alrededor.
- Es un super set de JavaScript. nos va a ayudar a expandir un poco las capacidades de JavaScript, va incorporar un chequeo de tipos para corrección de errores.
- Es una tecnología ideal para trabajo en proyectos en equipo.
Uno de los puntos a favor es que cada vez que hagamos cambios en nuestros archivos se van a ver reflejados en nuestro navegador de forma automática sin tener que recompilar a mano y sin tener que refrescar.
Instalar el CLI de Angular
$ npm i -g @angular/cli
Para crear un proyecto, ir a la carpeta y escribir lo siguiente:
$ ng new [proyecto]
Para correr la aplicación:
$ ng serve
Angular Material son componentes basados en Material Design para Angular.
Paso 1: Instalar las dependencias
$ npm i @angular/material @angular/cdk
Para instalar las animaciones de Angular Material:
$ npm i @angular/animations
Paso 2: Configurar las animaciones
Para habilitar las animaciones, se debe de importar el paquete en la aplicación:
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
@NgModule({
// ...
imports: [BrowserAnimationsModule],
// ...
})
export class AppModule { }
Paso 3: Importar los módulos de los componentes
Para importar los módulos, se va a crear un archivo llamado material.module.ts. Ahí se van a agregar todos los módulos que se desean importar.
import { NgModule } from '@angular/core';
import { MatButtonModule, MatCheckboxModule } from '@angular/material';
const modules = [
MatButtonModule,
MatCheckboxModule
]
@NgModule({
imports: modules,
exports: modules
})
export class MaterialModule {}
Luego, se va a importar el archivo creado en el módulo principal de la aplicación.
import { MaterialModule } from './material.module';
@NgModule({
//...
imports: [
MaterialModule
]
//...
})
export class AppModule { }
Paso 4: Incluir un theme
El theme se va a encargar de aplicar todos los estilos básicos a la aplicación. Para esto, solo se debe de agregar el theme en el archivo styles.css.
@import "~@angular/material/prebuilt-themes/indigo-pink.css";
Paso 5: Soporte de gestos
Algunos componentes requieren HammerJS para realizar gestos en los dispositivos táctiles.
$ npm i hammerjs
Luego de instalarlos, solo se debe de importar en el módulo principal (ej. app.module.ts
).
import 'hammerjs';
Paso 6: Agregar íconos de material
Para usar los íconos de Material Design, se debe de agregar la fuente de el index.html.
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
Existen dos maneras de organizar la estructura de los archivos dentro de nuestro proyecto:
- Siguiendo la funcionalidad de cada uno de los archivos.
- Tenemos ciertos modelos, ciertas pantallas, ciertos componentes o ciertos servicios. Podemos ir almacenando todo en una carpeta según el tipo.
Para crear un componente, se va a crear un archivo con la siguiente nomenclatura: mi-componente.component.ts.
import { Component } from '@angular/core';
@Component({
selector: 'app-mi-componente',
templateUrl: './mi-componente.component.html'
})
export class MiComponenteComponent {}
El html del componente se va a escribir en mi-componente.component.html
.
<div>hola mundo</div>
Para usar el componente, se debe de importar dentro del componente principal.
import { MiComponenteComponent } from './mi-componente.component';
@NgModule({
//...
declarations: [
MiComponenteComponent
]
//...
})
export class AppModule { }
Luego se puede usar en el html de la siguiente manera:
<app-mi-componente></app-mi-componente>
Los modelos se van a escribir en un archivo aparte. Por ejemplo: question.model.ts.
El caracter ? va a indicar que un atributo puede ser nulo.
export class Question {
title: string;
description: string;
createdAt?: Date;
icon?: string;
constructor(
title: string,
description: string,
createdAt?: Date,
icon?: string
) {
this.title = title;
this.description = description;
this.createdAt = createdAt;
this.icon = icon;
}
}
Otra forma de hacer modelos es directamente desde el constructor:
export class User {
constructor(
public firstName: string,
public lastName: string
) {}
}
Para consumir el modelo dentro del componente, hacerlo de la siguiente manera:
import { Question } from './question.model';
//...
export class QuestionDetailComponent {
question: Question = new Question(
'Esta es una nueva pregunta sobre Android',
'Miren, tengo una dura con una aplicación que estoy escribiendo para Android',
new Date,
'devicon-android-plain'
);
}
ngx-moment es una librería que nos permite saber la temporalidad de las acciones en las aplicaciones.
$ npm i moment ngx-moment
Luego de instalarlo, se debe de impotar en el módulo de la aplicación:
import { MomentModule } from 'ngx-moment';
@NgModule({
imports: [
MomentModule
]
})
Se usa en el html por medio de los pipes:
<div>{{question.createdAt | amTimeAgo}}</div>
Si se quiere cambiar el idioma a español, primero se debe de importar el idioma en el app component.
import 'moment/locale/es';
Y para consumirlo se hace de la siguiente forma:
<small>{{question.createdAt | amLocale:'es' | amTimeAgo}}</small>
Existen 2 formas de crear formularios en Angular:
Template-Driven Form
Para usar formularios en Angular, va a usar @angular/forms
.
import { Component } from '@angular/core';
import { NgForm } from '@angular/forms';
@Component({
selector: 'app-answer-form',
templateUrl: './answer-form.component.html'
})
export class AnswerFormComponent {
onSubmit(form: NgForm) {
console.log(form.value.description);
}
}
En el html, se debe indicar que cuando se haga submit al formulario, se llame a la función onSubmit. Esto se hace poniendo (ngSubmit)="onSubmit(f)" #f="ngForm"
en el <form>
.
Asimismo, en los diferentes campos, es importante poner un ngModel
para poder acceder al valor desde el component y un name
.
<form (ngSubmit)="onSubmit(f)" #f="ngForm">
<mat-form-field>
<textarea matInput placeholder="Respuesta" name="description" ngModel></textarea>
</mat-form-field>
<button type="submit" mat-raised-button color="accent">Responder</button>
</form>
Reactive Forms
Es una filosfia más "Angular" de crear formularios. Se comienza definiendo el formulario desde la clase del componente.
Una ventaja de este tipo de formularios es la facilidad al momento de validar los campos.
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';
import { User } from './user.model';
@Component({
selector: 'app-signin-screen',
templateUrl: 'signin-screen-component.html'
})
export class SigninScreenComponent implements OnInit {
signinForm: FormGroup;
ngOnInit() {
this.signinForm = new FormGroup({
email: new FormControl(null, [
Validators.required,
Validators.pattern(/^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/)
]),
password: new FormControl(null, Validators.required)
});
}
onSubmit() {
if(this.signinForm.valid) {
const { email, password } = this.signinForm.value;
const user = new User(email, password);
console.log(user)
}
}
}
En el HTML, hay que hacer el binding del formularios (formGroup) y del evento (onSubmit). Asimismo, para poder acceder a los campos del formulario (input, textare, etc), se va a definir una propiedad formControlName
-
<form [formGroup]="signinForm" (ngSubmit)="onSubmit()">
<mat-form-field>
<input matInput placeholder="Correo electrónico" formControlName="email" id="email">
</mat-form-field>
<mat-form-field>
<input type="password" matInput placeholder="Contraseña" formControlName="password" id="password">
</mat-form-field>
<button type="submit" mat-raised-button color="accent">Iniciar sesión</button>
</form>
Para ocultar elementos HTML, se va a usar *ngIf
. Si se cumple la condición, se muestra el elemento, de lo contrario se oculta.
<p *ngIf="answers.length === 0">Hola Mundo</p>
Para repetir varias veces un elemento en el HTML, se puede usar *ngFor
.
<li *ngFor="let answer of answers">
<h5>
{{ answer.user.firstName }} {{ answer.user.lastName }}
<small>{{answer.createdAt | amLocale:'es' | amTimeAgo}}</small>
</h5>
<p class="description">
{{ answer.description }}
</p>
</li>
Para recibir datos en un componente, se va a usar @Input()
.
import { Component, Input } from '@angular/core';
import { NgForm } from '@angular/forms';
import { Answer, User } from './answer.model';
import { Question } from '../question/question.model';
@Component({
selector: 'app-answer-form',
templateUrl: './answer-form.component.html'
})
export class AnswerFormComponent {
@Input() question: Question;
onSubmit(form: NgForm) {
const answer = new Answer("Es una pregunta");
this.question.answers.unshift(answer);
}
}
Para enviar el dato desde el html, se agrega con [input]="data"
.
<app-answer-form [question]="question" class="answer-form"></app-answer-form>
Lo primero que se debe de hacer para definir rutas es agregar el Routing en el app.module.
import { Routing } from './app.routing';
@NgModule({
//...
imports: [
Routing
]
//...
})
export class AppModule { }
Luego, para definir rutas dentro de Angular, se va a crear el archivo app.routing.ts.
En ese archivo, se va a importar Routes y RouterModule y se van a definir las rutas con sus respectivos componentes.
import { Routes, RouterModule } from '@angular/router';
import { QuestionListComponent } from './question/question-list.component';
import { SigninScreenComponent } from './auth/signin-screen-component';
import { SignupScreenComponent } from './auth/signup-screen.component';
const APP_Routes: Routes = [
{ path: '', component: QuestionListComponent, pathMatch: 'full' },
{ path: 'signin', component: SigninScreenComponent },
{ path: 'signun', component: SignupScreenComponent }
];
export const Routing = RouterModule.forRoot(APP_Routes);
Para usar las rutas en el HTML, se hace de la siguiente manera:
router-outlet
: Define el lugar en donde se va a cargar el componente definido en app.routing.ts dependiendo de la url.routerLink
: define a dónde va redirigir al hacer click.
<mat-toolbar color="primary">
<span [routerLink]="['/']">PlatziOverflow</span>
<span class="space"></span>
<mat-icon [routerLink]="['/signin']">account_circle</mat-icon>
</mat-toolbar>
<router-outlet></router-outlet>
Child routex se usa para definir una ruta con múltiples opciones. Por ejemplo, /question y /question/1265.
Para definir un child route, primero se va a crear un nuevo archivo en donde se van a definir las rutas correspondientes.
//question.routing.ts
import { QuestionListComponent } from './question-list.component';
import { QuestionDetailComponent } from './question-detail.component';
export const QUESTION_ROUTES = [
{ path: '', component: QuestionListComponent },
{ path: ':id', component: QuestionDetailComponent }
];
Luego, en el archivo de rutas principal, se va a definir la ruta de la siguiente manera:
import { Routes, RouterModule } from '@angular/router';
import { QUESTION_ROUTES } from './question/question.routing';
const APP_Routes: Routes = [
{ path: 'questions', children: QUESTION_ROUTES }
];
export const Routing = RouterModule.forRoot(APP_Routes);
Los servicios permiten consumir API dentro de una aplicación de Angular.
Los servicios usan el módulo Http de Angular por lo que también hay que incluirlo dentro de app.module.ts.
Un ejemplo de cómo hacer servicios es el siguiente:
//question.service.ts
import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import { Question } from './question.model';
import { environment } from '../../environments/environment';
import urljoin from 'url-join';
@Injectable()
export class QuestionService {
private questionsUrl: string;
constructor(private http: Http) {
this.questionsUrl = urljoin(environment.apiUrl, 'questions');
}
getQuestions(): Promise<void | Question[]> {
return this.http.get(this.questionsUrl)
.toPromise()
.then(response => response.json() as Question[])
.catch(this.handleError);
}
getQuestion(id): Promise<void | Question> {
const url = urljoin(this.questionsUrl, id);
return this.http.get(url)
.toPromise()
.then(response => response.json() as Question)
.catch(this.handleError);
}
handleError(error: any) {
const errMsg = error.message ? error.message :
error.status ? `${error.status} - ${error.statusText}` : 'Server error';
console.log(errMsg);
}
}
La url base de donde se van a consumir la api se va a definir en environments/environments.ts.
export const environment = {
apiUrl: 'http://localhost:3000/api'
};
También se va a usar una librería llamada url-join la cual servirá para juntar elementos y crear urls.
$ npm i url-join
Los servicios se consumen desde el componente. Todo servicio que se vaya a usar se debe de declarar en la sección de providers.
Asimismo, el servicio debe de injectarse en el constructor de la clase.
import { Component, OnInit } from '@angular/core';
import { Question } from './question.model';
import { QuestionService } from './question.service';
@Component({
selector: 'app-question-list',
templateUrl: 'question-list.component.html',
providers: [QuestionService]
})
export class QuestionListComponent implements OnInit {
constructor(private questionService: QuestionService) {}
questions: Question[];
ngOnInit() {
this.questionService.getQuestions()
.then((questions: Question[]) => {
this.questions = questions;
});
}
}
Para poder obtener un parámetro de la url dentro del componente, se va a usar ActivatedRoute.
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({
//...
})
export class Component implements OnInit, OnDestroy {
sub: any;
constructor(
private route: ActivatedRoute
) {}
ngOnInit() {
this.sub = this.route.params.subscribe(params => {
console.log(params.id)
});
}
ngOnDestroy() {
this.sub.unsubscribe();
}
}
NodeJS es un entorno donde se puede correr JavaScript a través de un motor llamado V8 que Google desarrolló. Este motor permite correr JS en el servidor.
La principal diferencia entre Node y otros lengiajes de backend es que con el primero se programa también el servidor web. No solamente cierta lógica.
Node tiene un único thread en toda la aplicación. Hay un solo proceso que corre y atiende todos los pedidos y este único proceso corre el event loop.
¿Qué puedo construir con NodeJS?
- Servidores web/Web APIs
- Herramientas utilitarias
Express es un framework de NodeJS. Con Express puedo:
- Crear servidores web.
- Hacer API.
Características:
- Rápido: brinda código y funcionalidades que se usan comunmente sin que decaiga el performance del servidor web.
- Sin opiniones (opinionated): da la posibilidad de hacer las cosas como uno quiere.
- Minimalista: entrega solo lo justo y necesario.
Express funciona por medio de middlewares. Un middleware es como un tubo donde vamos conectando un tubo al lado del otro.
Los middlewares son funciones de javascript. Estas funciones reciben 3 parámetros como máximo:
- Request
- Response
- Apuntador al siguiente middleware
La idea de los middlewares es que sean reutilizables y que cumplan con una tarea específica.
Alternativas a Express:
- NodeJs puro
- Koa
Lo primero que se debe de usar para usar node, es configurar el servidor. Para esto, se va a crear un archivo server/index.js.
import http from 'http'
import Debug from 'debug'
const PORT = 3000
const app = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.end()
})
app.listen(PORT, () => {
console.log(`Server running at port ${PORT}`)
})
Babel permite traspilar código moderno de Javascript en uno que entiendan los diferentes navegadores.
$ npm i babel-cli babel-preset-es2015
Para usar babel, se tiene que definir un archivo de configuración llamado .babelrc.
{
"presets": ["es2015"]
}
Finalmente se crea un script en package.json en donde se usa babel.
"scripts": {
"start:server": "babel-node server/index.js"
}
Debug permite mostrar mensajes de debugging en la consola.
$ npm i debug
Para usarlo, se puede hacer de la siguiente forma:
import Debug from 'debug'
const debug = new Debug('platzi-overflow:root')
const app = http.createServer((req, res) => {
debug('new request')
})
Finalmente, para poder ejecutar el debug, se va a correr el siguiente script:
"scripts": {
"start:server": "set DEBUG=platzi-overflow* & babel-node server/index.js"
}
Nodemon permite que el servidor se reinicie cada vez que hay un cambio.
$ npm i nodemon
Para usar nodemon, configurarlo en los scripts de package.json.
"scripts": {
"start:server": "nodemon server/index.js --exec babel-node"
}
Concurrently permite correr procesos en paralelo con un solo comando. Esto va a ayudar a levantar el servidor de backend y el de Angular al mismo tiempo.
$ npm i concurrently
Para usarlo, en scripts del package.json, usamos el comando concurrently
y entre comillas los scripts que queremos que se ejecuten en simultaneo.
"scripts": {
"start": "concurrently -r \"npm run start:server\" \"npm run start:client\" ",
"start:server": "set DEBUG=platzi-overflow* & nodemon server/index.js --exec babel-node",
"start:client": "ng serve"
}
Express es un microframework de nodejs.
$ npm i express
Configurar el servidor con Express
Crear un archivo en server/app.js
import express from 'express'
const app = express() //express devuelve en una variable el servidor
app.get('/', (req, res) => res.send('Hola desde express'))
export default app
Y en server/index.js hacer un listen de server/app.js
import http from 'http'
import Debug from 'debug'
import app from './app'
const PORT = 3000
app.listen(PORT, () => {
console.log(`Server running at port ${PORT}`)
})
Las rutas se van a definir en una carpeta routes.
Dentro de esta carpeta, se va a crear un archivo por cada modelo de ruta. Por ejemplo, question.js:
import express from 'express'
const app = express.Router()
const question = {
_id: 1,
title: '¿Cómo utilizo un componente en Android?',
}
const questions = new Array(10).fill(question)
// /api/questions
app.get('/', (req, res) => res.status(200).json(questions))
// /api/questions/:id
app.get('/:id', (req, res) => res.status(200).json(question))
export default app
También se va a tener un archivo routes/index.js en donde se van a listar todas las rutas:
export { default as question } from './question'
Finalmente, las rutas se van a consumir en el archivo app.js:
import express from 'express'
import { question } from './routes'
const app = express()
app.use('/api/questions', question)
export default app
Por motivos de seguridad, los navegadores no permite el acceso a una API desde una url diferente.
Para permitir el acceso a API en un ambiente de desarrollo, hacer lo siguiente.
if (process.env.NODE_ENV === 'development') {
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Request-With, Content-Type, Accept')
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, PATCH, DELETE, OPTIONS')
next()
})
}
Además, hay que setear el NODE_ENV en el package.json.
"scripts": {
"start:server": "set NODE_ENV=development& set DEBUG=platzi-overflow* & nodemon server/index.js --exec babel-node"
}
Para leer los formatos json y utf-8 en el servidor, se va a usar una librería llamada body-parser.
$ npm i body-parser
El siguiente paso es agregar las siguientes líneas en el archivo server/app.js.
import express from 'express'
import bodyParser from 'body-parser'
const app = express()
app.use(bodyParser.json()) //poder leer todo lo que viene en formato json del cliente
app.use(bodyParser.urlencoded({ extended: true })) //poder leer todo lo que viene en formato utf-8
Para crear una aplicacion de Heroku, ir al directorio del archivo y escribir:
$ heroku create
Esto va a crear una url en donde se va a encontrar la aplicación. También anañe al git un acceso remoto llamado Heroku.
Para ingresar a la url:
$ heroku open
Se va a usar el addon mLab MongoDB de Heroku para conectarse a una bd de Mongo.
$ heroku addons:create mongolab:sandbox
Se va a crear la base de datos y se va a guardar la url de mongo como una variable de entorno.
Heroku permite almacenar variables de entorno las cuales van a ser accesibles solo desde el servidor.
Para setear variables de entorno:
$ heroku config:set VARIABLE=miVariableDeEntorno
Para ver el listado de las variables:
$ heroku config
Para poder crear un build del cliente, se va a crear un nuevo script en package.json:
"scripts": {
"build:client": "ng build --configuration=production"
}
Asimismo, para poder ejecutar el comando en Heroku, es importante pasar algunas devDependencies a Dependencies.
"dependencies": {
"@angular/cli": "~6.1.5",
"@angular/compiler-cli": "^6.1.0",
"babel-plugin-transform-async-to-generator": "^6.24.1",
"babel-polyfill": "^6.26.0",
"babel-preset-es2015": "^6.24.1",
"babel-preset-stage-0": "^6.24.1",
"typescript": "~2.7.2"
}
Si se quiere generar el build, se puede ejecutar el siguiente comando:
$ npm run build:client
Se va a instalar babel-plugin-transform-async-to-generator el cual va a permitir transformar las funciones que tienen async en generadores de ES2015 que entiende node.
$ npm i babel-plugin-transform-async-to-generator
Luego, se va a agregar babel-plugin-transform-async-to-generator a la lista de plugins de .babelrc.
{
"presets": ["es2015", "stage-0"],
"plugins": ["transform-async-to-generator"]
}
Otra cosa que hay que hacer el instalar el polyfill de Babel. El polyfill va a permitir correr las funciones generator dentro de nuestro entorno.
$ npm i babel-polyfill
Tambien se debe de crear un archivo llamado server.js en la raiz del proyecto. Este va a ser el archivo que el servidor de Heroku va a correr con el comando de node.
require('babel-polyfill')
require('./dist/server')
Finalmente, agregar el siguiente script para hacer build al servidor:
"scripts": {
"build:server": "babel server --out-dir dist/server"
}
Agregar los siguientes scripts en el package.json:
"scripts": {
"start-prod": "NODE_ENV=production node server.js",
"build": "npm run build:client && npm run build:server",
"postinstall": "npm run build"
}
- start-prod: va a iniciar el servidor.
- build: hace un build del cliente y del servidor.
- postinstall: se ejecuta al finalizar de instalar todas las dependencias en Heroku.
Además de esto, se va a crear un archivo Procfile en la raiz del proyecto:
web: npm run start-prod
Finalmente, para subir la aplicación a producción, hacer un push a Heroku:
$ git push heroku origin