Programación Reactiva Con React-NodeJS-MongoDB-rev005
Programación Reactiva Con React-NodeJS-MongoDB-rev005
[email protected]
Programación
Reactiva con React,
NodeJS & MongoDB
❝Se proactivo ante los cambios, pues una vez que llegan no tendrás tiempo para
reaccionar❞
Página | 2
Datos del autor:
Ciudad de México
e-mail: [email protected]
Autor y Editor
Composición y redacción:
Edición:
Portada
Primera edición
3 | Página
Acerca del autor
Oscar Blancarte es originario de Sinaloa, México donde estudió la carrera de
Ingeniería en Sistemas Computacionales y rápidamente se mudó a la Ciudad de
México donde actualmente radica.
Página | 4
Otras obras del autor
5 | Página
Crear tu propia máquina de estados para administrar el ciclo de vida de tu
servidor.
Éstos son sólo algunos de los 25 ejemplos que abordaremos en este libro, los
cuales están acompañados, en su totalidad, con el código fuente para que seas
capaz de descargarlos, ejecutarlos y analizarlos desde tu propia computadora.
Adquiérelo en https://patronesdediseño.com
Página | 6
Agradecimientos
Este libro tiene una especial dedicación a mi esposa Liliana y mi hijo Oscar,
quienes son la principal motivación y fuerza para seguir adelante todos los días,
por su cariño y comprensión, pero sobre todo por apoyarme y darme esperanzas
para escribir este libro.
A mis padres, quien con esfuerzo lograron sacarnos adelante, darnos una
educación y hacerme la persona que hoy soy.
7 | Página
Prefacio
Cada día, nacen nuevas tecnologías que ayudan a construir aplicaciones web más
complejas y elaboras, con relativamente menos esfuerzo, ayudando a que casi
cualquiera persona con conocimientos básicos de computación puede realizar una
página web. Sin embargo, no todo es felicidad, pues realizar un trabajo
profesional, requiere de combinar muchas tecnologías para poder entregar un
producto terminado de calidad.
Puede que aprender React o NodeJS no sea un reto para las personas que ya
tiene un tiempo en la industria, pues ya están familiarizados con HTML,
JavaScript, CSS y JSON, por lo que solo deberá complementar sus conocimientos
con una o dos tecnologías adicionales, sin embargo, para las nuevas
generaciones de programadores o futuros programadores, aprender React o
NodeJS puede implicar un reto aun mayor, pues se necesita aprender primero
las bases, antes de poder programar en una capa más arriba.
Este libro pretende evitarte ese gran dolor de cabeza que yo tuve por mucho
tiempo, pues a lo largo de este libro aprenderemos a utilizar React + NodeJS con
Express + MongoDB y aderezaremos todo esto con Redux, uno de los módulos
más populares y avanzados para el desarrollo de aplicaciones Web profesionales.
Finalmente aprenderemos a crear un API REST completo con NodeJS y utilizando
el Estándar de Autenticación JSON Web Tokens.
El objetivo final de este libro es que aprendas a crear aplicaciones Reactivas con
React, apoyado de las mejores tecnologías disponibles. Es por este motivo que,
durante la lectura de este libro, trabajaremos en un único proyecto que irá
evolucionando hasta terminarlo por completo. Este proyecto será, una réplica de
la red social Twitter, en la cual podremos crear usuarios, autenticarnos, publicar
Tweets, seguir a otros usuarios y ver las publicaciones de los demás en nuestro
feed.
Página | 8
Cómo utilizar este libro
Este libro es en lo general fácil de leer y digerir, pues tratamos de enseñar todos
los conceptos de la forma más simple y asumiendo que el lector tiene poco o
nada de conocimiento del tema, así, sin importar quien lo lea, todos podamos
aprender fácilmente.
Como parte de la dinámica de este libro, hemos agregado una serie de tipos de
letras que hará más fácil distinguir entre los conceptos importantes, código,
referencias a código y citas. También hemos agregado pequeñas secciones de
tips, nuevos conceptos, advertencias y peligros, los cuales mostramos mediante
una serie de íconos agradables para resaltar a la vista.
Texto normal:
Negritas:
Cursiva:
Es texto lo utilizamos para hacer referencia a fragmentos de código como
una variable, método, objeto o instrucciones de líneas de comandos. Pero
también es utilizada para resaltar ciertas palabras técnicas.
Código
1. ReactDOM.render(
2. <h1>Hello, world!</h1>,
3. document.getElementById('root')
4. );
El texto con fondo verdes, lo utilizaremos para indicar líneas que se agregan al
código existente.
9 | Página
Por otra parte, tenemos los íconos, que nos ayudan para resaltar algunas cosas:
Tip
Esta caja la utilizamos para dar un tip o sugerencia
que nos puede ser de gran utilidad.
Importante
Esta caja se utiliza para mencionar algo muy
importante.
Error común
Esta caja se utiliza para mencionar errores muy
comunes que pueden ser verdadero dolor de cabeza o
para mencionar algo que puede prevenir futuros
problemas.
Documentación
Esta caja se muestra cada vez que utilizamos un nuevo
servicio del API REST, la cual tiene la intención de
indicar al lector donde puede encontrar la
documentación del servicio, como es la URL,
parámetros de entrada/salida y restricciones de
seguridad.
Página | 10
Código fuente
Todo el código fuente de este libro está disponible en GitHub y dividido de tal
forma que, cada capítulo tenga un Branch independiente. El código fuente lo
puedes encontrar en:
https://github.com/oscarjb1/books-reactiveprogramming.git
11 | Página
Requisitos previos
Este libro está diseñado para que cualquier persona con conocimientos básicos
de programación web, puedo entender la totalidad de este libro, sin embargo,
debido a la naturaleza de React y NodeJS, es necesario conocer los fundamentos
de JavaScript, pues será el lenguaje que utilizaremos a lo largo de todo este libro.
Página | 12
INTRODUCCIÓN
Muy lejos han quedado los tiempos en los que Tim Berners Lee, conocido como
el padre de la WEB; estableció la primera comunicación entre un cliente y un
servidor, utilizando el protocolo HTTP (noviembre de 1989). Desde entonces, se
ha iniciado una guerra entre las principales compañías tecnológicas por dominar
el internet. El caso más claro, es la llamada guerra de los navegadores,
protagonizado por Netscape Communicator y Microsoft, los cuales buscaban que
sus respectivos navegadores (Internet Explorer y Netscape) fueran el principal
software para navegar en internet.
Durante esta guerra, las dos compañías lucharon ferozmente, Microsoft luchaba
por alcanzar a Netscape, quien entonces le llevaba la delantera y Netscape
luchaba para no ser alcanzados por Microsoft. Como hemos de imaginar,
Microsoft intento comprar a Netscape y terminar de una vez por todas con esta
guerra, sin embargo, Netscape se negó reiteradamente a la venta, por lo que
Microsoft inicio una de las más descaradas estrategias; amenazo con copiar
absolutamente todo lo que hiciera Netscape si no accedían a la venta.
Para no hacer muy larga esta historia, y como ya sabrás, Microsoft termino
ganando la guerra de los navegadores, al proporcionar Internet Explorer
totalmente gratis y preinstalado en el sistema operativo Windows.
Hasta este punto te preguntarás, ¿qué tiene que ver toda esta historia con React
y NodeJS?, pues la verdad es que mucho, pues durante la guerra de los
navegadores Netscape invento JavaScript. Aunque en aquel momento, no era un
lenguaje de programación completo, sino más bien un lenguaje de utilidad, que
permitía realizar cosas muy simples, como validar formularios, lanzar alertas y
realizar algunos cálculos. Es por este motivo que debemos recordar a Netscape,
pues fue el gran legado que nos dejó.
13 | Página
Retornando a JavaScript, este lenguaje ha venido evolucionando de una manera
impresionante, de tal forma que hoy en día es un lenguaje de programación
completo, como Java o C#. Tan fuerte ha sido su evolución y aceptación que hoy
en día podemos encontrar a JavaScript del lado del servidor, como es el caso de
NodeJS, creado por Ryan Dahl en 2009. De la misma forma, Facebook desarrollo
la librería React basada en JavaScript.
Página | 14
Índice
Agradecimientos .................................................................................................................................... 7
Prefacio ................................................................................................................................................. 8
Requisitos previos................................................................................................................................ 12
INTRODUCCIÓN ................................................................................................................................... 13
Índice ................................................................................................................................................... 15
15 | Página
React Developer Tools ...................................................................................................................... 63
Resumen .......................................................................................................................................... 65
Página | 16
Jerarquía de los componentes del proyecto .................................................................................... 117
El enfoque Top-down & Bottom-up................................................................................................. 117
Top-down.................................................................................................................................. 118
Bottom-up................................................................................................................................. 119
El enfoque utilizado y porque .................................................................................................... 119
Preparando el entorno del proyecto ............................................................................................... 120
Instalar API REST ....................................................................................................................... 120
Probando nuestra API................................................................................................................ 123
Invocando el API REST desde React ................................................................................................. 126
Mejorando la clase APIInvoker................................................................................................... 128
El componente TweetsContainer .................................................................................................... 131
El componente Tweet..................................................................................................................... 135
Resumen ........................................................................................................................................ 141
17 | Página
Flujos de actualización de las propiedades...................................................................................... 181
Flujos de desmontaje de un componente ........................................................................................ 182
Mini Twitter (Continuación 2) ......................................................................................................... 182
El componente TwitterApp ........................................................................................................ 183
El componente TwitterDashboard ............................................................................................. 187
El componente Profile ............................................................................................................... 189
El componente SuggestedUsers ................................................................................................. 193
El componente Reply ................................................................................................................. 196
Resumen ........................................................................................................................................ 213
Página | 18
Últimos retoques al proyecto .................................................................................................... 287
Resumen ........................................................................................................................................ 289
Redux................................................................................................................................................. 290
Introducción a Redux...................................................................................................................... 291
Componentes de Redux............................................................................................................. 292
Los tres principios de Redux ...................................................................................................... 294
Como funciona Redux................................................................................................................ 296
Implementando Redux Middleware ................................................................................................ 300
Debugging Redux ........................................................................................................................... 301
Implementando Redux con React ................................................................................................... 303
Estrategia de migración ............................................................................................................. 303
Instalar las dependencias necesarias.......................................................................................... 306
Estructura del proyecto con Redux ............................................................................................ 306
Creando nuestro archivo de acciones ........................................................................................ 307
Creando el primer reducer......................................................................................................... 308
Funciones de dispatcher ............................................................................................................ 310
Implementar la connect en TwitterApp...................................................................................... 311
Crear el Store de la aplicación .................................................................................................... 313
Comprobando la funcionalidad de Redux. .................................................................................. 315
Migrando el proyecto Mini Twitter a Redux .................................................................................... 316
Refactorizando el componente Login ......................................................................................... 316
Refactorizando el componente Signup ....................................................................................... 321
Refactorizando el componente TweetContainer ........................................................................ 327
Refactorizando el componente Tweet ....................................................................................... 331
Refactorizando el componente Reply......................................................................................... 337
Refactorizando el componente Profile ....................................................................................... 341
Refactorizando el componente SuggestedUsers ......................................................................... 343
Refactorizando el componente TwitterDashboard ..................................................................... 346
Refactorizando el componente Toolbar ..................................................................................... 347
Refactorizando el componente Followers & Followings.............................................................. 349
Refactorizando el componente UserCard ................................................................................... 354
Refactorizando el componente MyTweets ................................................................................. 354
Refactorizando el componente UserPage................................................................................... 355
Refactorizando el componente TweetReply ............................................................................... 365
Refactorizando el componente TweetDetails ............................................................................. 366
Últimas observaciones............................................................................................................... 371
Resumen ........................................................................................................................................ 372
19 | Página
Método PUT .............................................................................................................................. 384
Método DELETE ......................................................................................................................... 384
Consideraciones adicionales. ..................................................................................................... 384
Implementemos algunos métodos. ............................................................................................ 385
Trabajando con parámetros ........................................................................................................... 388
Query params............................................................................................................................ 388
URL params ............................................................................................................................... 389
Body params ............................................................................................................................. 390
Middleware ................................................................................................................................... 392
Middleware de nivel de aplicación ............................................................................................. 394
Middleware de nivel de direccionador ....................................................................................... 394
Middleware de terceros ............................................................................................................ 395
Middleware incorporado ........................................................................................................... 395
Error Handler ................................................................................................................................. 396
Resumen ........................................................................................................................................ 397
Página | 20
Implementar los servicios REST....................................................................................................... 458
Servicio - usernameValidate ...................................................................................................... 458
Servicio - Signup ........................................................................................................................ 461
Autenticación con JSON Web Token (JWT) ................................................................................. 463
Servicio - Login .......................................................................................................................... 466
Servicio - Relogin ....................................................................................................................... 469
Servicio - Consultar los últimos Tweets ...................................................................................... 471
Servicio - Consultar se usuarios sugeridos .................................................................................. 474
Servicio – Consulta de perfiles de usuario .................................................................................. 477
Servicio – Consulta de Tweets por usuario ................................................................................. 480
Servicio – Actualización del perfil de usuario .............................................................................. 483
Servicio – Consulta de personas que sigo ................................................................................... 485
Servicio – Consulta de seguidores .............................................................................................. 487
Servicio – Seguir ........................................................................................................................ 489
Servicio – Crear un nuevo Tweet................................................................................................ 491
Servicio – Like............................................................................................................................ 495
Servicio – Consultar el detalle de un Tweet ................................................................................ 497
Documentando el API REST ............................................................................................................ 501
Introducción al motor de plantillas Pug ...................................................................................... 501
API home .................................................................................................................................. 506
Service catalog .......................................................................................................................... 508
Service documentation .............................................................................................................. 513
Algunas observaciones o mejoras al API ......................................................................................... 516
Aprovisionamiento de imágenes................................................................................................ 516
Guardar la configuración en base de datos ................................................................................ 518
Documentar el API por base de datos ........................................................................................ 518
Resumen ........................................................................................................................................ 519
21 | Página
Por dónde empezar
Capítulo 1
Hoy en día existe una gran cantidad de propuestas para desarrollar aplicaciones
web, y cada lenguaje ofrece sus propios frameworks que prometen ser los
mejores, aunque la verdad es que nada de esto está cercas de la realidad, pues
el mejor framework dependerá de lo que buscas construir y la habilidad que ya
tengas sobre un lenguaje determinado.
Algunas de las propuestas más interesantes para el desarrollo web son, Angular,
Laravel, Vue.JS, Ember.js, Polymer, React.js entre un gran número de etcéteras.
Lo que puede complicar la decisión sobre que lenguaje, librería o framework
debemos utilizar.
Desde luego, en este libro no tratamos de convencerte de utilizar React, sino más
bien, buscamos enseñarte su potencial para que seas tú mismo quien pueda
tomar esa decisión.
Udemy
Bitbucket
Anypoint (Mule Soft)
Facebook
Courcera
Airbnb
American Express
Atlassian
Docker
Dropbox
Instagram
Reddit
Son solo una parte de una inmensa lista de empresas y páginas que utilizan React
como parte medular de sus desarrollos. Puedes ver la lista completa de páginas
Página | 22
que usan React aquí: https://github.com/facebook/react/wiki/sites-using-react.
Esta lista debería ser una evidencia tangible de que React es sin duda una librería
madura y probada.
23 | Página
Introducción a React
React fue lanzado por primera vez en 2013 por Facebook y es actualmente
mantenido por ellos mismo y la comunidad de código abierto, la cual se extiende
alrededor del mundo. React, a diferencia de muchas tecnologías del desarrollo
web, es una librería, lo que lo hace mucho más fácil de implementar en muchos
desarrollos, ya que se encarga exclusivamente de la interface gráfica del
usuario y consume los datos a través de API que por lo general son REST.
El nombre de React proviene de su capacidad de crear interfaces de usuario
reactivas, la cual es la capacidad de una aplicación para actualizar toda la
interface gráfica en cadena, como si se tratara de una formula en Excel, donde
al cambiar el valor de una celda automáticamente actualiza todas las celdas que
depende del valor actualizado y esto se repite con las celdas que a la vez
dependía de estas últimas. De esta misma forma, React reacciona a los cambios
y actualiza en cascada toda la interface gráfica.
Uno de los datos interesantes de React es que es ejecutado del lado del
cliente (navegador), y no requiere de peticiones GET para cambiar de una
página a otra, pues toda la aplicación es empaquetada en un solo archivo
JavaScript (bundle.js) que es descargado por el cliente cuando entra por primera
vez a la página. De esta forma, la aplicación solo requerirá del backend para
recuperar y actualizar los datos.
React suele ser llamado React.js o ReactJS dado que es una librería de
JavaScript, por lo tanto, el archivo descargable tiene la extensión .js, sin
embargo, el nombre real es simplemente React.
Página | 24
Server Side Apps vs Single Page Apps
Las aplicaciones del lado del servidor, son aquellas en las que el código fuente
de la aplicación está en un servidor y cuando un cliente accede a la aplicación, el
servidor solo le manda el HTML de la página a mostrar en pantalla, de esta
manera, cada vez que el usuario navega hacia una nueva sección de la página,
el navegador lanza una petición GET al servidor y este le regresa la nueva página.
Esto implica que cada vez que el usuario de click en una sección se tendrá que
comunicar con el servidor para que le regresa la nueva página, creado N
solicitudes GET para N cambios de página. En una página del lado del servidor,
cada petición retorna tanto el HTML para mostrar la página, como los datos que
va a mostrar.
Como vemos en la imagen, el cliente lanza un GET para obtener la nueva página,
el servidor tiene que hacer un procesamiento para generar la nueva página y
tiene que ir a la base de datos para obtener la información asociada a la página
de respuesta. La nueva página es enviada al cliente y este solo la muestra en
pantalla. En esta arquitectura todo el trabajo lo hace el servidor y el cliente
solo se limita a mostrar las páginas que el server le envía.
25 | Página
Single page app
Las aplicaciones de una sola página se diferencian de las aplicaciones del lado del
servidor debido a que gran parte del procesamiento y la generación de las
vistas las realiza directamente el cliente (navegador). Por otro lado, el
servidor solo expone un API mediante el cual, la aplicación puede consumir datos
y realizar operaciones transaccionales.
Introducción a NodeJS
NodeJS es sin duda una de las tecnologías que más rápido está creciendo, y que
ya hoy en día es indispensable para cubrir posiciones de trabajo. NodeJS ha sido
revolucionario en todos los aspectos, desde la forma de trabajar hasta que
ejecuta JavaScript del lado del servidor.
Página | 26
NodeJS es básicamente un entorno de ejecución JavaScript del lado del
servidor. Puede sonar extraño, ¿JavaScript del lado del servidor? ¿Pero que no
JavaScript se ejecuta en el navegador del lado del cliente? Como ya lo platicamos,
JavaScript nace inicialmente en la década de los noventas por los desarrolladores
de Netscape, el cual fue creado para resolver problemas simples como validación
de formularios, alertas y alguna que otra pequeña lógica de programación, nada
complicado, sin embargo, JavaScript ha venido evolucionando hasta convertirse
en un lenguaje de programación completo. NodeJS es creado por Ryan Lienhart
Dahl, quien tuvo la gran idea de tomar el motor de JavaScript de Google Chrome
llamado V8, y montarlo como el Core de NodeJS.
27 | Página
NodeJS y la arquitectura de Micro Servicios
Todo esto viene al caso, debido a que NodeJS se ha convertido en unos de los
servidores por excelencia para implementar microservicios, ya que es muy ligero
y puede ser montado en servidores virtuales con muy pocos recursos, algo que
es imposible con servidores de aplicaciones tradiciones como Wildfy, Websphere,
Glashfish, IIS, etc.
Hoy en día es posible rentar un servidor virtual por 5 USD al mes con 1GB de
RAM y montar una aplicación con NodeJS, algo realmente increíble y es por eso
mi insistencia en que NodeJS es una de las tecnologías más prometedoras
actualmente. Por ejemplo, yo suelo utilizar Digital Ocean, pues me permite rentar
servidores desde 5 usd al mes.
Página | 28
Introducción a MongoDB
Uno de los principales retos al trabajar con MongoDB es entender cómo funciona
el paradigma NoSQL y abrir la mente para dejar a un lado las tablas y las
columnas para pasar un nuevo modelo de datos de Colecciones y documentos,
los cuales no son más que estructuras de datos en formato JSON.
Es probable que hallas notado que dije que MongoDB no soporta transacciones,
y es verdad, pero eso no significa que MongoDB no sirva, si no que no fue
diseñado para aplicaciones que requieren de transacciones y bloque de registro,
un que es posible simularlos con un poco de programación. Más adelante
analizaremos más a detalle estos puntos.
29 | Página
toda la información, pues un documento por sí solo, contiene toda la información
requerida
1. { 6. {
2. "name": "Juan Perez", 7. "name": "Juan Perez",
3. "age": 20, 8. "age": 20,
4. "tel": "1234567890" 9. "tels": [
5. } 10. "1234567890",
11. "0987654321"
12. ]
13. }
Observemos que los dos objetos son relativamente similares, y tiene los mismos
campos, sin embargo, uno tiene un campo para el teléfono, mientras que el
segundo objeto, tiene una lista de teléfonos. Es evidente que aquí tenemos dos
incongruencias con un modelo de bases de datos relacional, el primero, es que
tenemos campos diferentes para el teléfono, ya que uno se llama tel y el otro
tels. La segunda incongruencia, es que la propiedad tels, del segundo objeto,
es en realidad un arreglo, lo cual en una DB relacional, sería una tabla secundaria
unida con un Foreign Key.
Página | 30
Hasta este momento, ya hemos hablado de estas tres tecnologías de forma
individual, sin embargo, no hemos analizado como es que estas se combinan
para crear proyectos profesionales.
Como vemos en la imagen, React está del lado del FrontEnd, lo que significa que
su único rol es la representación de los datos y la apariencia gráfica. En el
BackEnd tenemos a NodeJS, quien es el intermediario entre React y MongoDB.
MongoDB también está en el Backend, pero este no suele ser accedido de forma
directa por temas de seguridad.
Resumen
31 | Página
También hemos analizado de forma rápida a React, NodeJS y MongoDB para
dejar claro los conceptos y como es que estas 3 tecnologías se acoplan para dar
soluciones de software robustas.
Página | 32
Preparando el ambiente de
desarrollo
Capítulo 2
Cabe mencionar que Atom, no es un IDE como tal, sino más bien, un editor de
texto, lo cual lo hace una herramienta robusta pero no tan sofisticada como un
IDE de programación completo, como sería Webstorm, Eclipse o Visual Studio.
En este libro nos hemos inclinado por Atom debido a que es una herramienta
ampliamente utilizada y es open source, lo que te permitirá descargarlo e
instalarlos sin pagar una licencia. Sin embargo, si tú te sientes cómodo en otro
editor o IDE, eres libre de utilizarlo.
Instalación de Atom
Instalar Atom es muy simple, tan solo es necesario seguir los clásicos pasos de
siguiente, siguiente y finalizar, por lo que no te aburriré haciendo un tutorial de
como instalarlo.
33 | Página
En las siguientes ligas encontrarás el procedimiento de instalación actualizado
por sistema operativo:
Windows: http://flight-manual.atom.io/getting-
started/sections/installing-atom/#platform-windows
Mac: http://flight-manual.atom.io/getting-started/sections/installing-
atom/#platform-mac
Linux: http://flight-manual.atom.io/getting-started/sections/installing-
atom/#platform-linux
Instalar PlugIns
Página | 34
Ya en este punto, solo procederemos a instalar algunos plugins interesantes. Para
esto, nos dirigiremos al menú superior y presionaremos en File > Settings, una
vez allí, veremos una pestaña como la siguiente.
Si todo salió bien, deberás de poder ver los plugins de la siguiente manera:
Estos son los dos plugins que, recomiendo para empezar, pero puedes navegar
un poco para ver toda la gran lista de plugins disponibles, y la gran mayoría
escritos por contribuidores de código libre.
35 | Página
Existen otros plugins interesantes para el desarrollo, que, si bien no los
aprovecharemos en este libro, si te que recomiendo que los análisis, estos son:
Finalmente, puedes aprovechar que estas en settings para ver los temas
disponibles, incluso descargar algunos de la comunidad.
Instalar NodeJS es también realmente simple, pues tan solo será necesario
descargarlo de su página oficial https://nodejs.org, asegurándonos de tomar la
versión correcta para nuestro sistema operativo. En la siguiente página
encontraras la guía de instalación para los diferentes sistemas operativos:
https://nodejs.org/es/download/package-manager/
Página | 36
Fig. 11 - Comprobando la instalación de NodeJS.
Esto será todo lo que tendremos que hacer por el momento. Más adelante
veremos cómo ejecutarlo y descargar módulos con NPM.
Instalando MongoDB
Lo primero que deberemos hacer será crea una cuenta en MongoDB Atlas, al
entrar verás una pantalla como la siguiente:
Presionaremos el botón que dice “Get started free” para iniciar el registro. Nos
llevará a un pequeño formulario en donde tendremos que capturar los datos que
nos solicite.
37 | Página
Fig. 13 - Crear una base de datos de MongoDB.
Una vez que finalicemos este paso, nos mandará nuevamente al dashboard, en
donde vemos todas las bases de datos. Normalmente el proceso de creación de
la base de datos tarde unos minutos, por lo que desde esta pantalla podrás ver
el avance. Cuando el proceso termine, podremos apreciar nuestra base de datos
operativa:
Página | 38
Fig. 15 - Cluster de MongoDB creado.
NOTA: Si ya habías entrado antes una base de datos y no te sale esta pantalla,
busca el botón que dice Build a New Cluster.
Una vez allí, daremos click en el botón que dice “+ ADD IP ADDRESS” y saldrá
una pantalla como la siguiente:
39 | Página
Fig. 17 - Autorizar todas las IP’s
En esta sección, podemos determinar desde que IP’s nos podemos conectar, lo
cual es muy importante en ambiente productivo, sin embargo, dado que estamos
desde una PC con IP cambiante, debemos definir que nos permite todas las IP’s
mediante el botón “ALLOW ACCESS FROM ANYWHERE” y presionamos el
botón de guardar.
Te recomiendo te des un tour por la aplicación y que conozcas cada detalle que
nos ofrece. Entre las características más interesantes son las métricas, pues no
dice el número de transacciones, conexiones activas, número de operaciones,
ancho de banda utilizado, etc. También cuenta con alertas, Respaldos, etc.
Desafortunadamente algunas características son de pago, pero con las cosas que
nos da free es más que suficiente para empezar.
https://docs.mongodb.com/manual/administration/install-community/
Una vez que la base de datos esta activa y funcionando, nos queda solo un paso,
instalar un cliente para conectarnos a la base de datos de MongoDB.
Página | 40
En el mercado existen varios productos que nos permiten conectarnos a una base
de datos Mongo, como lo son:
Compass
Mongobooster
Estudio 3T
Robomongo
MongoVue
MongoHub
RockMongo
Y seguramente hay más. hay algunos muy simples, otros muy completos, hay
gratis otros de paga, es cuestión de que te des una vuelta a la página de cada
uno para que veas qué características tiene.
En la práctica, a mí me gusta mucho Estudio 3T, pues es uno de los más robustos
y completos, pero tiene el inconveniente que es de paga, por esa razón, nos
iremos por la segunda mejor opción que es Compass y que además es Open
Source.
41 | Página
Fig. 19 - Seleccionado versión de MongoDB Compass.
Página | 42
Fig. 20 - Conexión exitosa a MongoDB.
Del lado derecho izquierdo podemos ver las opciones “admin”, “local” y “test”,
de esas tres, la que nos interesa es “test”, sin embargo, como todavía no vamos
a hacer nada con ella. Más adelante a medida que vallamos trabajando con el
proyecto Mini-Twitter, vamos a ir viendo cómo se van creando las colecciones (lo
que conocemos como tablas en SQL).
En este punto ya nos deberíamos sentir muy orgullosos, pues ya hemos creado
un clúster en la nube y nos hemos logro conectar.
Estas es sin duda la sección más esperada para todos, pues por fin empezaremos
a programar con React, puede que de momento hagamos algo muy simple, pero
a medida que avancemos en el libro iremos avanzando en un proyecto final, el
cual, contemplará todo el conocimiento de este libro. Entonces, sin más
preámbulos, comencemos.
43 | Página
Puede que resulte obvio que la mejor forma es mediante las utilerías, pero como
en este punto queremos aprender a utilizar React, entonces tendremos que
iniciar de la forma difícil, es decir, crea a mano cada archivo del proyecto.
Los que viene de trabajar de entornos de IDE’s, es muy probable que estén
acostumbrados a crear proyectos mediante Wizzards, los cuales ya nos crean
todo el proyecto y sus archivos, pero en esta sección aprenderemos a crearlo de
forma manual.
Lo primero que tendremos que hace es crear una carpeta sobre la que estaremos
trabajando, puede estar en cualquier dirección del disco duro, en este caso,
crearemos una carpeta llamada TwitterApp y nos ubicaremos en esta carpeta por
medio de la consola:
Página | 44
name: nombre del proyecto, no permite camel case, por lo que tendrá
que ser todo en minúsculas. En este caso ponemos twitter-app.
version: versión actual del proyecto, por default es 1.0.0, por lo que
solo presionamos enter sin escribir nada.
description: una breve descripción del proyecto, en nuestro caso
podemos poner Aplicación de redes sociales, o cualquiera otra
descripción, al final, es meramente descriptiva.
entry point: indica el archivo principal del proyecto, en nuestro caso no
nos servirá de nada, así que presionamos enter para tomar el valore por
default.
test command: nos permite definir un comando de prueba,
generalmente se imprime algo en pantalla para ver que todo anda bien.
En nuestro caso, solo presionamos enter y dejamos el valor por default.
git repository: si nuestro proyecto está asociado a un repositorio de git,
aquí podemos poner la URL, por el momento, solo presionamos enter.
keywords: como su nombre lo dice, palabras clave que describen el
proyecto. Nuevamente tomamos el valor por defecto.
author: permite establecer el nombre del autor del proyecto, puede ser
el nombre de una persona o empresa. En mi caso pongo mi nombre
Oscar Blancarte, pero tú puedes poner tu nombre.
license: se utiliza en proyectos que tienen una determinada licencia,
para prevenir a los que utilicen el código de tu proyecto. En nuestro
caso, dejamos los valores por defecto.
45 | Página
Fig. 22 - Inicialización del proyecto
Una vez finalizado todos los pasos, podremos ver que, en la carpeta del proyecto,
se habrá creado un nuevo archivo llamado package.json, el cual se verá de la
siguiente manera:
1. {
2. "name": "twitter-app",
3. "version": "1.0.0",
4. "description": "Aplicación de redes sociales",
5. "main": "index.js",
6. "scripts": {
7. "test": "echo \"Error: no test specified\" && exit 1"
8. },
9. "author": "Oscar Blancarte",
10. "license": "ISC"
11. }
Página | 46
Index.html
El siguiente paso será crear una página de inicio, la cual por lo general solo
importa un script de JavaScript y una hoja de estilos. Este archivo lo llamaremos
index.html y lo crearemos en la raíz del proyecto, el cual se verá de la siguiente
manera:
1. <!DOCTYPE html>
2. <html>
3. <head>
4. <title>Mini Twitter</title>
5. <link rel="stylesheet" href="/public/resources/css/styles.css">
6. </head>
7. <body>
8. <div id="root"></div>
9. <script type="text/javascript" src="/public/bundle.js"></script>
10. </body>
11. </html>
Un dato curioso de React, es que este será la única página HTML que tendremos
en todo el proyecto, pues como ya lo hablamos, las aplicaciones en React se
empaquetan en un solo archivo JavaScript, el cual denominaremos bundle.js.
Cuando el usuario accede a la página, iniciará la descarga del Script, una vez
descargado se ejecutará y remplazará el div con id=root, por la aplicación
contenida en bundle.js.
webpack.config.js
1. module.exports = {
2. entry: [
3. __dirname + "/app/App.js",
4. ],
5. output: {
6. path: __dirname + "/public",
7. filename: "bundle.js",
8. publicPath: "/public"
9. },
10.
11. module: {
12. loaders: [{
13. test: /\.jsx?$/,
14. exclude: /node_modules/,
15. loader: 'babel-loader',
16. query:{
17. presets: ['env','react']
18. }
19. }]
20. }
21. }
47 | Página
Recuerda que no importa si no entiendas nada en este archivo, más adelante lo
retomaremos en la sección de Webpack.
1. {
2. "name": "twitter-app",
3. "version": "1.0.0",
4. "main": "index.js",
5. "description": "Aplicación de redes sociales",
6. "author": "Oscar Blancarte",
7. "license": "ISC",
8. "devDependencies": {
9. "webpack": "^1.12.*",
10. "webpack-dev-server": "^1.10.*",
11. "babel-preset-react": "^6.24.1"
12. },
13. "dependencies": {
14. "react": "^15.0.0",
15. "react-dom": "^15.0.0",
16. "babel-core": "^6.24.1",
17. "babel-loader": "^6.4.1",
18. "babel-preset-env": "^1.7.0"
19. }
20. }
Una vez aplicados estos últimos cambios, será necesario ejecutar le comando npm
install sobre la carpeta del proyecto para descargar las dependencias.
Página | 48
Instalar actualizaciones
Cada vez que se agregue o modifique una dependencia
del archivo package.json, será necesario instalar los
nuevos módulos mediante el comando npm install, el
cual se deberá ejecutar en la raíz del proyecto. De lo
contrario, los módulos no estarán disponibles en
tiempo de ejecución.
styles.css
Dado que todas las páginas web deben de verse atractivas, es necesario crear al
menos un archivo de estilos, en el cual iremos declarando las clases de estilo que
utilizaremos a lo largo del libro. Para esto, será necesario crear la siguiente
estructura de carpetas iniciando desde la raíz del proyecto.
/public/resources/css, es importante respetar correctamente el path, ya que de
lo contrario, nuestra página no cargara los estilos. Dentro de la carpeta css,
crearemos un archivo llamado styles.css, el cual se verá de la siguiente manera:
1. body{
2. background-color: #F5F8FA;
3. }
Por el momento, solo estableceremos el color de fondo del body en un gris suave,
y más adelante iremos complementando los estilos.
Observemos que el path del archivo de estilos, corresponde con el path definido
en el archivo index.html
App.js
Lo primero que debemos de hacer es crear una carpeta llamada app en la raíz del
proyecto. Dentro de esta carpeta, crearemos el archivo App.js el cual se verá de
la siguiente manera:
49 | Página
6. render(){
7. return(
8. <h1>Hello World</h1>
9. )
10. }
11. }
12. render(<App/>, document.getElementById('root'));
En las primeras líneas del archivo, importamos las librerías de React (Líneas 1 y
2), por un lado, importamos React del módulo ‘react’ y después importamos la
función render del módulo ‘react-dom’.
Lo que sigues, es la declaración de una nueva clase llamada App, la cual extiende
de React.Component. La clase App tiene un método llamado render, el cual es el
encargado de generar la vista del componente, en este caso, está retornando el
elemento <h1>Hello Word</h1>. Finalmente, utilizamos la función render, para
remplazar el elemento root por el nuevo componente.
Ya estamos casi listo para ejecutar nuestra primera aplicación con React, solo
nos queda un paso más. Regresamos al archivo package.json y agregamos la
sección scripts, tal como lo vemos a continuación:
1. {
2. "name": "twitter-app",
3. "version": "1.0.0",
4. "main": "index.js",
5. "description": "Aplicación de redes sociales",
6. "scripts": {
7. "start": "node_modules/.bin/webpack-dev-server --progress"
8. },
9. "author": "Oscar Blancarte",
10. "license": "ISC",
11. "devDependencies": {
12. "webpack": "^1.12.*",
13. "webpack-dev-server": "^1.10.*",
14. "babel-preset-react": "^6.24.1"
15. },
16. "dependencies": {
17. "react": "^15.0.0",
18. "react-dom": "^15.0.0",
19. "babel-core": "^6.24.1",
20. "babel-loader": "^7.0.0",
21. "babel-preset-env": "^1.7.0"
22. }
23. }
La sección Script nos permitirá definir una serie de comandos pre definidos, para
compilar, ejecutar pruebas, empaquetar y desplegar/ejecutar la aplicación. Estos
scripts son ejecutados con ayuda de npm.
Página | 50
Hello Word!!
Si al entrar a la URL puedes ver “Helllo World” quieres decir que has hecho
perfectamente bien todos los pasos, de lo contrario, será necesario que regreses
a los pasos anteriores y análisis donde está el problema. Muchas veces una coma,
un punto o una letra de más puede hacer que no funcione, así que no te
desanimes, ya que es raro que a alguien le salga bien a la primera.
51 | Página
Tras haber realizados todos los pasos, el proyecto debería de quedar con la
siguiente estructura:
Página | 52
Una vez instalado el módulo, nos dirigiremos a la carpeta en donde queremos
crear el proyecto, en mi caso me dirigiré a C:\create-react-app y desde allí
ejecutare el comando “create-react-app twitter-app”. Este comando demorará
un tiempo considerable así que seamos pacientes.
53 | Página
En este caso, la aplicación se iniciará en el puerto 3000, por lo que la URL será
http://localhost:3000/.
Esta es una herramienta que nos ayuda a iniciar un proyecto para React de una
forma muy rápida y simple, pero tiene el inconveniente que no prepara el
proyecto para usar Webpack. En la siguiente sección hablaremos acerca de
Webpack y veremos las ventajas de utilizarlo.
Esta alternativa es muy buena si solo vas a crear una aplicación sin el BackEnd,
pero si ya requerimos utilizar NodeJS + Express, entonces sería bueno crear el
proyecto paso a paso.
En este libro trabajaremos con la estructura del proyecto paso a paso, ya que
necesitaremos más control sobre el proyecto y para eso utilizaremos Webpack.
Página | 54
https://github.com/oscarjb1/books-reactiveprogramming.git
55 | Página
comprimir archivos, también proporcionan utilidades en tiempo de
ejecución para debuger y muchas cosas más.
Como ya lo hablamos, las librerías globales están disponibles para todos los
proyectos si la necesidad de instarlos en cada proyecto.
Librerías globales
Hay que tener cuidado con las librerías que instalamos
de forma global, pues podrían entrar en conflicto con
las librerías locales. Por lo que solo hay que instalar
de forma global las que tienen un motivo justificable.
Página | 56
El tercer tipo de librerías son las de desarrollo, las cuales son utilizadas para la
fase de desarrollo. La importancia de separar estas las librerías de desarrollo, es
no llevárnoslas a un ambiente de producción, pues puede hacer mucho más
pesada la página de lo que debería.
Librerías de desarrollo
Es muy importante identificar que librerías será
utilizadas únicamente durante el desarrollo, pues
exportarlas a producción puede dar problemas de
rendimiento, además de una experiencia desagradable
para el usuario, pues tendrá que descargar un archivo
mucho más grande.
Una vez que ya hemos visto como npm gestiona las dependencias, ha llegado el
momento de retomar el archivo package.json para analizar las dependencias que
ya hemos instalado. Empecemos con las dependencias de desarrollo:
1. "devDependencies": {
2. "webpack": "^1.12.*",
3. "webpack-dev-server": "^1.10.*",
4. "babel-preset-react": "^6.24.1",
5. "babel-core": "^6.24.1",
6. "babel-loader": "^6.4.1",
7. "babel-preset-env": "^1.7.0"
8. }
57 | Página
webpack: esta es la dependencia a Webpack, el cual analizaremos en la
siguiente sección.
webpack-dev-server: dependencia al servidor de desarrollo que
proporciona Webpack, permite compilar todos los archivos y
empaquetarlos para generar el bundle.js.
babel-preset-react: este es un módulo para trabajar con React. El cual
nos ayuda debido a que empaqueta todo lo necesario y no hace falta
buscar las dependencias de forma individual. Este paquete está
orientado a la Transpilación.
babel-core: no hay mucho que decir, solo es la librería principal de
babel, el módulo encargado de la transpilación.
babel-loader: este módulo es para utilizar babel como loader en
Webpack.
babel-preset-env: paquete de librerías para la transpilación de
JavaScript en formato ECMAScript 6 (2015)
1. "dependencies": {
2. "react": "^15.0.0",
3. "react-dom": "^15.0.0"
4. }
Desinstalando librerías
Página | 58
Micro modularización
Algo a tomar en cuenta, es que NodeJS al igual que todos los módulos disponibles
en NPM (incluyendo React) son micro modulares, lo que quiere decir que son
librerías muy pequeñas, diseñadas para realizar una tarea muy específica, a
diferencia de los lenguajes de programación tradiciones, como el JDK de Java o
framework de .NET, los cuales pasan años antes de liberar una nueva versión, y
las versiones suelen tener grandes cambios.
Con NodeJS, cada proyecto es independiente, lo que permite que cada módulo
evolucione a su propio ritmo, lo cual es muy bueno, pues en el caso de Java o
NET, tenemos que esperar años, antes de tener mejoras en el lenguaje o las
librerías proporcionadas.
Esta micromodularización tiene grandes ventajas, pero también puede ser una
trampa para programadores inexpertos, pues la gran mayoría de los
programadores siempre buscan la última versión de un módulo, sin importar que
agrega o que compatibilidad rompe.
Incluso, es muy probable que varios de los módulos que utilizamos en este libro,
liberen nuevas versiones mientras escribimos este libro, pero eso no quiere decir
que estamos desactualizados, ya que muchos de los features que agregan los
módulos, ni siquiera los utilizamos. Es por eso que mi recomendación es siempre
evaluar que nuevas cosas trae un módulo antes de actualizarlo. Como regla, las
versiones que corrigen bug siempre es bueno actualizarlas, las versiones
menores es importante investigar que nuevas cosas tiene y las mayores hay que
tratarlas con mucho cuidado, pues con frecuencia rompen compatibilidad.
Introducción a WebPack
59 | Página
Fig. 31 - Funcionamiento de Webpack
Como puedes ver en la imagen, Webpack tomará todos los archivos de nuestro
proyecto, los compilará, empaquetará, comprimirá y finalmente los minificara,
todo esto sin que nosotros tengamos que hacer prácticamente nada.
Uno de los aspectos más interesantes de Webpack, son los cargadores o loaders,
los cuales nos permite procesar diferentes tipos de archivos y arrojarnos un
resultado, un ejemplo de estos son los procesadores de SASS y LESS, que nos
permite compilar los archivos y arrojarnos CSS, también está el loader Babel,
que permite que el código JavaScript en formato ECMAScript 6 sea compatible
con todos los navegadores. También podemos configurar a Webpack para que
aplique compresiones a las imágenes y minificar el código (compactar).
Instalando Webpack
Instalar Webpack es mucho más simple de lo que creeríamos, pues solo falta
instalar la dependencia con npm como ya lo hemos visto antes.
Página | 60
Este comando habilitará el uso de Webpack para el proyecto en cuestión. Pero
también es posible instalar Webpack a nivel global agregando el parámetro -g
como ya vimos.
1. "scripts": {
2. "start": "node_modules/.bin/webpack-dev-server --progress"
3. }
Este script permite que cuando ejecutemos npm start, inicie una nueva instancia
del server webpack-dev-server, y el parámetro --progress es solo para ver el
avance a medida que compila los archivos.
Sin este módulo, tendríamos que crear un servidor con NodeJS antes de poder
ejecutar un Hello World en React.
El archivo webpack.config.js
1. module.exports = {
2. entry: [
3. __dirname + "/app/App.js",
61 | Página
4. ],
5. output: {
6. path: __dirname + "/public",
7. filename: "bundle.js",
8. publicPath: "/public"
9. },
10.
11. module: {
12. loaders: [{
13. test: /\.jsx?$/,
14. exclude: /node_modules/,
15. loader: 'babel-loader',
16. query:{
17. presets: ['env','react']
18. }
19. }]
20. }
21. }
Página | 62
Nuevo concepto: Transpilación
Hasta este momento, hemos utilizado incorrectamente
la palabra compilar, para referirnos al proceso por el
cual, convertimos los archivos de React a JavaScript
puro, sin embargo, el proceso por el cual se lleva a
cabo esto es Transpilación, que no es precisamente
una compilación, si no la conversión del código de
React a JavaScript compatible con todos los
navegadores.
Webpack puede parecer simple, pero es mucho más complejo de lo que parece,
tiene una gran cantidad de plugins y configuraciones que pueden ser requeridas
en cualquier momento. Puedes darle una revisada a la documentación oficial para
que te des una idea: https://webpack.github.io/docs/
Una vez instalado probablemente tengas que reiniciar el navegador. Una vez
hecho esto, deberá aparecer el ícono de React a un lado de la barra de búsqueda.
Por default este ícono se ve gris, lo cual indica que la página en la que estamos,
no utiliza React. Para probar el funcionamiento del plugin, nos iremos a Facebook
y veremos que el ícono pasa a tener color. Esto nos indica que la página utiliza
React y es posible debugearla con el plugin.
63 | Página
Una vez que estemos en Facebook, daremos click derecho en cualquier parte de
la página y presionaremos la opción Inspeccionar. Una vez allí, nos vamos al tab
React:
Desde esta sección, es posible ver los componentes React y saber en tiempo real,
el valor de su estado y propiedades. Por ahora no entraremos en los detalles,
pues primero necesitamos aprender los conceptos básicos como estados y
propiedades, antes de poder entender lo que nos arroja el plugin. Más adelante
retomaremos el plugin para analizar las páginas.
Página | 64
Resumen
Hemos concluido uno de los capítulos más complicados, pues nos tuvimos que
enfrentar a varias tecnologías, aprendimos nuevos conceptos y echamos a andar
nuestra primera aplicación con React, lo cual es un enorme avance.
Hasta este punto hemos aprendido a instalar React, NodeJS, y configurar una
base de datos Mongo en la nube, también aprendimos a gestionar dependencias
con npm, para finalmente introducirnos en Webpack.
65 | Página
Introducción al desarrollo con
React
Capítulo 3
Si recordamos nuestra clase App.js, teníamos una función llamada render, la cual
tiene como finalidad crear una vista, esta función debe de retornar la página que
finalmente el usuario verá en pantalla. Veamos la función para recordar:
1. render(){
2. return(
3. <h1>Hello World</h1>
4. )
5. }
Observemos que la función regresa una etiqueta <h1>, la cual corresponde con la
etiqueta <h1> que podemos ver en el navegador:
Página | 66
Fig. 34 - Inspector de elementos de Google <h1>
Inspector de elementos
Todos los navegadores modernos nos permiten
inspeccionar una página para ver los elementos que la
componen. En el caso de Chrome, solo requieres dar
click derecho sobre la página y presionar
Inspeccionar.
Dado que JSX y HTML pueden ser muy similares, es muy fácil confundirnos y no
entender cuáles son sus diferencias, provocando errores de compilación o incluso
en tiempo de ejecución. Vamos a analizar las diferencias que existen entre JSX,
HTML y XML
67 | Página
Elementos balanceados
Notemos que no tiene una etiqueta de cierre </img> ni termina en />, esto sería
totalmente válido en HTML, sin embargo, en JSX no lo es, ya que JSX utiliza las
reglas de XML, por lo que todos los elementos deben de cerrarse, incluso si en
HTML no es requerido.
1. render(){
2. return(
3. <img src="/images/img1.png" alt="mi imagen">
4. )
5. }
Guardemos los cambios y veremos que Webpack detectará los cambios y tratará
de transpilar los cambios, dando un error en el proceso:
Página | 68
Para corregir este error, es necesario cerrar el elemento, y lo podemos hacer de
dos formas:
1. <img src="https://facebook.github.io/react/img/logo.svg"></img>
Puedes utilizar el método que más te agrade, al final el resultado será el mismo.
Una de las principales reglas que tiene JSX es que solo podemos regresar un solo
elemento raíz. Esto quiere decir que no podemos retornas dos o más elementos
a nivel de la función return. Por ejemplo, en el componente App.js eliminamos
el <h1> para agregar una etiqueta <img>, pero ¿qué pasaría si quiero retornar las
dos al mismo tiempo?, bueno podría hacer lo siguiente:
1. render(){
2. return(
3. <h1>Hello World</h1>
4. <img src="https://facebook.github.io/react/img/logo.svg"></img>
5. )
6. }
Observemos que tanto <h1> como <img> están declarados al mismo nivel, lo que
quiere decir que tenemos dos elementos raíz, lo que es inválido para JSX.
Guardemos los cambios ver qué sucede:
69 | Página
Como podemos ver, nuevamente sale un error al transpilar el archivo. ¿Esto
quieres decir que tengo que crear una clase para cada elemento?, la respuesta
es no, tan solo es necesario encapsular los dos elementos dentro de otro, como
podría ser un <div>. veamos cómo quedaría:
1. render(){
2. return(
3. <div>
4. <h1>Hello World</h1>
5. <img src="https://facebook.github.io/react/img/logo.svg"/>
6. </div>
7. )
8. }
Esta nueva estructura ya cumple con la regla de un elemento raíz único, en donde
el <div> sería el elemento raíz. Dentro del <div> ya es posible incluir cualquier
tipo de estructura sin restricciones.
Fragments
Página | 70
Como acabamos de ver, mediante JSX solo podemos regresar un elemento raíz,
sin embargo, existe ocasiones en las que es necesario retornar más de un solo
elemento sin tener un elemento padre, para esto React incorpora los Fragments,
los cuales son elementos dummy, lo que quiere decir que nos permite agregarlos
como padre de una serie de elementos, pero al momento de realizar el render
del componente, son ignorados.
1. <html>
2. <head>
3. <title>Mini Twitter</title>
4. <link rel="stylesheet" href="/public/resources/css/styles.css">
5. </head>
6. <body>
7. <div id="root">
8. <h1>Hello World</h1>
9. <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-
icon.svg/210px-React-icon.svg.png">
10. </div>
11. <script type="text/javascript" src="/public/bundle.js"></script>
12. </body>
13. </html>
Puedes observar que los elementos <h1> y <img> quedaron al mismo nivel dentro
del elementos root, pero ya no tiene un div adicional que los encapsule.
71 | Página
Camel Case en atributos
Otra de las características de JSX es que los atributos deben ser escritos en Camel
Case, esto quiere decir que debemos utilizar MAYUSCULAS entre cada palabra.
Un ejemplo muy claro de esto, es el evento onclick que tiene todos los elementos
de HTML. En JSX onclick no es correcto, por lo que tendría que escribirse como
onClick, notemos que tenemos la C mayúscula. Veamos qué pasa si poner de
forma incorrecta el atributo:
1. return(
2. <div>
3. <h1>Hello World</h1>
4. <img src="https://facebook.github.io/react/img/logo.svg"/>
5. <br/>
6. <button onclick={()=>alert('Hello World')}>Hello!!</button>
7. </div>
8. )
Página | 72
Adicional a esto, si presionamos el botón, no pasará nada, pues React no sabrá
qué hacer con eso.
Contenido dinámico
Lo primero que debemos de saber, es que React permite definir valores dinámicos
en prácticamente cualquier lugar. Estos valores pueden ser una variable, el
resultado de un servicio o incluso parámetros enviados a los componentes. Para
ellos es necesario agregar los valores en entre un par de llaves { }, tal como lo
vimos hace un momento con el evento onClick.
Veamos que la alerta está dentro de unas llaves, de lo contrario, React no sabrá
cómo interpretar el valor introducido.
73 | Página
Variables
1. render(){
2. let variable = {
3. message: "Hello World desde una variable"
4. }
5.
6. return(
7. <div>
8. <h1>{variable.message}</h1>
9. <img src="https://facebook.github.io/react/img/logo.svg"/>
10. <br/>
11. <button onClick={()=>alert('Hello World')}>Hello!!</button>
12. </div>
13. )
14. }
En este ejemplo, hemos definido una variable (línea 2) y luego la hemos utilizado
dentro de un bloque {}. Esto hace que React no interprete como código.
Valores condicionales
Veamos un ejemplo muy simple, supongamos que debo saludar al usuario según
su sexo. Por lo que antes de mostrar un mensaje, deberá validar el sexo. Para
hacer esta prueba, vamos a retomar el ejemplo pasado, donde saludamos con
una variable, pero agregaremos datos adicionales para saludar con un mensaje
diferente según el sexo.
Página | 74
La primera forma, es mediante expresiones ternarias:
1. render(){
2. let variable = {
3. sexo: "woman",
4. man: "Hola Amigo",
5. woman: "Hola Amiga"
6. }
7. return(
8. <div>
9. <h1>{variable.sexo === 'man' ? variable.man : variable.woman}</h1>
10. <img src="https://facebook.github.io/react/img/logo.svg"/>
11. <br/>
12. <button onClick={()=>alert('Hello World')}>Hello!!</button>
13. </div>
14. )
15. }
Este método es bastante efectivo cuando solo tenemos dos posibles valores, pues
las expresiones ternarias nos regresan dos valores posibles. Pero qué pasa
cuando existen más de 2 posibles resultados, una sería anidar expresiones
ternarías, lo cual es posible, pero se crearía un código muy complicado y verboso.
La otra opción es trabajar la condición por fuera, de esta forma, podemos realizar
todas las validaciones que requiramos y al final solo imprimimos el resultado por
medio de una variable.
75 | Página
1. render(){
2. let variable = {
3. sexo: "",
4. man: "Hola Amigo",
5. woman: "Hola Amiga",
6. other: "Hola Amig@"
7. }
8. let message = null
9. if(variable.sexo === 'man'){
10. message = variable.man
11. }else if(variable.sexo === 'woman'){
12. message = variable.woman
13. }else{
14. message = variable.other
15. }
16. return(
17. <div>
18. <h1>{message}</h1>
19. <img src="https://facebook.github.io/react/img/logo.svg"/>
20. <br/>
21. <button onClick={()=>alert('Hello World')}>Hello!!</button>
22. </div>
23. )
24. }
Primero que nada, veamos que el sexo está en blanco, también que hemos
agregado una nueva propiedad other, la cual utilizaremos para saludar si no
conocemos el sexo del usuario. Por otra parte, observa la secuencia de
if..elseif..else, en ella, validamos el sexo del empleado, y según el sexo,
escribimos un valor diferente en la variable message. Finalmente, en el <h1> solo
imprimimos el valor de la variable message.
Instalar jsx-control-statements
Primero que nada, será necesario instalar el módulo mediante npm con el
siguiente comando:
npm install -D --save jsx-control-statements
Página | 76
Una vez terminada la instalación, se nos agregará la dependencia en el archivo
package.json en la sección de librerías de desarrollo.
1. module.exports = {
2. entry: [
3. __dirname + "/app/App.js",
4. ],
5. output: {
6. path: __dirname + "/public",
7. filename: "bundle.js",
8. publicPath: "/public"
9. },
10.
11. module: {
12. loaders: [{
13. test: /\.jsx?$/,
14. exclude: /node_modules/,
15. loader: 'babel-loader',
16. query:{
17. presets: ['env','react'],
18. plugins: ["jsx-control-statements"]
19. }
20. }]
21. }
22. };
Solo hemos agregado el plugin en la línea 18, con esto Webpack extenderá el
lenguaje de JSX para soportar un set de estructuras de control, las cuales
analizaremos a continuación.
If
Anteriormente vimos cómo era necesario crear una expresión ternaria o crear
una estructura de if…else para saludar a nuestro usuario según el sexo, pero
ahora con el plugin jsx-control-statements el lenguaje se ha ampliado,
permitiéndonos crear la etiqueta <If>, la cual solo tiene un atributo llamado
condition, que deberá tener una expresión que se resuelva en booleano. Si la
expresión es true, entonces todo lo que este dentro de la etiqueta se mostrará.
Veamos cómo quedaría el ejemplo anterior:
1. render(){
2. let variable = {
3. sexo: "woman",
77 | Página
4. man: "Hola Amigo",
5. woman: "Hola Amiga",
6. other: "Hola Amig@"
7. }
8.
9. return(
10. <div>
11. <img src="https://facebook.github.io/react/img/logo.svg"/>
12. <br/>
13. <button onClick={()=>alert('Hello World')}>Hello!!</button>
14. <If condition={variable.sexo === 'man' }>
15. <h1>{variable.man}</h1>
16. </If>
17. <If condition={variable.sexo === 'woman' }>
18. <h1>{variable.woman}</h1>
19. </If>
20. </div>
21. )
22. }
Veamos que esta vez en lugar de crear una variable y luego asignarle el valor
mediante una serie de if…elseif…else, lo hacemos directamente sobre el JSX.
Un dato importante de <If> es que no nos permite poner <else> o <elseif>, por
lo que solo nos sirve cuando tenemos una expresión a evaluar.
Choose
La estructura de control Choose, nos permite crear una serie de condiciones que
se evalúan una tras otra, permitiendo tener un caso por default, exactamente lo
mismo que hacer un if…elseif…else.
1. render(){
2. let variable = {
3. sexo: "",
4. man: "Hola Amigo",
5. woman: "Hola Amiga",
6. other: "Hola Amig@"
7. }
8. return(
9. <div>
10. <img src="https://facebook.github.io/react/img/logo.svg"/>
11. <br/>
12. <button onClick={()=>alert('Hello World')}>Hello!!</button>
13. <Choose>
14. <When condition={variable.sexo === 'man' }>
15. <h1>{variable.man}</h1>
16. </When>
17. <When condition={variable.sexo === 'woman'}>
18. <h1>{variable.woman}</h1>
19. </When>
20. <Otherwise>
21. <h1>{variable.other}</h1>
22. </Otherwise>
Página | 78
23. </Choose>
24. </div>
25. )
26. }
Observemos que Choose, permite definir una serie de <When>, donde cada una
tendrá una condición a evaluarse, si la condición de un <When> se cumple,
entonces su contenido se mostrará. En caso de que ningún <When> se cumpla, se
mostrará el valor que hay en <Otherwise> el cual no requiere de ningún atributo,
pues sería el valor por default.
For
La etiqueta <For> nos permite iterar una array con la finalidad de arrojar un
resultado para cada elemento de la colección. Hasta el momento no hemos visto
como imprimir un arreglo, por lo que iniciaremos con un ejemplo sin utilizar la
etiqueta <For> para poder comparar los resultados.
Lo que haremos será imprimir una lista de usuarios que tenemos en un array sin
utilizar jsx-control-statements:
1. render(){
2. let usuarios = [
3. 'Oscar Blancarte',
4. 'Juan Perez',
5. 'Manuel Juarez',
6. 'Juan Castro'
7. ]
8.
9. let userList = usuarios.map(user => {
10. return (<li>{user}</li>)
11. })
12.
13. return(
14. <div>
15. <ul>
16. {userList}
17. </ul>
18. </div>
19. )
20. }
Primero que nada, veamos la lista de usuarios (línea 2), la cual tiene 4 nombres,
seguido, lo que hacemos es iterar el array mediante el método map, esto nos
permitirá obtener el nombre individual de cada usuario en la variable user,
seguido, regresamos el nombre del usuario dentro de un tag <li> para
mostrarlos en una lista. Finalmente, en la respuesta del método render,
retornamos la lista de usuarios dentro de una lista <lu>.
79 | Página
1. render(){
2. let usuarios = [
3. 'Oscar Blancarte',
4. 'Juan Perez',
5. 'Manuel Juarez',
6. 'Juan Castro'
7. ]
8.
9. return(
10. <div>
11. <For each="user" index="index" of={ usuarios }>
12. <li>{user}</li>
13. </For>
14. </div>
15. )
16. }
En este ejemplo se pueden apreciar mucho mejor los beneficios, pues hemos
eliminado la necesidad de una variable secundaria.
Transpilación
Para que el navegador sea capaz de entender nuestro código escrito en JSX es
necesario pasarlo por el proceso de transpilación, este proceso lo hace un
módulo llamado Babel, el cual toma los archivos en JSX y los convierte en
JavaScript nativo, para que, de esta forma, el navegador pueda interpretarlo.
Página | 80
1. loaders: [{
2. test: /\.jsx?$/,
3. exclude: /node_modules/,
4. loader: 'babel-loader',
5. query:{
6. presets: ['env','react'],
7. plugins: ["jsx-control-statements"]
8. }
9. }]
Esto ha sido una corta explicación acerca de lo que es Babel, pues era importante
entender que, es Babel y no Webpack, el que transpila los archivos. Pero es a
través de los loaders que Webpack que se puede automatizar el proceso de
transpilación por medio de Babel.
Aunque JSX cubre casi todas las necesidades para crear componentes, existen
ocasiones, en las que es necesario crear elementos mediante JavaScript puro.
No es muy normal ver aplicación que utilicen esta forma de programar, pero
pueden existir ocasiones que lo ameriten.
Veamos cómo quedaría la clase App editándola para usar JavaScript Nativo en
lugar de JSX:
81 | Página
7. let helloWorld = React.createElement('h1',null,'Hello World')
8. let img = React.createElement('img',
9. {src:'https://facebook.github.io/react/img/logo.svg'}, null)
10. let div = React.createElement('div',null,[helloWorld,img])
11. return div
12. }
13. }
14. render(<App/>, document.getElementById('root'));
Primero que nada, veamos que estamos creando un <h1> (línea 7), el primer
parámetro es el tipo de elemento (h1), el segundo parámetro es null, pues no
tiene ninguna propiedad, y como tercer parámetro, le mandamos el texto ‘Hello
World’.
Element Factorys
Como ya vimos, crear elementos con JavaScript es mucho más fácil de lo que
parece, pero por suerte, es posible crear los elementos de una forma mucho más
fácil mediante los Element Factory.
Los Element Factory, son utilidades que ya trae React para facilitarnos la creación
de elementos de HTML, y utilizarlos es tan fácil como hacer lo siguiente:
React.DOM.<element>
Página | 82
El ejemplo es bastante parecido al anterior, solo que cambiamos la función
createElement por los Element Factory. Otra diferencia, es que ya no requiere el
tipo de elemento, pues ya viene implícito.
Como ya lo platicamos, los Element Factory solo sirve para etiquetas HTML que
existen, por lo que cuando queremos utilizar un Element Factory para un
componente personalizado como sería App, no sería posible, es por este motivo
que existe los Element Factory Personalizados.
83 | Página
Resumen
En este capítulo hemos aprendido los más esencial e importante del trabajo con
React, pues hemos aprendido a utilizar el lenguaje JSX que nos servirá durante
todo el libro.
Página | 84
Introducción a los Componentes
Capítulo 4
-- mozilla.org
85 | Página
realidad es que todavía está en una fase experimental
o en desarrollo.
Como vimos, los Web Componentes son pequeños widgets que podemos
simplemente ir añadiendo a nuestra página con tan solo importarlos y no requiere
de programación.
Puede que esta imagen no impresione mucho, pues todas las tecnologías Web
nos permiten crear archivos separados y luego simplemente incluirlos o
importarlos en nuestra página, pero existe una diferencia fundamental, los Web
componentes viven del lado del cliente y no del servidor. Además, en las
tecnologías tradicionales, el servidor no envía al navegador un Web Component,
por ejemplo, un tag <App>, si no que más bien hace la traducción del archivo
incluido a HTML, por lo que al final, lo que recibe el navegador es HTML puro.
Con los Web Componentes pasa algo diferente, pues el navegador si conoce de
Web Components y es posible enviarle un tag personalizado como <App>.
En React, si bien los componentes no son como tal Web Components, es posible
simular su comportamiento, ya que es posible crear etiquetas personalizadas que
simplemente utilizamos en conjunto con etiquetas HTML, sin embargo, React no
regresa al navegador las etiquetas custom, si no que las traduce a HTML para
que el navegador las pueda interpretar, con la gran diferencia que esto lo hace
del lado del cliente.
Página | 86
Fig. 43 - React Components Transpilation
87 | Página
Componentes sin estado
Este tipo de componentes son los más simples, pues solo se utilizan para
representar la información que les llega como parámetros. En algunas ocasiones,
estos componentes pueden transformar la información con el único objetivo de
mostrarla en un formato más amigable, sin embargo, estos compontes no
consumen datos de fuentes externas ni modifican la información que se les envía.
Este componte utiliza algo que hasta ahora no habíamos utilizado, las Props, las
cuales son parámetros que son enviados durante la creación del componente. En
este caso, se le envía una propiedad llamada product, la cual debe de tener un
nombre (name) y un precio (price).
Página | 88
Observemos que el componente ItemList solo muestra las propiedades que
recibe como parámetro, sin realizar ninguna actualización sobre ella.
Para completar este ejemplo, modificaremos el componente App para que quede
de la siguiente manera:
Veamos que hemos creado un array de ítems (línea 8), los cuales cuentan con
un nombre y un precio. Seguido, iteramos los ítems (línea 18) para crear un
componente <ItemList> por cada ítem de la lista, también le mandamos los datos
del producto mediante la propiedad product, la cual podrá ser accedida por el
componente <ItemList> utilizando la instrucción this.props.product.
89 | Página
Fig. 45 - Componentes sin estado.
Los componentes con estado se distinguen de los anteriores, debido a que estos
tienen un estado asociado al componente, el cual manipulan a mediad que el
usuario interactúa con la aplicación. Este tipo de componentes en ocasiones
consumen servicios externos para recuperar o modificar la información.
Página | 90
28. <br/>
29. <label htmlFor='lastName'>Apellido</label>
30. <input id='lastName' type='text' value={this.state.lastName}
31. onChange={this.handleChanges.bind(this)}/>
32. <br/>
33. <label htmlFor='age'>Edad</label>
34. <input id='age' type='number' value={this.state.age}
35. onChange={this.handleChanges.bind(this)}/>
36. </form>
37. )
38. }
39. }
40.
41. render(<App/>, document.getElementById('root'));
Primero que nada, vemos que en la línea 9 se establece el estado inicial del
componente, el cual tiene un firstName (nombre), lastName (apellido) y ege
(edad). Los cuales están inicialmente en blanco.
Seguramente al ver esta imagen, no quede muy claro que está pasando con el
estado, es por eso que utilizaremos el plugin React Developer Tools que
instalamos en el segundo capítulo para analizar mejor como es que el estado se
actualiza. Para esto, nos iremos al inspector, luego seleccionaremos el Tab React:
91 | Página
Fig. 47 - Inspeccionando un componente con estado
Una vez en el tab React, seleccionamos el tag <App> y del lado izquierdo
veremos las propiedades y el estado. Con el inspector abierto, actualiza los
campos de texto y verás cómo el estado también cambia.
Jerarquía de componentes
Página | 92
Fig. 48 - Jerarquía de componentes
En la imagen anterior que se puede ver claramente como una aplicación completa
es creada a partir de otros compontes.
Para crear los paths, solo debemos seguir las reglas de siempre cuando
trabajamos con JavaScript.
93 | Página
Referenciar correctamente el componente
Un error común cuando empezamos a programar en
React, es querer llamar al componente por el nombre
de la clase, sin embargo, el nombre del componente
será el que le pongamos en la instrucción export
default. Por lo que se aconseja que siempre lo
exportemos con el mismo nombre de la clase.
Propiedades (Props)
Las propiedades son la forma que tiene React para pasar parámetros de un
componente padre a los hijos. Es normal que un componente pase datos a
los componentes hijos, sin embargo, no es lo único que se puede pasar, si no
que existe ocasiones en las que los padres mandar funciones a los hijos, para
que estos ejecuten operaciones de los padres, puede sonar extraño, pero ya
veremos cómo funciona.
1. <ItemList product={item}/>
Página | 94
La única diferencia entre estos dos métodos será la forma de recuperar las
propiedades. Ya habíamos hablado que para recuperar una propiedad es
necesario usar el prefijo, this.props, por lo que en el primer ejemplo, el ítem se
recupera como this.props.product, y en el segundo ejemplo, sería
this.props.productName para el nombre y this.props.productPrice para el
precio.
95 | Página
29. onChange={this.handleChanges.bind(this)}/>
30. <br/>
31. <label htmlFor='lastName'>Apellido</label>
32. <input id='lastName' type='text' value={this.state.lastName}
33. onChange={this.handleChanges.bind(this)}/>
34. <br/>
35. <label htmlFor='age'>Edad</label>
36. <input id='age' type='number' value={this.state.age}
37. onChange={this.handleChanges.bind(this)}/>
38. <br/>
39. <button onClick={this.saveEmployee.bind(this)}>Guardar</button>
40. </form>
41. )
42. }
43. }
44. export default EmployeeForm
Este componente es casi idéntico al primer formulario que creamos, pero hemos
agregado dos cosas, lo primero es que en la línea 39 agregamos un botón, el
cual, al ser presionado, ejecutar la función saveEmployee declarada en este mismo
componente, el segundo cambios, la función saveEmployee que declaramos en la
línea 20, el cual lo único que hace es ejecutar la función save enviada por el
parent como prop.
Por otra parte, tenemos al componente App que será el padre del componente
anterior:
Página | 96
Fig. 49 - Funciones como props
Binding functions
Cuando es necesario bindear una función con una prop
o queremos utilizar una función en un evento como
onClick, onChange, etc. es necesario siempre empezar
con this, y finalizar con binding(this) como se ve a
continuación this.<function>.bind(this).
PropTypes
Debido a que los componentes no tienen el control sobre las props que se le
envía, y el tipo de datos, React proporciona un mecanismo que nos ayuda a
validar este tipo de aspectos. Mediante PropTypes es posible definir las
propiedades que debe de recibir un componente, el tipo de datos, estructura e
incluso si son requeridas o no. Definir los PropTypes es tan simple cómo:
1. <component>.propTypes = {
2. <propName>: <propType>
3. ...
4. }
Donde:
97 | Página
1. import React from 'react'
2. import PropTypes from 'prop-types'
3.
4. class ItemList extends React.Component{
5.
6. constructor(props){
7. super(props)
8. }
9.
10. render(){
11. return(
12. <li>{this.props.product.name} - {this.props.product.price}</li>
13. )
14. }
15. }
16.
17. ItemList.propTypes = {
18. product: PropTypes.shape({
19. name: PropTypes.string.isRequired,
20. price: PropTypes.number.isRequired
21. }).isRequired
22. }
23.
24. export default ItemList
isRequired es opcional
Si marcamos una propiedad como isRequired, React
validará que el campo haya sido enviado durante la
creación del componente, sin embargo, si no lo pones,
le indicamos que es un parámetro esperado, pero no
es obligatorio.
Por otra parte, el componente App debe de mandar la propiedad product con la
estructura exacta que se está solicitando, respetando los nombre y los tipos de
datos.
Página | 98
Fig. 50 - Probando los shape propsTypes
Validaciones avanzadas
La siguiente tabla muestra todos los tipos de datos que es posible validar con
PropTypes.
99 | Página
PropTypes.string Valida que la propiedad sea tipo String
Eje: {name: PropTypes.string}
PropTypes.number Valida que la propiedad sea numérica
Eje: {price: PropTypes.number}
PropTypes.bool Valida que la propiedad sea booleana
Eje: {checked: PropTypes.bool}
PropTypes.object Valida que la propiedad sea un objeto con cualquier
estructura
Eje: {product: PropTypes.object}
PropTypes.objectOf Valida que la propiedad sea un objeto con propiedades
de un determinado tipo
Eje: {tels: PropTypes.objectOf(PropType.string)}
PropTypes.shape Valida que la propiedad sea un objeto de una
estructura determinada
Eje:
{product: PropTypes.shape({
name: PropTypes.string,
price: PropTypes.number
})}
PropTypes.array Valida que la propiedad sea un arreglo
Eje: {tels: PropTypes.array}
PropTypes.arrayOf Valida que la propiedad sea un arreglo de un
terminado tipo de dato
Eje: {tels: PropTypes.arrayOf(PropType.string)}
PropTypes.oneOfType Valida que la propiedad sea de cualquier de los tipos
de datos especificado (es decir, puede ser de uno o de
otro tipo)
Eje:
{tel: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.arrayOf(PropTypes.string)
])}
PropTypes.func Valida que la propiedad sea una función
Eje: {save: PropTypes.func}
PropTypes.node Valida que la propiedad sea cualquier valor que pueda
ser renderizado en pantalla.
Eje: {node: PropType.node}
PropTypes.element Valida que la propiedad sea cualquier elemento de
React
Eje: {element: PropType.element}
PropTypes.instanceOf Valida que la propiedad sea la instancia de una clase
determinada
Eje: { product: PropType.instanceOf(Product) }
PropTypes.oneOf Valida que el valor de la propiedad este dentro de una
lista de valores permitidos (Igual que una
Enumeración)
Eje:
{status: PropType.oneOf([‘ACTIVO’,’INACTIVO’])}
PropTypes.any Le indica a React que la propiedad puede ser de
cualquier tipo
Eje: {object: PropType.any }
Página | 100
DefaultProps
Los DefaultProps son el mecanismo que tiene React para establecer un valor por
default a las propiedades que no fueron definidas en la creación del componente,
de esta forma, podemos establecer un valor y no dejar la propiedad en null.
Definir valores por defecto mediante los defaults props, no tiene gran ciencia,
solo es establecer el nombre de la propiedad con el valor por default, solo
recordemos cumplir con la estructura definida propTypes.
Refs
Los Refs o referencias, son la forma que tiene React para hacer referencia a un
elemento de forma rápida, muy parecido a realizar una búsqueda por medio del
método getElementById de JavaScript. Mediante los Ref, es posible agregarle un
identificador único a los elementos para después accederlos mediante la
instrucción this.refs.<ref>, donde <ref> es el identificador único del elemento.
101 | Página
Regresemos al ejemplo del formulario de registro de empleados. Cuando la
pantalla carga, no hay ningún campo con el foco, por lo que queremos que
cuando inicie la página, el campo del nombre de empleado obtenga el foco.
Veamos cómo quedaría:
Veamos que al campo firstName le hemos agregado el atributo ref (línea 32), el
cual establece el identificador para Rect, lo segundo importante es la línea 14,
pues declaramos el método componentDidMount, este método no tiene un nombre
al azar, si no que corresponde con uno de los métodos del ciclo de vida de React,
por lo que el método se ejecuta de forma automática cuando el componente es
mostrado en pantalla. Más adelante analizaremos el ciclo de vida de un
componente, pero por ahora, esta breve explicación deberá ser suficiente.
Página | 102
Cuando el método componentDidMount se ejecuta, obtiene la referencia al campo
productName mediante la instrucción this.refs.<ref> y le establece el foco.
Las Refs tiene un alcance global en un proyecto y pueden ser accedidos desde
cualquier componente sin importar su posición en la jerarquía, por lo que es
común utilizarse para hacer referencia a elementos globales de una aplicación u
elementos que pueden ser afectados desde diversos puntos de la aplicación.
Keys
Los Keys pueden ser un tema avanzado para nosotros en este momento, pues
está relacionado con la optimización de React, sin embargo, quisiera explicarlos
en este momento, pues es un concepto muy simple y es fundamental utilizarlos.
Los keys son utilizados por React para identificar de forma más eficiente los
elementos que han cambiado, agregados o eliminados dentro de la aplicación,
los keys son utilizados únicamente cuando queremos mostrar los datos de un
arreglo, como una lista. En el ejemplo de la lista de productos, iterábamos un
array para mostrar todos los productos dentro de una lista <li>.
103 | Página
13. price: 200
14. }]
15.
16. return (
17. <ul>
18. <For each="item" index='index' of={ items }>
19. <ItemList product={item} key={ ítem.name } />
20. </For>
21. </ul>
22. )
23. }
24. }
25.
26. render(<App/>, document.getElementById('root'));
Como vez, es tan simple como añadir el atributo key y asignarle un valor único
en la colección. Si nosotros ejecutamos este ejemplo, veremos la lista de
siempre, por lo que no hay nada que mostrar, lo interesante se genera, cuando
quitamos el key:
Como podemos ver en la imagen, nos está solicitando un key para cada elemento
de la lista
Página | 104
Debido a que React está creado bajo el lenguaje de programación JavaScript,
este también depende de su sintaxis para construir y declarar sus componentes
y es por ello que existen actualmente 4 formas de crear componentes, las cuales
obedecen a las distintas versiones del Estándar ECMAScript.
ECMAScript 5 – createClass
Este fue el primer método que existió para crear componentes en React y aun
que ya está bastante anticuado, la realidad es que puedes encontrar mucho
material en internet que todavía utiliza este método. Es muy probable que ya no
te toque trabajar con este método, sin embargo, no está de más mencionarlo y
si te lo llegaras a encontrar, sepas de que están hablando.
ECMAScript 6 - React.Component
Este es el método que hemos está utilizando hasta ahora, en la cual los
componentes se cran mediante clases que extienden de React.Component.
vemos un ejemplo rápido:
105 | Página
20.
21. ECMAScript6Class.defaultProps = {
22. ...
23. }
Cuando declaramos una clase, es necesario crear el constructor que reciba los
props, para después enviarlos a la superclase, de esta forma iniciamos
correctamente el componente. Los propsTypes y defaultsProps son declarados
fuera de la clase.
ECMAScript 7 - React.Component
Página | 106
2.
3. static propTypes = {
4. ...
5. }
6.
7. static defaultProps = {
8. ...
9. }
10.
11. state = { ... }
12.
13. constructor(props){
14. super(props)
15. }
16.
17. render(){
18. ...
19. }
20. }
Este método es prácticamente igual que crear una clase en ES6, con la diferencia
que es posible declarar el estado como una propiedad, adicional, podemos
declarar los defaultProps y propTypes dentro de la clase y marcarlos como static
para poder ser accedidos desde fuera sin necesidad de crear una instancia.
Resumen
Este capítulo junto con el anterior, serán claves para el resto de tu aprendizaje
en React, pues hemos visto casi todas las características de React y serán la
base para construir componentes más complejos.
Tener claros todos los conceptos que hemos aprendido hasta ahora, será
claves, por lo que te recomiendo que, si todavía tienes dudas en algunas cosas,
sería buen momento para repasarlos.
107 | Página
Introducción al proyecto Mini
Twitter
Capítulo 5
Página de inicio
Página | 108
La siguiente página corresponde a la página de inicio de Mini Twitter, en la cual
podemos ver el menú bar en la parte superior, los datos del usuario del lado
izquierdo, en el centro tenemos los tweets y del lado derecho, tenemos una lista
de usuarios sugeridos.
Perfil de usuario
109 | Página
La página de perfil puede cambiar de estado a editable, la cual permite cambiar
la foto de perfil, banner, nombre y la descripción:
Página de seguidores
Página | 110
También es posible ver el detalle de cada Tweet, para ver los comentarios que
tiene y agregar nuevos.
Otra de las páginas que cuenta la aplicación son las clásicas pantallas de iniciar
sección (login), la cual autenticarse ante la aplicación mediante usuario y
password:
111 | Página
Mediante esta página, es posible crear una nueva cuenta para poder acceder a
la aplicación.
Hemos visto un recorrido rápido a lo que será la aplicación de Mini Twitter, pero
la aplicación es engañosa, porque tiene muchas más cosas de las podemos ver a
simple vista, las cuales tenemos que analizar mucho más a detalle. Es por ese
motivo, que una vez que dimos un tour rápido de la aplicación, es hora de verla
con rayos X y ver cómo es que la aplicación se compone y todos los Components
que vamos a requerir para termina la aplicación.
En esta sección analizaremos con mucho detalle todos los componentes que
conforman la aplicación Mini Twitter.
Componente TwitterDashboard
Página | 112
Fig. 59 - Topología de la página de inicio
En la imagen anterior, podemos ver con más detalle cómo está compuesta la
página de inicio. A simple vista, es posible ver 5 componentes, los cuales son:
Adicional a los componentes que podemos ver en pantalla, existe uno más
llamado TwitterApp, el cual envuelve toda la aplicación e incluyendo las demás
componentes, como las páginas de login, signup, y el perfil del usuario.
Componente TweetsContainer
113 | Página
Fig. 60 - Topología del componente TweetsContainer
Componente UserPage
La página de perfil, permite a los usuarios ver su perfil y ver el perfil de los demás
usuarios. Este componente se muestra de dos formas posibles, ya que si estás
en tu propio los datos siempre y cuando estés en tu propio perfil. Por otra parte,
si estas en el perfil de otro usuario, te dará la opción de seguirlo.
Página | 114
Fig. 61 - Topología de la página UserPage
En esta página es posible ver varios componentes que se reúnen para formar la
página:
Componente Signup
Este es el formulario para crear un nuevo usuario, el cual solo solicita datos
mínimos para crear un nuevo perfil.
115 | Página
Fig. 62 - Topología de la página Signup
Componente Login
Página | 116
Hasta este momento, hemos visto los componentes principales de la aplicación,
lo que falta son algunas componentes de popup y compontes secundarios en los
cuales no me gustaría nombrar aquí, pues no quisiera entrar en mucho detalle
para no perdernos. Por ahora, con que tengamos una idea básica de cómo está
formada la aplicación será más que suficiente y a medida que entremos en los
detalles, explicaremos los componentes restantes.
117 | Página
Uno de los aspectos más importantes cuando vamos a desarrollar una nueva
aplicación, es determina el orden en que vamos a construir los componentes,
pues la estrategia que tomemos, repercutirá en la forma que vamos a trabajar.
Es por este motivo que vamos a presentar el en enfoque Top-down y Bottom-up
para analizar sus diferencias y las ventajas que traen cada una.
Top-down
Página | 118
Bottom-up
Este otro enfoque es todo lo contrario que Top-down, pues propone empezar con
los componentes más abajo en la jerarquía, de esta forma, iniciamos con los
componentes que no tienen dependencias y vamos subiendo en la jerarquía hasta
llegar al primer componente en la jerarquía.
Este enfoque es utilizado por los desarrolladores más experimentados, que son
capases de analizar con buen detalle la aplicación a desarrollar. Un mal análisis
puede hacer que replanteemos gran parte de la estructura y con ellos, se genera
un gran impacto en el desarrollo.
119 | Página
Preparando el entorno del proyecto
Dado que la aplicación Mini Twitter cuenta con una serie de servicios para
funcionar, deberemos instalar y ejecutar nuestra API Rest antes de empezar a
desarrollar, ya que toda la información que consultemos o actualicemos, será por
medio del API REST.
Por el momento no entraremos en detalles acerca del API REST, pues más
adelante hablaremos de cómo desarrollar desde cero, todo el API, publicarlos y
prepararlo para producción. Por ahora, solo instalaremos el API y lo utilizaremos.
Para instalar el API, deberemos bajar la última versión del repo, es decir el branch
“Capitulo-16-Produccion” directamente desde el repo del libro:
https://github.com/oscarjb1/books-reactiveprogramming.git
Página | 120
Fig. 67 - Obtener el String de conexión.
1. module.exports = {
2. server: {
3. port: 3000
4. },
5. tweets: {
6. maxTweetSize: 140
7. },
8. mongodb: {
9. development: {
10. connectionString: "<Copy connection String from Mongo Atlas>"
11. },
12. production: {
13. connectionString: "<Copy connection String from Mongo Atlas>"
14. }
15. },
16. jwt: {
17. secret: "#$%EGt2eT##$EG%Y$Y&U&/IETRH45W$%whth$Y$%YGRT"
18. }
19. }
121 | Página
En Windows:
1. 127.0.0.1 api.localhost
En Linux
1. 127.0.0.1 api.localhost
En Mac
En Mac, tendremos que hacer exactamente lo mismo que en los anteriores, sin
embargo, el archivo se encuentra en /private/etc/hosts, allí agregaremos la
línea:
1. 127.0.0.1 api.localhost
Iniciar el API
npm install
npm start
Con esto, si todo sale bien, habremos iniciado correctamente el API REST.
Página | 122
Probando nuestra API
123 | Página
Creando un usuario de prueba
Antes de poder empezar a utilizar nuestra API, será necesario crear un usuario,
por lo cual, utilizaremos un archivo de utilidad llamado InitMongoDB.js, que se
encargará de esto. El archivo se ve así:
No vamos entrar en los detalles, pues este archivo tiene cosas avanzadas que no
hemos visto aún. Sin embargo, lo muestro por si desean analizarlo.
Por ahora, tendremos que dirigirnos a la terminal y dirigirnos a la carpeta del API
(donde descargamos el código) y ejecutar el comando
node InitMongoDB.js
Página | 124
Fig. 70 - Creación del usuario de prueba.
Si sale algún error, deberemos revisar los datos de conexión a la base de datos.
Username: test
Password: 1234
Para comprobar que todo salió bien, podemos regresar a Compass y ver la
colección profiles, en ella deberíamos ver el usuario:
125 | Página
Invocando el API REST desde React
En este punto ya tenemos el API funcionando y listo para ser utilizando, pero
debemos aprender cómo es que un servicio REST es consumido desde una
aplicación React.
1. class APIInvoker {
2.
3. invoke(url, okCallback, failCallback, params){
4. fetch(`http://api.localhost:3000${url}`, params)
5. .then((response) => {
6. return response.json()
7. })
8. .then((responseData) => {
9. if(responseData.ok){
10. okCallback(responseData)
11. }else{
12. failCallback(responseData)
13. }
14. })
15. }
16. }
17. export default new APIInvoker();
1. url: String que representa el recurso que se quiere consumir, sin contener el
host y el puerto, ejemplo “/tweets”.
2. okCallback: deberá ser una función, la cual se llamará solo en caso de que
el servicio responda correctamente. La función deberá admitir un parámetro
que representa la respuesta del servicio.
3. failCallback: funciona igual al anterior, solo que este se ejecuta cuando el
servicio responde con algún error.
4. params: Representa los parámetros de invocación HTTP, como son los
header y body.
De estos cuatro parámetros solo dos son requeridos por la función fetch en la
línea 4, la url y params. Cuando el servicio responde, la respuesta es procesada
mediante una promesa (Promise), es decir, una serie de then, los cuales procesan
la respuesta por partes. El primer then (línea 5) convierte la respuesta en un
Página | 126
objeto json (línea 6) y lo retorna para ser procesado por el siguiente then (línea
8), el cual, valida la propiedad ok de la respuesta, si el valor de esta propiedad
es true, indica que el servicio termino correctamente y llamada la función
okCallback, por otra parte, si el valor es false, indica que algo salió mal y se
ejecuta la función failCallback.
El siguiente paso será probar nuestra clase con un pequeño ejemplo que consulte
los Tweets de nuestro usuario de pruebas, para esto modificaremos la clase App
para dejarla de la siguiente manera:
127 | Página
No entraremos en los detalles del todo el componente, pues nos es el objetivo
de esta sección, pero lo que solo nos centraremos en la función
componentWillMount (línea 14). Esta función no tiene un nombre aleatorio, si no
que corresponde a una de las funciones del ciclo de vida de los componentes de
React, el cual se ejecuta automáticamente justo antes del que el componente
sea montado, es decir, que sea visible en el navegador. Lo primero que hacemos
será preparar los parámetros de la invocación (línea 15). Dentro de la variable
definimos un objeto con las propiedades:
Finalmente, los Tweets retornados son mostrados en el método render con ayuda
de un <For> para iterar los resultados.
Hasta este punto ya sabemos cómo invocar el API REST desde React, sin
embargo, necesitamos mejorar aún más nuestra clase APIInvoker para
reutilizarla en todo el proyecto.
Lo primero que aremos será quitar todas las secciones Hardcode, como lo son el
host y el puerto y enviarlas a un archivo de configuración externo llamado
config.js, el cual deberemos crear justo en la raíz del proyecto, es decir a la
misma altura que los archivos package.json y webpack.config.js:
Página | 128
Fig. 72 - Archivo config.js
1. module.exports = {
2. debugMode: true,
3. server: {
4. port: 3000,
5. host: "http://api.localhost"
6. }
7. }
129 | Página
27.
28. this.invoke(url, okCallback, failCallback,params);
29. }
30.
31. invokePOST(url, body, okCallback, failCallback){
32. let params = {
33. method: 'post',
34. headers: this.getAPIHeader(),
35. body: JSON.stringify(body)
36. };
37.
38. this.invoke(url, okCallback, failCallback,params);
39. }
40.
41. invoke(url, okCallback, failCallback,params){
42. if(debug){
43. console.log("Invoke => " + params.method + ":" + url );
44. console.log(params.body);
45. }
46.
47. fetch(`${configuration.server.host}:${configuration.server.port}${url}`,
48. params)
49. .then((response) => {
50. if(debug){
51. console.log("Invoke Response => " );
52. console.log(response);
53. }
54. return response.json()
55. })
56. .then((responseData) => {
57. if(responseData.ok){
58. okCallback(responseData)
59. }else{
60. failCallback(responseData)
61. }
62.
63. })
64. }
65. }
66. export default new APIInvoker();
Si nos vamos al detalle de cada uno de estos métodos, veremos que en realidad
contienen lo mismo, ya que lo único que hacen es crear los parámetros HTTP,
como son los headers y el body, para finalmente llamar al método invoke (sin
postfijo). La única diferencia que tienen estas funciones es que el método
invokeGET no requiere un body.
Página | 130
Otro punto interesante a notar es que cuando definimos los header, lo hacemos
mediante la función getAPIHeader, la cual retorna el Content-Type y una
propiedad llamada authorization. No entraremos en detalle acerca de esta, pues
lo analizaremos más adelante cuando veamos la parte de seguridad.
En este punto tenemos lista la clase APIInvoker para ser utilizada a lo largo de
toda la implementación del proyecto Mini Twitter, Y ya solo nos restaría ajustar
la clase App.js para reflejar estos últimos cambios, por lo que vamos a modificar
únicamente la función componentWillMount para que se vea de la siguiente
manera:
1. componentWillMount(){
2. APIInvoker.invokeGET('/tweets/test', response => {
3. this.setState({
4. tweets: response.body
5. })
6. },error => {
7. console.log("Error al cargar los Tweets", error);
8. })
9. }
El componente TweetsContainer
131 | Página
Fig. 73 - Ubicando el componente TweetsContainer
Página | 132
Lo primero que haremos será crear el archivo TweetsContainer.js en el path
/app, es decir, a la misma altura que el archivo App.js y lo dejaremos de la
siguiente manera:
133 | Página
Lo primero interesante a resaltar es la función componentWillMount, la cual
recupera dos propiedades, username y onlyUserTweet, las cuales necesitará pasar
a la función loadTweets para cargar los Tweet iniciales. Cabe mencionar que este
componente puede mostrar Tweet de dos formas, es decir, puede mostrar los
Tweet de todos los usuarios de forma cronológica o solo los Tweet del usuario
autenticado, y ese es el motivo por el cual son necesarios estas dos propiedades.
Si la propiedad onlyUserTweet es true, le indicamos al componente que muestre
solo los Tweet del usuario autenticado, de lo contrario, mostrara los Tweet de
todos los usuarios. Más adelante veremos la importancia de hacerlo así para
reutilizar el componente.
Usuarios registrados
De momento solo tendremos el usuario test, por lo
que de momento solo podremos consultar los Tweets
de este usuario. Más adelante implementaremos el
registro de usuarios para poder hacer pruebas con más
usuarios.
En las líneas 35 a 38 podemos ver como se Iteran todos los Tweets consultados
para mostrar el nombre de usuario del Tweet, el ID y el texto del Tweet.
Al final del archivo también podemos apreciar que hemos definidos los PropTypes
y los DefaultProps, correspondientes a las propiedades onlyUserTweet y profile.
El primero ya lo hemos mencionado, pero el segundo corresponde al usuario
autenticado en la aplicación. De momento no nos preocupemos por esta
propiedad, más adelante regresaremos a analizarla.
Página | 134
Una vez terminado de editar el archivo TweetsContainer, regresaremos al
componente App.js y actualizaremos la función render para que se vea de la
siguiente manera:
1. render(){
2. return (
3. <TweetsContainer />
4. )
5. }
El componente Tweet
Hasta este momento solo representamos algunos campos del Tweet para poder
comprobar que el componente TweetsContainer está consultando realmente los
datos desde el API REST, por lo que ahora nos concentraremos en el componente
Tweet, el cual utilizaremos pare representar los Tweets en pantalla.
Lo primero que haremos será crear un nuevo archivo llamado Tweet.js en el path
/app y lo dejaremos de la siguiente manera:
135 | Página
14. let tweetClass = null
15. if(this.props.detail){
16. tweetClass = 'tweet detail'
17. }else{
18. tweetClass = this.state.isNew ? 'tweet fadeIn animated' : 'tweet'
19. }
20.
21. return (
22. <article className={tweetClass} id={"tweet-" + this.state._id}>
23. <img src={this.state._creator.avatar} className="tweet-avatar" />
24. <div className="tweet-body">
25. <div className="tweet-user">
26. <a href="#">
27. <span className="tweet-name" data-ignore-onclick>
28. {this.state._creator.name}</span>
29. </a>
30. <span className="tweet-username">
31. @{this.state._creator.userName}</span>
32. </div>
33. <p className="tweet-message">{this.state.message}</p>
34. <If condition={this.state.image != null}>
35. <img className="tweet-img" src={this.state.image}/>
36. </If>
37. <div className="tweet-footer">
38. <a className={this.state.liked ? 'like-icon liked' :
39. 'like-icon'} data-ignore-onclick>
40. <i className="fa fa-heart " aria-hidden="true"
41. data-ignore-onclick></i> {this.state.likeCounter}
42. </a>
43. <If condition={!this.props.detail} >
44. <a className="reply-icon" data-ignore-onclick>
45. <i className="fa fa-reply " aria-hidden="true"
46. data-ignore-onclick></i> {this.state.replys}
47. </a>
48. </If>
49. </div>
50. </div>
51. <div id={"tweet-detail-" + this.state._id}/>
52. </article>
53. )
54. }
55. }
56.
57. Tweet.propTypes = {
58. tweet: PropTypes.object.isRequired,
59. detail: PropTypes.bool
60. }
61.
62. Tweet.defaultProps = {
63. detail: false
64. }
65.
66. export default Tweet;
Página | 136
indica si el Tweet debe mostrar el detalle, es decir, los comentarios relacionados
al Tweet.
Las líneas 38 a 48 son las que muestran los iconos de like y compartir, por el
momento no tendrá funcionalidad, pero más adelante regresaremos para
implementarla.
En la línea 51 tenemos un div con solo un ID, este lo utilizaremos más adelante
para mostrar el detalle del Tweet, por lo pronto no lo prestemos atención.
1. render(){
2. return (
3. <main className="twitter-panel">
4. <If condition={this.state.tweets != null}>
5. <For each="tweet" of={this.state.tweets}>
6. <Tweet key={tweet._id} tweet={tweet}/>
7. </For>
8. </If>
9. </main>
10. )
11. }
Como último paso tendremos que agregar las clases de estilo CSS al archivo
styles.css que se encuentra en el path /public/resources/css/styles.css. Solo
agreguemos lo siguiente al final del archivo:
137 | Página
1. /** TWEET COMPONENT **/
2.
3. .tweet{
4. padding: 10px;
5. border-top: 1px solid #e6ecf0;
6. }
7.
8.
9. .tweet .tweet-link{
10. position: absolute;
11. display: block;
12. left: 0px;
13. right: 0px;
14. top: 0px;
15. bottom: 0px;
16. }
17.
18. .tweet:hover{
19. background-color: #F5F8FA;
20. cursor: pointer;
21. }
22.
23. .tweet.detail{
24. border-top: none;
25. }
26.
27. .tweet.detail:hover{
28. background-color: #FFF;
29. cursor: default;
30. }
31.
32.
33. .tweet .tweet-img{
34. max-width: 100%;
35. border-radius: 5px;
36. }
37.
38. .tweet .tweet-avatar{
39. border: 1px solid #333;
40. display: inline-block;
41. width: 45px;
42. height: 45px;
43. position: absolute;
44. border-radius: 5px;
45. text-align: center;
46. }
47.
48. .tweet .tweet-body{
49. margin-left: 55px;
50. }
51.
52. .tweet .tweet-body .tweet-name{
53. color: #333;
54. }
55.
56. .tweet .tweet-body .tweet-name{
57. font-weight: bold;
58. text-transform: capitalize;
59. margin-right: 10px;
60. z-index: 10000;
61. }
62.
63. .tweet .tweet-body .tweet-name:hover{
64. text-decoration: underline;
65. }
66.
Página | 138
67. .tweet .tweet-body .tweet-username{
68. text-transform: lowercase;
69. color: #999;
70. }
71.
72. .tweet.detail .tweet-body .tweet-user{
73. margin-left: 70px;
74. }
75.
76. .tweet.detail .tweet-body{
77. margin-left: 0px;
78. }
79.
80. .tweet.detail .tweet-body .tweet-name{
81. font-size: 18px;
82. }
83.
84. .tweet-detail-responses .tweet.detail .tweet-body .tweet-message{
85. font-size: 16px;
86. }
87.
88. .tweet.detail .tweet-body .tweet-username{
89. display: block;
90. font-size: 16px;
91. }
92.
93. .tweet.detail .tweet-message{
94. position: relative;
95. display: block;
96. margin-top: 25px;
97. font-size: 26px;
98. left: 0px;
99. }
100.
101. .reply-icon,
102. .like-icon{
103. color: #999;
104. transition: 0.5s;
105. padding-right: 40px;
106. font-weight: bold;
107. font-size: 18px;
108. z-index: 99999;
109. }
110.
111. .like-icon:hover{
112. color: #E2264D;
113. }
114.
115. .like-icon.liked{
116. color: #E2264D;
117. }
118.
119. .reply-icon:hover{
120. color: #1DA1F2;
121. }
122.
123. .like-icon i{
124. color: inherit;
125. }
126.
127. .reply-icon i{
128. color: inherit;
129. }
139 | Página
Guardamos todos los cambios y refrescar el navegador para apreciar cómo va
tomando forma el proyecto.
Una cosa más antes de concluir este capítulo. Hasta el momento hemos trabajado
con la estructura de los Tweets retornados por el API REST, pero no los hemos
analizado, es por ello que dejo a continuación un ejemplo del JSON.
1. {
Página | 140
2. "ok":true,
3. "body":[
4. {
5. "_id":"598f8f4cd7a3b239e4e57f3b",
6. "_creator":{
7. "_id":"598f8c4ad7a3b239e4e57f38",
8. "name":"Usuario de prueba",
9. "userName":"test",
10. "avatar":""
11. },
12. "date":"2017-08-12T23:29:16.078Z",
13. "message":"Hola mundo desde mi tercer Tweet",
14. "liked":false,
15. "likeCounter":0,
16. "replys":0,
17. "image":null
18. }
19. ]
20. }
Lo primero que vamos a observar de aquí en adelante es que todos los servicios
retornados por el API tienen la misma estructura base, es decir, regresan un
campo llamado ok y un body, el ok nos indica de forma booleana si el resultado
es correcto y el body encapsula todo el mensaje que nos retorna el API. Si
regresamos al componente TweetContainer en la línea 22 veremos lo siguiente:
1. this.setState({
2. tweets: response.body
3. })
Observemos que el estado lo crea a partir del body y no de todo el retorno del
API.
Resumen
141 | Página
Este capítulo ha sido bastante emocionante pues hemos iniciado con el proyecto
Mini Twitter y hemos aplicados varios de los conceptos que hemos venido
aprendiendo a lo largo del libro. Si bien, solo hemos empezado, ya pudimos
apreciar un pequeño avance en el proyecto.
Por otra parte, hemos visto como instalar el API REST y hemos aprendido como
consumirlo desde React, también hemos analizado la estructura de un Tweet
retornado por el API.
Página | 142
Introducción al Shadow DOM y los
Estados
Capítulo 6
Una de las características más importantes de React, es que permite que los
componentes tengan estado, el cual es un objeto JavaScript de contiene la
información asociada al componente y que por lo general, representa la
información que ve el usuario en pantalla, pero también el estado puede
determinar la forma en que una aplicación se muestra al usuario. Podemos ver
el estado como el Modelo en la arquitectura MVC.
143 | Página
entonces solo mostrara los datos del empleado en etiquetas <label>, pero si
pasamos a edición, entonces los <label> se remplazan por etiquetas <input>, y
una vez que guardamos los datos, entonces podemos pasar el formulario
nuevamente a modo solo lectura.
1. {
2. editMode: false,
3. employee: {
4. name: "Juan Pérez",
5. age: 22,
6. tel: "12334567890",
7. birthdate: "10/10/1900"
8. }
9. }
Los estados pueden tener la estructura que sea y también pueden ser tan grandes
y complejos como nuestra interface lo requieres, de tal forma que podemos tener
muchos más datos y muchos más campos de control de interface gráfica.
1. constructor(props){
2. super(props)
3. this.state = {
4. tweets: []
5. }
6. }
Página | 144
En este ejemplo, definimos el estado del componente TweetsContainer mediante
una lista de tweets vacío, esto con la finalidad de que cuando el componente se
muestre por primera vez, no marce un error debido a que el estado es Null.
1. loadTweets(username, onlyUserTweet){
2. let url = '/tweets' + (onlyUserTweet ? "/" + username : "")
3. APIInvoker.invokeGET(url, response => {
4. this.setState({
5. tweets: response.body
6. })
7. },error => {
8. console.log("Error al cargar los Tweets", error);
9. })
10. }
El estado es actualizado cuando obtenemos la respuesta del API (línea 4). Cuando
actualizamos el estado mediante la función this.setState(), React
automáticamente actualiza el componente, para que de esta forma, la vista sea
actualizada para reflejando el nuevo Estado.
Una de las principales diferencias que existe entre las propiedades (Props) y el
Estado, es que el estado está diseñado para ser mutable, es decir, podemos
145 | Página
realizar cambios en el, de tal forma que los componentes puedan ser interactivos
y responder a las acciones del usuario.
React está pensado para que el objeto que representa el estado sea inmutable,
por lo que cuando creamos un nuevo estado, tendremos que hacer una copia del
estado actual y sobre esa copia agregar o actualizar los nuevos valores. Esto es
así para garantiza que React pueda detectar los nuevos cambios y actualizar la
vista.
Una de las formas que tenemos para actualizar el estado, es mediante el método
Object.assign, el cual se utiliza para copiar los valores de todas las propiedades
enumerables de uno o más objetos fuente a un objeto destino. Retorna un nuevo
objeto con los cambios. Veamos un ejemplo:
1. let newState = {
2. edit: true
3. }
4. let stateUpdate = Object.assign({},this.state,newState)
5. console.log(stateUpdate)
6. this.setState(stateUpdate)
Página | 146
entre el estado actual y el nuevo estado, por lo que los valores que ya están en
el estado actual solo se actualizarán y los que no estén, se agregarán. Como
resultado de “merge”, Finalmente, actualizamos el estado mediante
this.setState().
Tras ejecutar este código podremos ver en el log del navegador como quedaría
la variable stateUpdate:
Un ejemplo claro de este problema es cuando nuestro objeto tiene una array,
veamos otro ejemplo:
1. let newState = {
2. edit: true,
3. tweet: [
4. "tweet 1",
5. "tweet 2"
6. ]
7. }
8. let stateUpdate = Object.assign({},this.state,newState)
9. newState.tweet.push("tweet 3")
10. console.log(stateUpdate, newState);
Veamos que hemos agregar al estado una lista de tweets (línea 1 a 7), luego
aplicada la asignación (línea 8). Finalmente agregamos un nuevo Tweet al objeto
newState. (línea 9). Uno esperaría que el objeto newState tenga el tweet 3,
mientras que el stateUpdate se quedaría con los dos primero. Sin embargo, la
realidad es otra; veamos el resultado de log (línea 10) tras ejecutar este código:
147 | Página
cambios. Para solucionar este problema, existe una librería más sofisticada para
actualizar los estados de una forma más segura, la cual veremos a continuación.
La librería react-addons-update
Antes de empezar a trabajar con la librería, debemos instalarla con npm, para
ello, ejecutamos la siguiente instrucción:
npm install --save react-addons-update
Este ejemplo es parecido a los anteriores, pues modificamos el valor del atributo
edit a true, sin embargo, notemos una pequeña diferencia en la sintaxis, pues
en lugar de poner el valore del atributo directamente, tenemos que hacerlo
mediante un par de llaves, el cual tiene dos partes, la operación y el valor. La
operación le indica que hacer con el valor, en este caso, $set indica que se
establezca el valor true al atributo edit, en caso de que el valor exista lo
actualiza, y si no existe, lo agregará.
Operación Descripción
$push Agrega todos los elementos de una lista al final de la lista objetivo.
Funciona igual que la operación push() de ES
Formato: {$push: array}
Ejemplo:
Página | 148
$unshift Agrega todos los elementos de una lista al principio de la lista
objetivo. Funciona igual que la operación unshift de ES
Formato: {$unshift: array}
Ejemplo:
$apply Permite actualiza el valor actual por medio de una función, esta
función permite realizar cálculos más complejos.
Formato: {$apply: function}
Ejemplo:
Cabe mencionar que las secciones que no son manipuladas por alguna operación,
pasan intacta al objeto resultante. Por lo que solo es necesario realizar
operaciones sobre los campos que queremos modificar.
149 | Página
El Shadow DOM de React
Dado que React se ejecuta del lado del navegador, este está obligado a
arreglárseles solo para la actualización de las vistas. De esta forma, React es el
que tiene que decirle al navegador que elementos del DOM deberán ser
actualizados para reflejar los cambios.
El Shadow DOM es utilizado por React pare evaluar las actualizaciones que deberá
aplicar al DOM real, y una vez evaluados los cambios, se ejecuta un proceso
llamado reconciliación, encargado de sincronizar los cambios del Shadow DOM
hacia el DOM real. Este proceso hace que React mejore su rendimiento, al no
tener que actualizar la vista ante cada cambio, en su lugar, calcula todos los
cambios y los aplica en Batch al DOM real.
Página | 150
atributos en Camel Case, es posible que React no reconozca el atributo y/o que
nos lance un error en la consola.
React tiene una lista de todos los atributos que soporte, la cual nos puede servir
de guía:
accept, acceptCharset, accessKey, action, allowFullScreen, allowTransparency, alt,
async, autoComplete, autofocus, autoPlay, capture, cellPadding, cellSpacing, challenge,
charSet, checked classID, className, colSpan, cols, content, contentEditable,
contextMenu, controls, coords, crossOrigin, data, dateTime, default, defer, dir,
disabled, download, draggable, encType, form, formAction, formEncType, formMethod,
formNoValidate, formTarget, frameBorder, headers, height, hidden, high, href, hrefLang,
htmlFor, httpEquiv, icon, id, inputMode, integrity, is, keyParams, keyType, kind, label,
lang, list, loop, low, manifest, marginHeight, marginWidth, max, maxLength, media,
mediaGroup, method, min, minLength, multiple, muted, name, noValidate, nonce, open,
optimum, pattern, placeholder, poster, preload, radioGroup, readOnly, rel, required,
reversed, role, rowSpan, rows, sandbox, scope, scoped, scrolling, seamless, selected,
shape, size, sizes, span, spellCheck, src, srcDoc, srcLang, srcSet, start, step, style,
summary, tabIndex, target, title, type, useMap, value, width, wmode, wrap.
Muchos de los atributos que ves en esta lista se ven exactamente igual al HTML
tradicional, y esto se debe a que son atributos formados por una palabra, por lo
que no hay necesidad de utilizar Camel Case. Ahora bien, existe ciertos atributos
con los que debemos de tener cuidado, pues son difieren del nombre original de
HTML, como es el caso de className para class, defaultChecked para checked,
htmlFor para for, etc.
Eventos
Los eventos en React tiene el mismo tratamiento que los atributos, pues deben
de definirse en Camel Case, de lo contrario no serán tomados en cuenta por
React, los principales eventos son:
Keyboard Events
onKeyDown onKeyPress onKeyUp
Focus Events
DOMEventTarget relatedTarget
Mouse Events
151 | Página
onClick onContextMenu onDoubleClick onDrag onDragEnd onDragEnter onDragExit onDragLeave
onDragOver onDragStart onDrop onMouseDown onMouseEnter onMouseLeave onMouseMove
onMouseOut onMouseOver onMouseUp
Selection Events
onSelect
Página | 152
Resumen
A lo largo de este capítulo hemos analizado los Estados, y como es que estos
afectan la forma que se ven y actualizan los componentes. Por otra parte, hemos
analizado las dos formas de establecer el estado, ya sea en el constructor o
mediante la función setState(). También analizamos la librería react-addons-
update para actualizar de forma correcta el estado.
153 | Página
Trabajando con Formularios
Capítulo 7
Los formularios son una parte fundamental de cualquier aplicación, pues son la
puerta de entrada para que los usuarios puedan interactuar con la aplicación, y
en React no es la excepción. Sin embargo, existe una diferencia sustancia al
trabajar con formularios en React y en una tecnología web convencional, ya que
React almacena la información en sus Estados, y no en una sesión del lado del
servidor, como sería el caso de una aplicación web tradicional.
Controlled Components
Los componentes controlados son todos aquellos que su valor está ligado
directamente al estado del componente o una propiedad (prop), lo que
significa que el control siempre mostrará el valor del objeto asociado.
Página | 154
para comprender esto. Crearemos un nuevo archivo llamado FormTest.js en el
path /app y lo dejaremos de la siguiente forma:
1. render(){
2. return (
3. <FormTest/>
4. )
5. }
Una vez actualizado el navegador, podremos ver el campo de texto con el valor
que pusimos en el estado del componente, y si intentamos actualizar el valor,
veremos que este simplemente no cambiará. Esto es debido a que React muestra
el valor como inmutable.
155 | Página
Observemos en la imagen que React nos ha arrojado un Warning, esta
advertencia se debe a que un campo controlado, deberá definir el atributo
onChange para controlar los cambios en el control, de lo contrario, este campo
será de solo lectura.
Para solucionar este problema, deberemos de crear una función que tome los
cambios en el control y posteriormente actualice el estado con el nuevo valor.
Cuando React detecte el cambio en el estado, iniciará la actualización de la vista
y los cambios será reflejados. Veamos cómo implementar esta función:
Todos los controles funcionan exactamente igual que en HTML tradicional, sin
embargo, existe dos controles que se utilizan de forma diferente, los cuales son
los TextArea y Select. Analicémoslo por separado.
TextArea
1. <textarea>{this.state.field}</textarea>
Página | 156
En React, para definir el valor de un TextArea se deberá utilizar el atributo value,
como podemos ver en el siguiente ejemplo:
1. <textarea value={this.state.field}/>
Select
El control Select cambia solo la forma en que seleccionamos el valor por default,
pues en HTML tradicional solo deberemos utilizar el atributo selected sobre el
valor por default, veamos un ejemplo:
1. <select>
2. <option value="volvo">Volvo</option>
3. <option value="saab">Saab</option>
4. <option value="vw">VW</option>
5. <option value="audi" selected>Audi</option>
6. </select>
1. <select value="audi">
2. <option value="volvo">Volvo</option>
3. <option value="saab">Saab</option>
4. <option value="vw">VW</option>
5. <option value="audi">Audi</option>
6. </select>
Uncontrolled Components
Para crear un componente no controlado, están simple como definir el control sin
el atributo value. Al no definir este atributo, React sabrá que el valor de este
control es libre y permitirá su edición sin necesidad de crear una función que
controle los cambios en el control. Para analizar esto, modifiquemos el método
render del componente FormTest para que quede de la siguiente manera.
1. render(){
2. return (
3. <div>
4. <input type="text" value={this.state.field}
5. onChange={this.updateField.bind(this)}/>
157 | Página
6. <br/>
7. <input type="text" name="field2" defaultValue="Init Value 2" />
8. </div>
9. )
10. }
Una vez aplicados los cambios, actualizamos el valor del control y presionamos
el botón submit para que nos arroje en pantalla el valor capturado
Enviar el formulario
Ahora bien, si lo que queremos hacer es recuperar los valores del control al
momento de mandar el formulario, deberemos encapsular los controles dentro
de un form. Regresemos al archivo FormTest y actualicemos la función render
para que sea de la siguiente manera:
1. render(){
2. return (
3. <div>
4. <input type="text" value={this.state.field}
5. onChange={this.updateField.bind(this)}/>
6. <br/>
7. <form onSubmit={this.submitForm.bind(this)} >
8. <input type="text" name="field2" />
9. <br/>
10. <button type="submit">Submit</button>
11. </form>
Página | 158
12. </div>
13. )
14. }
1. submitForm(e){
2. alert(this.state.field)
3. alert(e.target.field2.value)
4. e.preventDefault();
5. }
Existen dos variantes para recuperar los valores de un control. Si está controlado,
solo tendremos que hacer referencia a la propiedad del estado al que está ligado
el campo (línea 2), por otra parte, si el campo no es controlado, entonces
deberemos recuperar el valor del campo mediante su tributo name, como lo vemos
en la línea 3.
preventDefault function
La función e.preventDefault previene que el
navegador mande el formulario al servidor, lo que
provocaría que el navegador se actualice y nos haga
un reset del estado del componente.
Debido a que React trabajo por lo general con AJAX, es importante impedir que
el navegador actualice la página, pues esto provocaría que los compontes sean
cargados de nuevo y nos borre los estados. Es por eso la importancia de utilizar
la función preventDefault.
Una vez que hemos explicado la forma de trabajar con los controles,
continuaremos desarrollando nuestro proyecto de Mini Twitter, pero en este
capítulo nos centraremos en los componentes que utilizan formularios.
El componente Signup
159 | Página
Fig. 85 - Signup Component
Antes de irnos al código, entendamos como funciona. Para dar de alta a un nuevo
usuario, este deberá de capturar un nombre de usuario, su nombre y una
contraseña. El usuario deberá ser único, por lo que, si selecciona una ya
existente, la aplicación le mostrará una leyenda advirtiendo el error y no lo
deberá dejar continuar. El usuario también deberá confirmar que acepta los
términos de licencia, de lo contrario, tampoco podrá continuar.
Para confirmar el envío del formulario crearemos un botón, el cual tome los datos
capturados y cree el nuevo usuario mediante el API REST. Si todo sale bien, el
usuario será redirigido al componente de login, el cual crearemos en la siguiente
sección.
En esta pantalla utilizaremos dos servicios del API REST, el primero nos ayudará
a validar que el nombre de usuario no este repetido, y este se ejecutará cuando
el campo del nombre de usuario pierda el foco. El segundo servicio es el de
creación del usuario, el cual se ejecutará cuando el usuario presione el botón de
registro.
Página | 160
Nuevo concepto: Foco
Se dice que un elemento tiene el foco cuando este está
recibiendo los eventos de entrada como el teclado. Por
lo que cuando seleccionamos un campo de texto para
escribir sobre él, se dice que tiene el foco. Solo puede
existir un elemento con el foco en un tiempo
determinado.
161 | Página
34. this.setState(update(this.state,{
35. [field] : {$set: value}
36. }))
37. }
38. }
39.
40. render(){
41.
42. return (
43. <div id="signup">
44. <div className="container" >
45. <div className="row">
46. <div className="col-xs-12">
47.
48. </div>
49. </div>
50. </div>
51. <div className="signup-form">
52. <form>
53. <h1>Únite hoy a Twitter</h1>
54. <input type="text" value={this.state.username}
55. placeholder="@usuario" name="username" id="username"
56. onChange={this.handleInput.bind(this)}/>
57. <label ref="usernameLabel" id="usernameLabel"
58. htmlFor="username"></label>
59.
60. <input type="text" value={this.state.name} placeholder="Nombre"
61. name="name" id="name" onChange={this.handleInput.bind(this)}/>
62. <label ref="nameLabel" id="nameLabel" htmlFor="name"></label>
63.
64. <input type="password" id="passwordLabel"
65. value={this.state.password} placeholder="Contraseña"
66. name="password" onChange={this.handleInput.bind(this)}/>
67. <label ref="passwordLabel" htmlFor="passwordLabel"></label>
68.
69. <input id="license" type="checkbox" ref="license"
70. value={this.state.license} name="license"
71. onChange={this.handleInput.bind(this)} />
72. <label htmlFor="license" >Acepto los terminos de licencia</label>
73.
74. <button className="btn btn-primary btn-lg"
75. id="submitBtn">Regístrate</button>
76. <label ref="submitBtnLabel" id="submitBtnLabel" htmlFor="submitBtn"
77. className="shake animated hidden "></label>
78. <p className="bg-danger user-test">
Crea un usuario o usa el usuario <strong>test/test</strong></p>
79. <p>¿Ya tienes cuenta? Iniciar sesión</p>
80. </form>
81. </div>
82. </div>
83. )
84. }
85. }
86. export default Signup
En el constructor no hay mucho que ver, salvo que iniciamos el estado (línea 9)
con los valores en blanco para username, name, password, los cuales son
precisamente los mismos campos que hay en la pantalla. Adicional, definimos
dos variables booleanas de control, userOk y license, la primera nos indica si el
nombre de usuario es válido (validado por el servicio de validación de nombre de
Página | 162
usuario) y license corresponde al checkbox para aceptar los términos de licencia.
Ambos deberán ser true para permitir la creación del usuario.
Podemos observar que creamos un input de texto para nombre de usuario (línea
54) y para el nombre (línea 60), los cuales están ligados a su campo
correspondiente del estado this.state.username y this.state.name
respectivamente, para el password (línea 64) creamos también un input de tipo
password ligado a this.state.password, finalmente, creamos otro input de tipo
checkbox para los términos de licencia (línea 69) ligado a this.state.license.
Cabe mencionar que todos estos controles tienen definido el atributo onChange
ligado a la función handleInput, el cual analizamos a continuación.
163 | Página
25. background-color: #1DA1F2;
26. padding: 10px;
27. outline: none;
28. border: none;
29. }
30.
31. #signup input[type="text"],
32. #signup input[type="password"]{
33. outline: none;
34. border: 1px solid #999;
35. font-size: 18px;
36. width: 100%;
37. padding: 8px;
38.
39. border-radius: 5px;
40. }
41.
42. #signup button{
43. display: block;
44. padding: 5px;
45. width: 100%;
46.
47. }
48.
49. #signup .user-test{
50. margin-top: 30px;
51. padding: 10px;
52. border-radius: 5px;
53. }
54.
55.
56. #signup #usernameLabel{
57. display: inline-block;
58. position: absolute;
59. width: 100%;
60. padding: 10px;
61. }
62.
63. #signup #usernameLabel.ok{
64. color: #1DA1F2;
65. }
66.
67. #signup #usernameLabel.fail{
68. color: tomato;
69. }
70.
71. #signup #submitBtnLabel{
72. display: block;
73. text-align: center;
74. color: red;
75. margin-top: 20px;
76. }
Ya con los estilos agregados, deberemos actualizar el archivo App.js para mostrar
el componente Signup al iniciar la aplicación:
Página | 164
10.
11. constructor(props){
12. super(props)
13. }
14.
15. render(){
16. return (
17. <Signup/>
18. )
19. }
20. }
21.
22. render(<App/>, document.getElementById('root'));
En este punto, el formulario puede capturar los datos del usuario y actualizando
el estado al mismo tiempo que esto pasa.
165 | Página
1. validateUser(e){
2. let username = e.target.value
3. APIInvoker.invokeGET('/usernameValidate/' + username, response => {
4. this.setState(update(this.state, {
5. userOk: {$set: true}
6. }))
7. this.refs.usernameLabel.innerHTML = response.message
8. this.refs.usernameLabel.className = 'fadeIn animated ok'
9. },error => {
10. console.log("Error al cargar los Tweets");
11. this.setState(update(this.state,{
12. userOk: {$set: false}
13. }))
14. this.refs.usernameLabel.innerHTML = error.message
15. this.refs.usernameLabel.className = 'fadeIn animated fail'
16. })
17. }
Crear el usuario
Para finalizar el componente, solo nos queda habilitar la función para que se cree
el usuario, para lo cual deberemos hacer 3 cambios a nuestro componente. El
primer cambio será agregar el evento onClick al botón para que llame la función
signup justo después de presionar el botón.
Página | 166
El segundo cambio es agregar el evento onSubmit a la etiqueta form para llamar
la función signup.
1. <form onSubmit={this.signup.bind(this)}>
Notemos que estamos mandando llamar la función signup desde el botón y desde
la etiqueta form, lo que puede resultar redundante o confuso, pero existe una
razón por la cual hacerlo así. Cuando el usuario presione el botón directamente,
entonces se mandará llamar la función por medio del evento onClick, sin
embargo, si el usuario decide presionar enter sobre algún campo en lugar del
botón, entonces el evento onSubmit procesará la solicitud.
1. signup(e){
2. e.preventDefault()
3.
4. if(!this.state.license){
5. this.refs.submitBtnLabel.innerHTML =
6. 'Acepte los términos de licencia'
7. this.refs.submitBtnLabel.className = 'shake animated'
8. return
9. }else if(!this.state.userOk){
10. this.refs.submitBtnLabel.innerHTML =
11. 'Favor de revisar su nombre de usuario'
12. this.refs.submitBtnLabel.className = ''
13. return
14. }
15.
16. this.refs.submitBtnLabel.innerHTML = ''
17. this.refs.submitBtnLabel.className = ''
18.
19. let request = {
20. "name": this.state.name,
21. "username": this.state.username,
22. "password": this.state.password
23. }
24.
25. APIInvoker.invokePOST('/signup',request, response => {
26. //browserHistory.push('/login');
27. alert('Usuario registrado correctamente')
28. },error => {
29. this.refs.submitBtnLabel.innerHTML = response.error
30. this.refs.submitBtnLabel.className = 'shake animated'
31. })
32. }
Veamos que en las líneas 4 y 9 validamos que los campos license y userOk del
estado sean true, de lo contrario mandamos el error correspondiente al usuario.
Si las validaciones son correctas, entonces limpiamos cualquier error sobre la
vista (líneas 16 y 17).
167 | Página
En la línea 19 creamos el request para crear el usuario por medio del API REST,
el cual contiene el nombre (name), el nombre de usuario (username) y el password.
Librería de animación
En la línea 7 y 30 estamos haciendo uso de la clase de
estilo animated, la cual es proporcionada por librería
de animación Animate, la cual sirve para dar efectos
de entrada y salida a los elementos, puede revisar su
documentación en
https://daneden.github.io/animate.css/
Esta librería la importamos en el archivo index.html
(línea 5)
Página | 168
El componente login
Este componente es muy parecido al anterior, pero mucho más simples, pues
este solo tiene dos campos de texto, tiene el botón para iniciar sesión y un link
que lleva al usuario a la pantalla de registro en caso de que no tenga una cuenta
registrada.
169 | Página
20. if(field === 'username'){
21. value = value.replace(' ','').replace('@','').substring(0, 15)
22. this.setState(update(this.state,{
23. [field] : {$set: value}
24. }))
25. }
26.
27. this.setState(update(this.state,{
28. [field] : {$set: value}
29. }))
30. }
31.
32. login(e){
33. e.preventDefault()
34.
35. let request = {
36. "username": this.state.username,
37. "password": this.state.password
38. }
39.
40. APIInvoker.invokePOST('/login',request, response => {
41. window.localStorage.setItem("token", response.token)
42. window.localStorage.setItem("username", response.profile.userName)
43. window.location = '/'
44. },error => {
45. this.refs.submitBtnLabel.innerHTML = error.message
46. this.refs.submitBtnLabel.className = 'shake animated'
47. console.log("Error en la autenticación")
48. })
49. }
50.
51. render(){
52.
53. return(
54. <div id="signup">
55. <div className="container" >
56. <div className="row">
57. <div className="col-xs-12">
58. </div>
59. </div>
60. </div>
61. <div className="signup-form">
62. <form onSubmit={this.login.bind(this)}>
63. <h1>Iniciar sesión en Twitter</h1>
64.
65. <input type="text" value={this.state.username}
66. placeholder="usuario" name="username" id="username"
67. onChange={this.handleInput.bind(this)}/>
68. <label ref="usernameLabel" id="usernameLabel"
69. htmlFor="username"></label>
70.
71. <input type="password" id="passwordLabel"
72. value={this.state.password} placeholder="Contraseña"
73. name="password" onChange={this.handleInput.bind(this)}/>
74. <label ref="passwordLabel" htmlFor="passwordLabel"></label>
75.
76. <button className="btn btn-primary btn-lg " id="submitBtn"
77. onClick={this.login.bind(this)}>Regístrate</button>
78. <label ref="submitBtnLabel" id="submitBtnLabel" htmlFor="submitBtn"
79. className="shake animated hidden "></label>
80. <p className="bg-danger user-test">
81. Crea un usuario o usa el usuario
82. <strong>test/test</strong></p>
83. <p>¿No tienes una cuenta? Registrate</p>
84. </form>
85. </div>
Página | 170
86. </div>
87. )
88. }
89. }
90. export default Login
En el constructor no hay mucho que ver, salvo que iniciamos el estado (línea 9)
con el nombre de usuario y password con un valor en blanco.
En la función render creamos el campo username (línea 65) de tipo text el cual
está ligado al estado mediante this.state.username, creamos otro campo de
texto de tipo password también ligado al estado, mediante this.state.password.
Creamos el botón (línea 76) que procesará la autenticación mediante la función
login. Como podemos ver, estamos haciendo exactamente lo mismo que en el
componente Signup.
171 | Página
Por último, regresaremos al archivo App.js para retornar el componente Login,
para esto, agregamos el import al componente Login y modificaremos la función
render para que se vea de la siguiente manera:
1. render(){
2. return (
3. <Login/>
4. )
5. }
Si hemos hecho todo bien hasta ahora, ya deberíamos de poder ver la página de
login al actualizar el navegador, y cuando esto pase, podremos intentar
autenticarnos con el usuario test/1234.
El token tiene una vigencia de 24 hrs, por lo que, al pasar este tiempo, el token
ya no servirá y será necesario autenticarnos de nuevo para renovarlo.
Página | 172
173 | Página
Resumen
Hemos hablado de que los controles no controlados son por lo general una mala
práctica, pues difieren de la propuesta de trabajo de React, aunque también
hemos dicho que existe situaciones en las que podría ser una buena idea, lo
importante es no abusar del uso de controles no controlados.
Página | 174
Ciclo de vida de los componentes
Capítulo 8
El ciclo de vida (life cycle) de un componente, representa las etapas por las que
un componente pasa durante toda su vida, desde la creación hasta que es
destruido. Conocer el ciclo de vida ayuda de un componente es muy importante
debido a que nos permite saber cómo es que un componente se comporta
durante todo su tiempo de vida y nos permite prevenir la gran mayoría de los
errores que se provocan en tiempo de ejecución.
175 | Página
En la imagen podemos apreciar los métodos que conforman el ciclo de vida de
un componente en React, los métodos están listados en el orden en que React
los ejecuta, aunque hay métodos que no siempre se ejecutan.
Function componentWillMount
1. componentWillMount(){
2. //Any action
3. }
En este punto, los elementos del componente no podrán ser accedidos por medio
del DOM, pues aún no han sido creados.
Function render
1. render(){
2. // Vars and logic sección
3. return (
4. //JSX section
5. )
6. }
Function componentDidMount
Página | 176
elementos del DOM. En este punto todos los elementos ya existen en el DOM y
pueden ser accedidos.
1. componentDidMount(){
2. //Any action
3. }
Function componentWillReceiveProps
Recibe la variable nextProps que contiene los valores de las nuevas propiedades,
por lo que es posible comparar los nuevos valores (nextProps) contra los props
actuales (this.props) para determinar si tenemos que realizar alguna acción para
actualizar el componente.
1. componentWillReceiveProps(nextProps){
2. // Any action
3. }
Hay que tener en cuenta que esta función se puede llamar incluso si no ha habido
ningún cambio en las props, por ejemplo, cuando el componente padre es
actualizado.
Function shouldComponentUpdate
1. shouldComponentUpdate(nextProps, nextState) {
2. // Any action
3. return boolean
4. }
177 | Página
Function componentWillUpdate
1. componentWillUpdate(nextProps, nextState) {
2. // Any action
3. }
Function componentDidUpdate
Esta función se utiliza para realizar operaciones sobre los elementos del DOM o
para consumir recursos de red, imágenes o servicios del API. Sin embargo, es
necesario validar el nuevo estado y props contra los anteriores, para determinar
si es necesario realizar alguna acción.
1. componentDidUpdate(prevProps, prevState){
2. // Any action
3. }
Function componentWillUnmount
1. componentWillUnmount() {
2. // Any action
Página | 178
3. }
179 | Página
Una vez que React ha terminado de renderizar el componente mediante
render(), ejecuta la función componentDidMount para darle la oportunidad a la
aplicación de realizar operaciones sobre los elementos del DOM. En este punto
es recomendable realizar carga de datos o simplemente actualizar elementos
directamente sobre el DOM.
Cuando React detecta cambios en el estado, lo primero que hará será validar si
el componente debe ser o no actualizado, para esto, existe la función
shouldComponentUpdate. Esta función deberá retornar true en caso de que el
componente requiera una actualización y false en caso contrario. Si esta función
retorna false, el ciclo de vida se detiene y no se ejecuta el resto de los métodos.
Página | 180
Cuando el componente es actualizado en pantalla, se ejecuta la función
componentDidUpdate, el cual permite realizar carga de datos o realizar operaciones
que requiera de la existencia de los elementos en el DOM.
Primero que nada, hay que resaltar que el ciclo de vida de la actualización de las
propiedades, es el mismo que el ciclo de vida del cambio de estado, con una
única diferencia, en este ciclo se ejecuta primero que nada la función
componentWillReceiveProps, el cual recibe como parámetro las nuevas
propiedades enviadas, incluso antes de actualizar el componente con estas
propiedades.
181 | Página
La función componenteWillUpdate es ejecutada siempre y cuando el método
shouldComponentUpdate retorna true y permite realizar carga de datos del servidor
o actualizar el estado. En este punto los elementos no existen en el DOM.
Página | 182
El componente TwitterApp
183 | Página
30. });
31. window.localStorage.setItem("token", response.token)
32. window.localStorage.setItem("username", response.profile.userName)
33. },error => {
34. console.log("Error al autenticar al autenticar al usuario " );
35. window.localStorage.removeItem("token")
36. window.localStorage.removeItem("username")
37. browserHistory.push('/login');
38. })
39. }
40. }
41.
42. render(){
43.
44. return (
45. <div id="mainApp">
46. <Choose>
47. <When condition={!this.state.load}>
48. <div className="tweet-detail">
49. <i className="fa fa-circle-o-notch fa-spin fa-3x fa-fw"></i>
50. </div>
51. </When>
52. <When condition={this.props.children == null
53. && this.state.profile != null}>
54. <TweetsContainer/>
55. </When>
56. <Otherwise>
57. {this.props.children}
58. </Otherwise>
59. </Choose>
60. <div id="dialog"/>
61. </div>
62. )
63. }
64. }
65. export default TwitterApp;
La segunda condición (línea 52) valida si el perfil del usuario ya fue cargado, lo
cual solo ocurrirá cuando el usuario se encuentra autenticado. En la condición
vemos la instrucción this.props.children el cual analizaremos a continuación.
Página | 184
Ya explicada la vista, entraremos en los detalles acerca de la forma en que un
usuario es autenticado una vez que inicia la aplicación, por tal motivo
analizaremos la función componentWillMount de la cual ya hemos hablado en el
pasado. Esta función tiene dos posibles escenarios, los cuales describiremos a
continuación:
Como te habrás dado cuenta, estamos haciendo uso del objeto browserHistory,
el cual es una utilidad del módulo react-router que no hemos utilizado hasta el
momento, por lo que será necesario instalarlo mediante npm, para lo cual
utilizaremos la instrucción
npm install --save [email protected]
npm install --save [email protected]
Por otra parte, tendremos que actualiza drásticamente el archivo App.js para que
quede de la siguiente manera:
185 | Página
1. import React from 'react'
2. import { render } from 'react-dom'
3. import APIInvoker from "./utils/APIInvoker"
4. import TwitterApp from './TwitterApp'
5. import Signup from './Signup'
6. import Login from './Login'
7. import { Router, Route, browserHistory, IndexRoute } from "react-router";
8.
9. var createBrowserHistory = require('history/createBrowserHistory')
10.
11. render((
12. <Router history={ browserHistory }>
13. <Router component={TwitterApp} path="/">
14. <Route path="/signup" component={Signup}/>
15. <Route path="/login" component={Login}/>
16. </Router>
17. </Router>
18. ), document.getElementById('root'));
Solo me adelantaré un poco, si ves la línea 13, podrás ver que el componente
Raíz es TwitterApp y dentro de él, están los componentes Signup (línea 14) en el
path /signup y Login (línea 14) en el path /login. Ahora bien, ¿recuerdas la
propiedad this.props.children? pues lo que estamos indicando es que tanto el
componente Login como Signup son hijos de TwitterApp pero solo se activan bajo
ciertos Paths, ¿recuerdas la instrucción browserHistory.push(‘/login’) y
browserHistory.push(‘/signup)? Pues lo que hacemos es justamente activar los
componentes hijos, redireccionando al usuario.
Página | 186
Fig. 97 - Usuario autenticado.
El componente TwitterDashboard
187 | Página
TwitterDashboard.js en el path /app, el cual se deberá ver de la siguiente
manera:
Página | 188
El componente Profile
189 | Página
53. <Link to={"/"+this.props.profile.userName + "/followers"}>
54. <p className="profile-resumen-title">SEGUIDORES</p>
55. <p className="profile-resumen-value">
56. {this.props.profile.followers}</p>
57. </Link>
58. </div>
59. </div>
60. </div>
61. </div>
62. </aside>
63. )
64. }
65. }
66.
67. Profile.propTypes = {
68. profile: PropTypes.object.isRequired
69. }
70.
71. export default Profile;
Página | 190
Avatar: línea 25
Nombre: líneas 26 a 29,
Nombre de usuario: líneas 30 a 32
Contador de Tweets: Líneas 39 a 42
Siguiendo: Líneas 46 a 49
Seguidores: Líneas 53 a 56
Finalmente, tendremos que agregar las clases de estilo para que el componente
se vea correctamente, para esto, regresaremos al archivo styles.css y
agregaremos las siguientes clases de estilo al final del archivo.
191 | Página
29. #profile .profile-banner a{
30. min-height: 100px;
31. background-size: cover;
32. display: block;
33.
34. }
35.
36. #profile .profile-body{
37. position: relative;
38. padding-top: 5px;
39. padding-bottom: 10px;
40. }
41.
42. #profile .profile-body img{
43. position: absolute;
44. display: inline-block;
45. border: 3px solid #fafafa;
46. border-radius: 8px;
47. height: 75px;
48. width: 75px;
49. left: 10px;
50. top: -30px;
51. }
52.
53. #profile .profile-body > a{
54. margin-left: 90px;
55. color: inherit;
56. }
57.
58. .profile-body > a:hover{
59. text-decoration: underline;
60. }
61.
62. #profile .profile-resumen a {
63. color:#657786;
64. }
65.
66. #profile .profile-resumen a:hover{
67. color: #1B95E0;
68. }
69.
70. #profile .profile-resumen a .profile-resumen-title{
71. font-size: 10px;
72. margin: 0px;
73. color:inherit;
74. transition: 0.5s;
75. }
76.
77. #profile .profile-resumen a .profile-resumen-value{
78. color: #1B95E0;
79. font-size: 18px;
80. }
81.
82. #profile .profile-name,
83. #profile .profile-username{
84. display: block;
85. margin: 0px;
86. }
87.
88. #profile .profile-username{
89. color: #66757f;
90. }
91.
92. #profile .profile-name{
93. font-weight: bold;
94. font-size: 18px;
Página | 192
95. }
En este punto no es posible probar los cambios, sino hasta terminar el siguiente
componente.
El componente SuggestedUsers
193 | Página
41. </div>
42. <a href={"/" + user.userName}
43. className="btn btn-primary btn-sm">
44. <i className="fa fa-user-plus" aria-hidden="true"></i>
45. Ver perfil</a>
46. </div>
47. </div>
48. </For>
49. </If>
50. </aside>
51. )
52. }
53. }
54. export default SuggestedUser;
Por otra parte, la función render, validará la propiedad load (línea 29) para
determinar si es posible mostrar los usuarios recomendados. Entonces, si la carga
ya termino, se realizará un <For> por cada usuario de la lista (línea 30). En cada
iteración se imprimiera en pantalla, la imagen del avatar (línea 33), nombre y
nombre de usuario (líneas 37 a 40) y un botón nos dirija al perfil del usuario
recomendado (línea 42 a 45).
Página | 194
Para finalizar el componente, tendremos que agregar las clases de estilo.
Regresaremos al archivo styles.css y agregaremos las siguientes clases de estilo
al final del archivo:
1. render(){
195 | Página
2.
3. return (
4. <div id="mainApp">
5. {/* <Toolbar profile={this.state.profile} selected="home"/> */}
6. <Choose>
7. <When condition={!this.state.load}>
8. <div className="tweet-detail">
9. <i className="fa fa-circle-o-notch fa-spin fa-3x fa-fw"></i>
10. </div>
11. </When>
12. <When condition={this.props.children == null
13. && this.state.profile != null}>
14. <TwitterDashboard profile={this.state.profile}/>
15. </When>
16. <Otherwise>
17. {this.props.children}
18. </Otherwise>
19. </Choose>
20. <div id="dialog"/>
21. </div>
22. )
23. }
El componente Reply
Página | 196
Fig. 102 - Estado inicial del componente Reply.
197 | Página
30. name="message"
31. type="text"
32. maxLength = {config.tweets.maxTweetSize}
33. placeholder="¿Qué está pensando?"
34. className={this.state.focus ? 'reply-selected' : ''}
35. value={this.state.message}
36. />
37. <If condition={this.state.image != null} >
38. <div className="image-box"><img src={this.state.image}/></div>
39. </If>
40.
41. </div>
42. <div className={this.state.focus ? 'reply-controls' : 'hidden'}>
43. <label htmlFor={"reply-camara-" + randomID}
44. className={this.state.message.length===0 ?
45. 'btn pull-left disabled' : 'btn pull-left'}>
46. <i className="fa fa-camera fa-2x" aria-hidden="true"></i>
47. </label>
48.
49. <input href="#" className={this.state.message.length===0 ?
50. 'btn pull-left disabled' : 'btn pull-left'}
51. accept=".gif,.jpg,.jpeg,.png"
52. type="file"
53. id={"reply-camara-" + randomID}>
54. </input>
55.
56. <span ref="charCounter" className="char-counter">
57. {config.tweets.maxTweetSize - this.state.message.length }</span>
58.
59. <button className={this.state.message.length===0 ?
60. 'btn btn-primary disabled' : 'btn btn-primary '}
61. >
62. <i className="fa fa-twitch" aria-hidden="true"></i> Twittear
63. </button>
64. </div>
65. </section>
66. )
67. }
68. }
69.
70. Reply.propTypes = {
71. profile: PropTypes.object,
72. operations: PropTypes.object.isRequired
73. }
74.
75. export default Reply;
Antes de continuar, será necesario instalar el módulo UUID que nos ayudará para
la generación de ID dinámicos mediante el comando:
npm install --save uuid
Página | 198
focus: Indica si el área de texto tiene el foco, con la finalidad de mostrar
el botón para la carga de la imagen y el botón para enviar el Tweet.
message: esta propiedad está ligada al área de texto, por lo que todo lo
que escribimos en él, actualiza esta propiedad.
Image: En esta propiedad se guarda la imagen carga.
Dentro del método render, podríamos dividir la vista en dos partes, lo que se ve
cuándo el componente no está activo (focus =false) y cuando está activo.
Cuando no está activo y solo se requiere mostrar un área de texto, solo se verá
lo que está entre las líneas 24 a 41 y lo que está entre las líneas 42 a 64 solo se
mostrará cuando el área de texto obtenga el foco.
También podemos ver el control esta mapeado contra la propiedad message del
estado (línea 35), por lo que todos lo que escribamos actualizará esta propiedad.
También tenemos un placeholder para mostrar un texto por default (línea 32).
199 | Página
Una vez esta sección es mostrada, tenemos 3 componentes que van a mostrarse,
el botón para cargar la foto, el contador de caracteres y el botón de Twittear.
Veamos la siguiente imagen:
Por otra parte, el botón es muy simple, pues solo mandará llamar una función en
el evento onClick. En este momento no hemos implementado la función, por lo
que más adelante lo retomaremos.
Página | 200
módulo UUID, en segundo lugar y debido a que Reply es un componente pensado
para ser reutilizado, es posible que exista más de una instancia del componente
en una misma vista, es por ello, que requerimos de un identificador que nos
ayude a distinguir a cada uno, y es allí donde entra el módulo UUID, pues nos
permite crear un ID que no se repita entre cada una de las instancias de este
componente.
Configuración
1. module.exports = {
2. debugMode: true,
3. server: {
4. port: 3000,
5. host: "http://api.localhost"
6. },
7. tweets: {
8. maxTweetSize: 140
9. }
10. }
201 | Página
Textarea:
o onFocus: Cuando el control gane el foco, deberá actualizar la
propiedad focus del estado, disparando la actualización de todo el
componente para mostrar el resto de controles.
o onKeyDown: Cuando el usuario presione la tecla escape, el
componente se deberá limpiar y pasar a su estado inicial.
o onBlur: Cuando el control pierda el foco deberá limpiar el componente
dejándolo en su estado inicial, siempre y cuando no allá texto en el
textarea.
o onChange: actualizará la propiedad message del estado, sincronizado el
textarea con el estado.
Input file:
o onChange: cuando el usuario seleccione una foto el evento onChange
se disparará para cargar la foto y ponerla en la propiedad image del
estado.
Botón Twittear:
o onClick: cuando el usuario presione el botón twittear, se invocará una
funcione para guardar el Tweet y regresar el componente en su estado
inicial.
Control TextArea
Una vez mencionados los eventos esperados, iniciaremos con los eventos del
textarea, para lo deberemos actualizar para agregar los siguientes eventos:
1. <textarea
2. ref="reply"
3. name="message"
4. type="text"
5. maxLength = {config.tweets.maxTweetSize}
6. placeholder="¿Qué está pensando?"
7. className={this.state.focus ? 'reply-selected' : ''}
8. value={this.state.message}
9. onKeyDown={this.handleKeyDown.bind(this)}
10. onBlur={this.handleMessageFocusLost.bind(this)}
11. onFocus={this.handleMessageFocus.bind(this)}
12. onChange={this.handleChangeMessage.bind(this)}
13. />
El primer evento a analizar será cuando toma el foco, pues es lo que sucede
primero, para ello agregaremos la siguiente función a nuestro componente:
1. handleMessageFocus(e){
2. let newState = update(this.state,{
3. focus: {$set: true}
4. })
5. this.setState(newState)
6. }
Como podemos ver, esta función únicamente cambia la propiedad focus del
estado a true. Este pequeño cambio hace que el componente se actualice y
Página | 202
muestre el botón para cargar una imagen, el contador de caracteres y el botón
para envía el Tweet.
1. handleChangeMessage(e){
2. this.setState(update(this.state,{
3. message: {$set: e.target.value}
4. }))
5. }
Esta función es tan simple como actualizar la propiedad message del estado a
medida que el usuario capturar el mensaje del Tweet.
1. handleKeyDown(e){
2. //Scape key
3. if(e.keyCode === 27){
4. this.reset();
5. }
6. }
7.
8. reset(){
9. let newState = update(this.state,{
10. focus: {$set: false},
11. message: {$set: ''},
12. image: {$set:null}
13. })
14. this.setState(newState)
15.
16. this.refs.reply.blur();
17. }
Por otra parte, la función reset además de limpiar el estado, invoca la función
blur del textarea, el cual se accede por medio de la referencia (refs), línea 16.
Con esto, el componente pierde el foco.
1. handleMessageFocusLost(e){
2. if(this.state.message.length=== 0){
203 | Página
3. this.reset();
4. }
5. }
1. imageSelect(e){
2. e.preventDefault();
3. let reader = new FileReader();
4. let file = e.target.files[0];
5. if(file.size > 1240000){
6. alert('La imagen supera el máximo de 1MB')
7. return
8. }
9.
10. reader.onloadend = () => {
11. let newState = update(this.state,{
12. image: {$set: reader.result}
13. })
14. this.setState(newState)
15. }
16. reader.readAsDataURL(file)
17. }
Con ayuda de la función readAsDataURL (línea 16) del objeto FileReader iniciamos
la carga del archivo.
Control Button
Página | 204
Finalmente tenemos el botón para enviar el Tweet, para lo cual,
implementaremos el evento onClick:
1. <button className={this.state.message.length===0 ?
2. 'btn btn-primary disabled' : 'btn btn-primary '}
3. onClick={this.newTweet.bind(this)}>
4. <i className="fa fa-twitch" aria-hidden="true"></i> Twittear
5. </button>
1. newTweet(e){
2. e.preventDefault();
3.
4. let tweet = {
5. _id: uuidV4(),
6. _creator: {
7. _id: this.props.profile._id,
8. name: this.props.profile.name,
9. userName: this.props.profile.userName,
10. avatar: this.props.profile.avatar
11. },
12. date: Date.now,
13. message: this.state.message,
14. image: this.state.image,
15. liked: false,
16. likeCounter: 0
17. }
18.
19. this.props.operations.addNewTweet(tweet)
20. this.reset();
21. }
Una vez que hemos creado el request mandamos llamar a la función addNewTweet
la cual es recibida como un prop dentro del objeto this.props.operations. Esta
función será que se encargue realmente de crear el Tweet, por lo este
componente no se deberá preocupar más por la creación. Vamos a analizar la
función addNewTweet más adelante.
205 | Página
Finalmente, se manda llamar la función reset, la cual limpiará el componente y
lo dejará en su estado inicial.
1. render(){
2.
3. let operations = {
4. addNewTweet: this.addNewTweet.bind(this)
5. }
6.
7. return (
8. <main className="twitter-panel">
9. <Choose>
10. <When condition={this.props.onlyUserTweet} >
11. <div className="tweet-container-header">
12. TweetsDD
13. </div>
14. </When>
15. <Otherwise>
16. <Reply profile={this.props.profile} operations={operations}/>
17. </Otherwise>
18. </Choose>
19. <If condition={this.state.tweets != null}>
20. <For each="tweet" of={this.state.tweets}>
21. <Tweet key={tweet._id} tweet={tweet}/>
22. </For>
23. </If>
24. </main>
25. )
26. }
Podemos apreciar dos cambios, por una parte, hemos creado una variable
llamada operations (línea 3) que contiene la referencia a la función addNewTweet,
observemos que hemos referenciado la función con bind(this), ya que, de lo
contrario, no funcionará.
Página | 206
1. addNewTweet(newTweet){
2. let oldState = this.state;
3. let newState = update(this.state, {
4. tweets: {$splice: [[0, 0, newTweet]]}
5. })
6.
7. this.setState(newState)
8.
9. //Optimistic Update
10. APIInvoker.invokePOST('/secure/tweet',newTweet, response => {
11. this.setState(update(this.state,{
12. tweets:{
13. 0 : {
14. _id: {$set: response.tweet._id}
15. }
16. }
17. }))
18. },error => {
19. console.log("Error al cargar los Tweets");
20. this.setState(oldState)
21. })
22. }
Analicemos que hace la función; primero que nada, recibe como parámetro el
Tweet a crear en la variable newTweet, lo segundo en hacer es respaldar el estado
actual en la variable oldState (línea 2), luego agregamos el nuevo Tweet a un
nuevo estado que hemos llamado newState, seguido actualizamos el estado del
componente con la variable newState, es decir con el nuevo Tweet que vamos a
crear. Para terminar, llamamos al servicio /secure/tweet del API REST para crear
el Tweet.
207 | Página
La verdad es que se podría haber hecho así, sin embargo, esta era una excelente
oportunidad para explicar una de las características más potentes React, que es
el Optimistic update.
Como acabamos de ver, el Optimistic Update nos permite actualizar la vista con
el nuevo Tweet sin tener la confirmación del servidor, lo que dará al usuario una
sensación de velocidad extraordinaria. Aunque, por desgracia, sabemos que
cualquier cosa puede fallar en cualquier momento, por lo que puede pasar que el
API nos regrese error o sea inaccesible en ese momento, es entonces cuando es
necesario realizar un Rollback.
En este punto ya está todo funcionando, pero falta agregar los estilos para que
todo se vea estéticamente bien, por lo que agregamos las siguientes clases de
estilo en el archivo styles.css:
Página | 208
25. .reply .reply-body textarea{
26. height: 35px;
27. display: block;
28. width: 100%;
29. border: 1px solid #DDE2E6;
30. outline: none;
31. padding-left: 10px;
32. padding-right: 10px;
33. resize: none;
34. border-radius: 5px;
35.
36. }
37.
38. .reply .reply-body .reply-selected{
39. height: 70px;
40. }
41.
42. .reply .reply-body .image-box{
43. display: block;
44. position: relative;
45. color: #F1F1F1;
46. padding: 10px;
47. border-left: 1px solid #DDE2E6;
48. border-right: 1px solid #DDE2E6;
49. border-bottom: 1px solid #DDE2E6;
50. border-radius: 0 0 5px 5px;
51. max-height: 250px;
52. width: 100%;
53. }
54.
55. .tweet-event{
56. position: absolute;
57. display: block;
58. left: 0;
59. right: 0;
60. top: 0;
61. bottom: 0;
62. z-index: 0;
63. }
64.
65. .reply .reply-body .image-box img{
66. display: inline-block;
67. position: relative;
68. max-height: 230px;
69. border-radius: 5px;
70. max-width: 100%;
71. }
72.
73.
74. .reply .reply-controls{
75. padding: 10px 0px 0px 0px;
76. text-align: right;
77. margin-left: 55px;
78. }
79.
80. .reply .reply-controls button{
81. font-size: 18px;
82. font-weight: bold;
83. }
84.
85. .reply .reply-controls button i{
86. color: inherit;
87. font-size: inherit;
88. }
89.
90. .reply .reply-controls .char-counter{
209 | Página
91. margin-right: 10px;
92. color: #333;
93. }
94.
95. input[type="file"]{
96. display: none;
97. }
Si hemos seguido todos los pasos hasta ahora, podrás guardar los cambios y
actualizar el navegador para ver como se ve nuestra aplicación hasta el
momento:
Página | 210
Fig. 108 - Nuevo Tweet crado.
Este componente ha sido por mucho el más complejo y largo de explicar, pues
tiene varios comportamientos que teníamos que explicar, de lo contrario, podrían
quedar dudas acerca de su funcionamiento.
Solo para recapitular lo que llevamos hasta el momento, te dejo esta imagen,
donde se ve la estructura actual de nuestro proyecto, para que la compares y
veas si todo está bien.
211 | Página
Página | 212
Resumen
Sin duda alguna, este ha sido unos de los capítulos más interesantes hasta el
momento, pues hemos aprendido el ciclo de vida de los componentes, lo cual es
clave para poder desarrollar aplicaciones correctamente.
Por otra parte, hemos hablado del concepto de Optimistic Update, una de las
ventajas que ofrece React para crear aplicaciones con una experiencia de
usuarios sin precedentes, pues crea una sensación de respuesta inmediata por
parte del servidor, incluso si este no responde a la misma velocidad.
También hemos visto con el Componente Reply, que las propiedades (props)
también pueden contener funciones, las cuales son transmitidas por los padres
hacia los hijos, con la intención de delegar la responsabilidad a otro componte.
Sin olvidar que hemos reforzado nuestros conocimientos acerca de la forma de
utilizar los eventos, como lo son el onClick, onBlur, onKeyDown, onChange, onFocus,
etc.
213 | Página
React Routing
Capítulo 9
Cuando la WEB inicio, las URL no representaban nada más que una simple
dirección a un documento, por lo que la estructura de la misma era casi
irrelevante. Sin embargo, con todas las mejoras que ha tenido la WEB, las URL
ha evolucionado a tal punto que hoy en día, son capases de darnos mucha
información acerca de donde estamos parados dentro de la aplicación. Veamos
el siguiente ejemplo:
http://twitter.com/juan/followers
Solo con ver esta URL puedo determinar que estoy en los seguidores (followers)
del usuario juan.
Podemos ver la ventaja evidente de crear URL amigables, no solo por estética y
que el usuario puede recordar mejor la URL, sino que también los buscadores
como Google toman en cuenta la URL para posicionar mejor nuestras páginas.
Veamos otro ejemplo rápido, imagina que tengo una página que vende cursos
online, por lo que cada curso debe de tener su URL, yo podría tener las siguientes
dos URL:
http://mysite.com/react
http://mysite.com/cursos/react
Cuál de las dos siguientes URL crees que es más descriptiva para vender un
curso, la primera URL me deja claro que se trata de React, pero no sé si sea un
artículo, un video, o cualquier otro caso, en cambio, la segunda URL me deja muy
en claro que se trata del curso de React.
Página | 214
Pues bien, para no entrar mucho en detalles, React Router es el módulo de React
que nos permite crear la navegación del usuario mediante las URL, utilizando un
concepto llamado Single Page App, el cual analizaremos a continuación.
El concepto single page app, consiste en aplicaciones que se ejecutan sobre una
única página, esto quiere decir que el navegador no requiere hacer una petición
GET al servidor para recuperar la siguiente página y mostrarla, en su lugar, React
crea la vista desde el navegador de forma dinámica, por lo que no es necesario
actualizar el navegador.
Debido a que React no requiere ir al servidor para obtener una página según la
URL ejecutada, debemos definir las reglas de navegación que tendrá nuestra
página mediante una serie de reglas.
215 | Página
Quiero que te tomes un momento y analices detenidamente el código anterior,
quisiera que seas capaz de descubrir cómo funciona las reglas con solo ver el
código. Desde luego que no espero que entiendas todo, pero sí que te des una
idea.
Lo siguiente que te puedes dar cuenta es que tenemos una serie de Route, los
cuales están anidados de forma jerárquica, es decir, unos dentro de otros. Cada
Route tiene un atributo path, el cual indica ante que URL se activarán, tiene,
además, un atributo llamado componente, el cual indica el componente asociado
a la URL.
Un error muy común cuando trabajamos con React Router es pensar que cuando
un path se activa, el único componente que se debería de ver es el componente
asociado al Route en cuestión, sin embargo, no es así. Cuando un Route se activa
por medio de la URL, lo que pasa es que en realidad el componente activado pasa
como un hijo (this.props.children) al componente padre. Si seguimos con el
ejemplo del path followers, si este se activará se crearía una estructura de
componentes parecida a la siguiente:
1. <TwitterApp>
2. <UserPage>
3. <Followers/>
4. <UserPage>
5. <TwitterApp>
IndexRoute
Existe ocasiones en donde un Router puede tener más de un Router hijo, lo que
indica que el Router padre puede mostrar dentro de sí, más de un componente,
Página | 216
sin embargo, solo un componente puede estar activo a la vez, veamos el
siguiente ejemplo:
History
El History es la forma en que React-router interpreta la barra de la navegación
del navegador, con la finalidad de saber que reglas de Route se han activado y
217 | Página
mostrar los componentes adecuados según la URL actual del navegador. Existen
básicamente tres formas distintas de crear un History, los cuales podríamos
llamar “in of the box” (o de caja), sin embargo, es posible crear tu propia
implementación de un objeto History y utilizarlo, aunque en la práctica es muy
raro ver una implementación custom. Los tres tipos más frecuentes son:
browserHistory
Mediante el BrowserHistory es posible crear URL mucho más limpias y claras, con
un formato parecido al siguiente: http://example.com/some/path. para esto, se
apoya en el objeto History del navegador para generar las URL.
Este tipo Historial es el más recomendado para aplicaciones web, pues permite
la creación de URL muy limpias y amigables, sin embargo, tiene un pequeño
inconveniente, y es que es necesaria una pequeña configuración del lado del
servidor. Te explico por qué:
Existen dos formas en las que un usuario puede navegar por una página, la
primera es navegar mediante los enlaces que hay en la misma página, y la
segunda es que el usuario coloque la URL completa en la barra de navegación y
presione “enter”. Pues bien, aunque aparentemente sea lo mismo, la realidad es
que no lo es, y esto se debe a que cuando nosotros presionamos un enlace desde
la página, React podrá capturar ese evento y mapearlo contra un <Route>, sin
embargo, cuando navegamos por medio de la barra de navegación, estamos
forzando al navegador a que busque una ruta en internet y refresque la pantalla.
Página | 218
Fig. 111 - Estado actual de la aplicación.
Como verás, no ha lanzado error. Es decir, la misma URL que pudimos acceder
por un enlacen, no la podemos acceder directamente por la barra de navegación.
219 | Página
Esto se debe a que, por default, nuestro servidor, en este caso la configuración de Webpack,
solo está respondiendo a las peticiones a la raíz del servidor y todos los archivos que estén
en la carpeta public de nuestro proyecto. es decir, hay una regla parecida a la siguiente:
Como puedes ver en el código, existe un oyente que escucha las peticiones en el
path /, y ante esa solicitud, regresa el archivo index.html, sin embargo, si llegara
una petición como la anterior /oscar, no sabría cómo resolverla y es allí donde
sale el error. Para solucionar este problema, requerimos crear nuestra propia
implementación de un Servidor y habilitar una función como la siguiente:
Como puedes ver, esta nueva función cambia ligeramente, pues escucha en /*,
donde el * indica cualquier cosa después de /. Lo que habilitaría URL’s como
/oscar, /oscar/followers, etc.
hashHistory
El HashHistory permite crear URL muy parecidas a las anteriores, sin embargo,
este antepone el signo de numeral (#) antes cada URL, por ejemplo
http://example.com/#/some/path. Este tipo de URL tiene la ventaja que no requiere
ninguna configuración por parte del servidor. Pues todas las rutas generadas son un ancla al
mismo documento raíz (/).
Este tipo de historiales no debe de utilizarse en ambientes productivos, pues generan URL
nada amigables y que pueden repercutir en el SEO. Además, este tipo de historial solo se
utiliza para realizar cosas muy simples o pruebas que no requieran de un servidor. Debido a
Página | 220
esto, no profundizaré en este tema. En su lugar, solo quería que supieras de su existencia
por si alguna vez te presentarás con ellas.
MemoryHistory
Link
Cuando trabajamos con react-router, hay que tener especial cuidado en la forma
en que creamos los enlaces con la etiqueta <a>, pues esta puede tener un
resultado adverso al esperado. Las etiquetas <a> fuerzan al navegador a realizar
una consulta al servidor y actualizar toda la página, provocando que todos
nuestros componentes se creen de nuevo, pasando por el ciclo de vida de
creación, como lo es el constructor, componentDidMount, render,
componentWillMount, etc. y puede provocar un degrado en el performance y la
experiencia de uso del usuario.
Para evitar esto, react-router ofrece un componente llamado <Link> el cual actual
exactamente como la etiqueta <a>, pero esta tiene la ventaja que no lanza un
request al servidor, si no que ejecuta las reglas de navegación de React-router.
Link da como resultado una etiqueta <a> al memento de renderizar en el
navegador.
1. <Link to={"/followers"}>
2. //Any element
221 | Página
3. </Link>
Link define únicamente el atributo to, el cual deberá definir el path al cual
redireccionaremos al usuario.
NOTA: Cabe mencionar que, no es obligatorio utilizar siempre Link, pues puede
haber ocasiones en las que utilizar un <a> puede ser una buena estrategia, sobre
todo cuando navegamos a una página en la cual es necesario actualizar toda la
información, en estos casos <a> puede ayudarnos, pues creará nuevamente los
componentes entorno a información totalmente nueva.
Props
Al igual que cualquier otro componente, es posible pasar propiedades a los Route,
los cuales son transmitidos a los componentes mediante
this.props.route.{prop-name}, donde {prop-name} es el nombre de la propiedad.
Veamos un ejemplo:
Ahora bien, existe un pequeño problema con las propiedades de los Route, y es
que no es posible pasar como propiedades los objetos creados dentro de los
componentes. Por ejemplo, el componente TwitterApp es el encargado de
autenticar al usuario, y es en este componente donde se crea el objeto del Perfil.
El Perfil es necesario prácticamente para todas los componentes que tenemos en
la aplicación, pues mediante este objeto sabes los datos del usuario autenticado,
pero si, por ejemplo, requerimos pasar este objeto a los Route hijos, no será
propiedades. En su lugar, tendremos que clonar los componentes hijos y definir
las propiedades en ese momento. Puede sonar extraño, pero es la manera que
propone React-router hacerlo en este momento.
Página | 222
Por lo general con los dos primeros parámetros es suficiente, como podemos ver
en este ejemplo:
URL params
Debido a que las aplicaciones cada vez requieren de la generación de URL más
amigables, hemos llegado al punto en que las URL pueden representar
parámetros para las aplicaciones, y de esta forma, saber qué información debe
de mostrar. Por ejemplo, la siguiente URL: http://localhost:8080/oscar o
http://localhost:8080/maria, estas dos URL deberían de llevarnos al perfil de
oscar y maría, y la página debería ser la misma, con la diferencia de la
información que muestra. Esto se hace debido a que oscar y maria, son
parámetros que React-router puede identificar y pasar como prop al componte.
Para lograr esto, utilizamos Route con el siguiente formato:
Esta URL la podríamos usar para ver un Tweet especifico de un usuario por medio
del ID del Tweet, por ejemplo, http://localhost:8080/oscar/tweet/110, donde
oscar es el parámetro (:user) y 110 es el parámetro (:id). Los parámetros
pueden ser recuperados mediante this.props.route.{prop-name}.
Hasta este punto hemos analizado lo más importante de react-router, pero sin
duda hay más cosas por explorar, por lo que, si quieres profundizar más en el
tema, te deje la documentación oficial para que lo análisis con más calma:
223 | Página
Mini Twitter (Continuación 3)
-- http://expressjs.com
Página | 224
npm install --save [email protected]
npm install --save [email protected]
npm install --save [email protected]
npm install -g nodemon
1. "scripts": {
2. "start": "nodemon server.js"
3. },
225 | Página
Con esto hemos hecho que, de ahora en adelante, cuando ejecutemos el
comando npm start, este ejecute el archivo server.js con nodemon, en lugar de
crear el servidor por default de Webpack
Página | 226
Implementando Routing en nuestro proyecto
El componente Toolbar
Iniciaremos con la creación del archivo Toolbar.js en el path /app, el cual deberá
tener la siguiente estructura:
227 | Página
15. window.location = '/login';
16. }
17.
18. render(){
19.
20. return(
21. <nav className="navbar navbar-default navbar-fixed-top">
22. <span className="visible-xs bs-test">XS</span>
23. <span className="visible-sm bs-test">SM</span>
24. <span className="visible-md bs-test">MD</span>
25. <span className="visible-lg bs-test">LG</span>
26.
27. <div className="container-fluid">
28. <div className="container-fluid">
29. <div className="navbar-header">
30. <a className="navbar-brand" href="#">
31. <i className="fa fa-twitter" aria-hidden="true"></i>
32. </a>
33. <ul id="menu">
34. <li id="tbHome" className="selected">
35. <Link to="/">
36. <p className="menu-item"><i
37. className="fa fa-home menu-item-icon" aria-hidden="true">
38. </i> <span className="hidden-xs hidden-sm">Inicio</span>
39. </p>
40. </Link>
41. </li>
42. </ul>
43. </div>
44. <If condition={this.props.profile != null} >
45. <ul className="nav navbar-nav navbar-right">
46. <li className="dropdown">
47. <a href="#" className="dropdown-toggle"
48. data-toggle="dropdown" role="button"
49. aria-haspopup="true" aria-expanded="false">
50. <img className="navbar-avatar"
51. src={this.props.profile.avatar}
52. alt={this.props.profile.userName}/>
53. </a>
54. <ul className="dropdown-menu">
55. <li><a href={"/"+this.props.profile.userName}>
56. Ver perfil</a></li>
57. <li role="separator" className="divider"></li>
58. <li><a href="#" onClick={this.logout.bind(this)}>
59. Cerrar sesión</a></li>
60. </ul>
61. </li>
62. </ul>
63. </If>
64. </div>
65. </div>
66. </nav>
67. )
68. }
69. }
70.
71. Toolbar.propTypes = {
72. profile: PropTypes.object
73. }
74.
75. export default Toolbar;
Este componte es sin duda uno de los más simples, pues en realidad solo muestra
un botón de inicio (línea 35 a 40) la cual regresará al usuario al inicio de la
aplicación ( / ).
Página | 228
Por otra parte, mostramos una foto del avatar del usuario (línea 50), que al
presionarse arrojará un menú (línea 45 a 62) con dos opciones, la primera nos
lleva al nuestro perfil de usuario (/:user), y la segunda opción es cerrar la sesión.
La opción de cerrar sesión detonará en la ejecución de la función logout.
1. render(){
2. return (
3. <div id="mainApp">
4. <Toolbar profile={this.state.profile} />
5. <Choose>
6. <When condition={!this.state.load}>
7. <div className="tweet-detail">
8. <i className="fa fa-circle-o-notch fa-spin fa-3x fa-fw"></i>
9. </div>
10. </When>
11. <When condition={this.props.children == null
12. && this.state.profile != null}>
13. <TwitterDashboard profile={this.state.profile}/>
14. </When>
15. <Otherwise>
16. {this.props.children}
17. </Otherwise>
18. </Choose>
19. <div id="dialog"/>
20. </div>
21. )
22. }
229 | Página
18.
19. .navbar-brand i{
20. color: #1DA1F2;
21. }
22.
23.
24. .navbar{
25. background-color: #FFFFFF;
26. height: 48px;
27. }
28.
29. .navbar-nav>li>a{
30. padding: 0px;
31. }
32.
33. .navbar-avatar{
34. height: 32px;
35. width: 32px;
36. border-radius: 50%;
37. margin: 9px 10px;
38. }
39.
40. #menu{
41. padding: 0px;
42. margin: 0px;
43. position: relative;
44. }
45.
46. #menu li{
47. display: inline-block;
48. color: #666;
49. position: relative;
50. }
51.
52. #menu li::before{
53. content: "";
54. position: absolute;
55. display: block;
56. left: 0px;
57. right: 0px;
58. height: 0px;
59. background-color: #1B95E0;
60. bottom: -2px;
61. z-index: 1000;
62. transition: 0.5s;
63. }
64.
65.
66.
67. #menu li:hover::before{
68. height: 5px;
69. }
70.
71. #menu li:hover{
72. color: #1DA1F2;
73. }
74.
75.
76. #menu li.selected::before{
77. height: 5px!important;
78. }
79.
80. #menu li.selected{
81. color: #1DA1F2;
82. }
83.
Página | 230
84.
85. #menu li a{
86. display: inline-block;
87. padding: 13px 18px 0px 0px;
88. font-size: 12px;
89. font-weight: bold;
90. color: inherit;
91. }
92.
93. #menu li a:focus{
94. text-decoration: none;
95. }
96.
97.
98. #menu li a span{
99. color: inherit;
100. }
101.
102.
103. #menu li .menu-item{
104. color: inherit;
105. padding: 0px 10px;
106. }
107.
108. #menu li .menu-item-icon{
109. font-size: 18px;
110. font-size: 24px;
111. vertical-align: sub;
112. padding-right: 5px;
113. color: inherit;
114. }
115.
116. @media (max-width: 576px) {
117. #menu li .menu-item{
118. padding: 0px 5px;
119. }
120.
121. #menu li a{
122. padding: 13px 0px 0px 0px;
123. margin-right: 5px;
124. }
125. }
231 | Página
Toques finales al componente Login
1. render(){
2.
3. return(
4. <div id="signup">
5. <div className="container" >
6. <div className="row">
7. <div className="col-xs-12">
8. </div>
9. </div>
10. </div>
11. <div className="signup-form">
12. <form onSubmit={this.login.bind(this)}>
13. <h1>Iniciar sesión en Twitter</h1>
14.
15. <input type="text" value={this.state.username}
16. placeholder="usuario" name="username" id="username"
17. onChange={this.handleInput.bind(this)}/>
18. <label ref="usernameLabel" id="usernameLabel"
19. htmlFor="username"></label>
20.
21. <input type="password" id="passwordLabel"
22. value={this.state.password} placeholder="Contraseña"
23. name="password" onChange={this.handleInput.bind(this)}/>
24. <label ref="passwordLabel" htmlFor="passwordLabel"></label>
25.
26. <button className="btn btn-primary btn-lg " id="submitBtn"
27. onClick={this.login.bind(this)}>Regístrate</button>
28. <label ref="submitBtnLabel" id="submitBtnLabel" htmlFor="submitBtn"
29. className="shake animated hidden "></label>
30. <p className="bg-danger user-test">Crea un usuario o usa el usuario
31. <strong>test/test</strong></p>
32. <p>¿No tienes una cuenta? <Link to="/signup">Registrate</Link> </p>
33. </form>
34. </div>
35. </div>
36. )
37. }
Página | 232
Fig. 117 - Nuevo link para crear una cuenta.
En el caso del componente Signup pasa algo similar al componente Login, pues
en este tendremos que agregar una ligar para redireccionar al usuario a la
pantalla de login en caso de que ya tenga una cuenta, para esto abriremos el
archivo Signup.js y modificaremos la función render para dejarla de la siguiente
manera:
1. render(){
2.
3. return (
4. <div id="signup">
5. <div className="container" >
6. <div className="row">
7. <div className="col-xs-12">
8.
9. </div>
10. </div>
11. </div>
12. <div className="signup-form">
13. <form onSubmit={this.signup.bind(this)}>
14. <h1>Únite hoy a Twitter</h1>
15. <input type="text" value={this.state.username}
16. placeholder="@usuario" name="username" id="username"
17. onBlur={this.validateUser.bind(this)}
233 | Página
18. onChange={this.handleInput.bind(this)}/>
19. <label ref="usernameLabel" id="usernameLabel"
20. htmlFor="username"></label>
21.
22. <input type="text" value={this.state.name} placeholder="Nombre"
23. name="name" id="name" onChange={this.handleInput.bind(this)}/>
24. <label ref="nameLabel" id="nameLabel" htmlFor="name"></label>
25.
26. <input type="password" id="passwordLabel"
27. value={this.state.password} placeholder="Contraseña"
28. name="password" onChange={this.handleInput.bind(this)}/>
29. <label ref="passwordLabel" htmlFor="passwordLabel"></label>
30.
31. <input id="license" type="checkbox" ref="license"
32. value={this.state.license} name="license"
33. onChange={this.handleInput.bind(this)} />
34. <label htmlFor="license" >Acepto los terminos de licencia</label>
35.
36. <button className="btn btn-primary btn-lg " id="submitBtn"
37. onClick={this.signup.bind(this)}>Regístrate</button>
38. <label ref="submitBtnLabel" id="submitBtnLabel" htmlFor="submitBtn"
39. className="shake animated hidden "></label>
40. <p className="bg-danger user-test">Crea un usuario o usa el usuario
41. <strong>test/test</strong></p>
42. <p>¿Ya tienes cuenta? <Link to="/login">Iniciar sesión</Link> </p>
43. </form>
44. </div>
45. </div>
46. )
47. }
Página | 234
Otros de los cambios está en la función Signup, el cual se encargar de crear la
cuenta. El cambio consiste en utilizar el módulo History para redireccionar al
usuario de forma automática a la pantalla de login, solo cuando el registro del
usuario se realizó exitosamente.
1. signup(e){
2. e.preventDefault()
3.
4. if(!this.state.license){
5. this.refs.submitBtnLabel.innerHTML =
6. 'Acepte los términos de licencia'
7. this.refs.submitBtnLabel.className = 'shake animated'
8. return
9. }else if(!this.state.userOk){
10. this.refs.submitBtnLabel.innerHTML =
11. 'Favor de revisar su nombre de usuario'
12. this.refs.submitBtnLabel.className = ''
13. return
14. }
15.
16. this.refs.submitBtnLabel.innerHTML = ''
17. this.refs.submitBtnLabel.className = ''
18.
19. let request = {
20. "name": this.state.name,
21. "username": this.state.username,
22. "password": this.state.password
23. }
24.
25. APIInvoker.invokePOST('/signup',request, response => {
26. browserHistory.push('/login');
27. alert('Usuario registrado correctamente');
28. },error => {
29. console.log("Error al cargar los Tweets");
30. this.refs.submitBtnLabel.innerHTML = response.error
31. this.refs.submitBtnLabel.className = 'shake animated'
32. })
33. }
Podemos realizar una prueba ahora mismo, para esto, creemos un nuevo usuario
desde la pantalla de Signup y si el registro sale bien, te debería de llevar
automáticamente a la pantalla de login.
El componente UserPage
235 | Página
El componente UserPage es el que utilizaremos para mostrar el perfil del usuario,
es decir, cuando entremos al path /:user. Este componente es sin duda unos de
los más complejos, pues está compuesto de otros componentes que juntos,
hacen que esta página toma forma. Además de esto, el componente tiene dos
estados, uno es de solo lectura y el otro es de edición.
Por otro lado, el modo de edición, solo se podrá habilitar cuando el usuario está
viendo su propio perfil, de esta forma, podrá modificar sus datos básicos, que
son: banner, avatar, nombre y la descripción.
View mode
Página | 236
Fig. 120 - Estructura de componentes de UserPage.
237 | Página
32. }
33.
34. follow(e){
35. let request = {
36. followingUser: this.props.params.user
37. }
38. APIInvoker.invokePOST('/secure/follow', request, response => {
39. if(response.ok){
40. this.setState(update(this.state,{
41. profile:{
42. follow: {$set: !response.unfollow}
43. }
44. }))
45. }
46. },error => {
47. console.log("Error al actualizar el perfil");
48. })
49. }
50.
51. render(){
52. let profile = this.state.profile
53. let storageUserName = window.localStorage.getItem("username")
54.
55. let bannerStyle = {
56. backgroundImage: 'url(' + (profile.banner) + ')'
57. }
58.
59. return(
60. <div id="user-page" className="app-container">
61. <header className="user-header">
62. <div className="user-banner" style={bannerStyle}>
63. </div>
64. <div className="user-summary">
65. <div className="container-fluid">
66. <div className="row">
67. <div className="hidden-xs col-sm-4 col-md-push-1
68. col-md-3 col-lg-push-1 col-lg-3" >
69. </div>
70. <div className="col-xs-12 col-sm-8 col-md-push-1
71. col-md-7 col-lg-push-1 col-lg-7">
72. <ul className="user-summary-menu">
73. <li className={this.props.route.tab === 'tweets' ?
74. 'selected':''}>
75. <Link to={"/" + profile.userName}>
76. <p className="summary-label">TWEETS</p>
77. <p className="summary-value">{profile.tweetCount}</p>
78. </Link>
79. </li>
80. <li className={this.props.route.tab === 'followings' ?
81. 'selected':''}>
82. <Link to={"/" + profile.userName + "/following" }>
83. <p className="summary-label">SIGUIENDO</p>
84. <p className="summary-value">{profile.following}</p>
85. </Link>
86. </li>
87. <li className={this.props.route.tab === 'followers' ?
88. 'selected':''}>
89. <Link to={"/" + profile.userName + "/followers" }>
90. <p className="summary-label">SEGUIDORES</p>
91. <p className="summary-value">{profile.followers}</p>
92. </Link>
93. </li>
94. </ul>
95.
96. <If condition={profile.follow != null &&
97. profile.userName !== storageUserName} >
Página | 238
98. <button className="btn edit-button"
99. onClick={this.follow.bind(this)} >
100. {profile.follow
101. ? (<span><i className="fa fa-user-times"
102. aria-hidden="true"></i> Siguiendo</span>)
103. : (<span><i className="fa fa-user-plus"
104. aria-hidden="true"></i> Seguir</span>)
105. }
106. </button>
107. </If>
108. </div>
109. </div>
110. </div>
111. </div>
112. </header>
113. <div className="container-fluid">
114. <div className="row">
115. <div className="hidden-xs col-sm-4 col-md-push-1 col-md-3
116. col-lg-push-1 col-lg-3" >
117. <aside id="user-info">
118. <div className="user-avatar">
119. <div className="avatar-box">
120. <img src={profile.avatar} />
121. </div>
122. </div>
123. <div>
124. <p className="user-info-name">{profile.name}</p>
125. <p className="user-info-username">@{profile.userName}</p>
126. <p className="user-info-description">
127. {profile.description}</p>
128. </div>
129. </aside>
130. </div>
131. <div className="col-xs-12 col-sm-8 col-md-7
132. col-md-push-1 col-lg-7">
133. </div>
134. </div>
135. </div>
136. </div>
137. )
138. }
139. }
140. export default UserPage;
Este servicio tiene doble propósito, pues si lo consumimos por medio del método
GET nos arrojara los datos del perfil, pero si lo ejecutamos por el método PUT,
239 | Página
estaremos haciendo una update. Por ahora estaremos estamos utilizando el
método GET, pues solo requerimos consultar los datos del usuario (línea 24) para
mostrarlos en pantalla. Si la consulta sale correctamente actualizamos el estado
con el nuevo perfil (línea 25); mientras que, si el perfil no se encuentra,
regresamos al usuario a la pantalla de inicio (/) (línea 30).
Lo segundo por implementar sería la barra de navegación que está justo por
debajo del banner. Esta barra es implementada mediante una lista <ul>, donde
cada ítem será una opción. Las opciones disponibles son:
Página | 240
Tweet: Lleva al usuario a la URL /{user}, es decir, al perfil del usuario.
Por default, en esta URL debemos ver solo los Tweets del usuario,
podemos como esta implementado en las líneas (75 a 78).
Siguiendo: Lleva al usuario a la URL /{user}/following, es decir, nos
lleva al perfil del usuario, pero nos muestra a los usuarios que sigue.
Podemos ver como quedo implementado en las líneas (82 a 85).
Seguidores: lleva al usuario a la URL /{user}/followers, es decir, nos
lleva al perfil del usuario, pero nos muestra las personas que lo siguen.
Podemos ver como quedo implementado en las líneas (89 a 92).
Este botón mandará llamar la función follow (línea 34), la cual es la encargada
de comunicarse con el API para seguir o dejar de seguir a un usuario. El servicio
utilizado para seguir o dejar de seguir a un usuario es /secure/follow, la cual
únicamente necesita que le enviemos el usuario al que queremos seguir, el API
determinará si ya seguimos al usuario o no y en base a eso, será la operación a
realizar.
Para concluir con el modo de solo lectura, solo nos quedaría la parte de los datos
básicos del usuario, compuestos por los campos que podemos ver a continuación:
241 | Página
Fig. 122 - Datos básicos del usuario.
Con esta última sección hemos concluido el estado de solo lectura, por lo que el
siguiente paso será agregar la regla /{user} a nuestras reglas de ruteo del
archivo App.js, para lo cual agregaremos las líneas marcadas:
Hemos agregado el Route con el path (:user) para atender las peticiones /{user}
y lo hemos ligado al componente UserPage, así como hemos agregado el import
correspondiente de este nuevo componente.
Página | 242
En este punto solo restaría agregar las clases de estilo para que los componentes
se vea correctamente, por lo que regresaremos al archivo styles.css y
agregaremos los siguientes estilos al final del archivo:
243 | Página
62. }
63.
64. #user-page .user-header .user-summary .user-avatar img{
65. height: 100%;
66. width: 100%;
67. }
68.
69. .select-avatar{
70. position: absolute;
71. left: 0px;
72. right: 0px;
73. bottom: 0px;
74. top: 0px;
75. padding-top: 50px;
76. font-size: 20px;
77. font-weight: bold;
78. }
79.
80. .select-avatar:hover{
81. padding-top: 40px;
82. border: 5px solid tomato;
83. }
84.
85. #user-page .user-avatar{
86. display: inline-block;
87. height: 200px;
88. width: 200px;
89. border-radius: 10px;
90. left: 50px;
91. top: -100px;
92. overflow: hidden;
93. border-radius: 12px;
94. box-sizing: content-box;
95. border: 5px solid #fafafa;
96. box-shadow: 0 0 3px #999;
97. }
98.
99. #user-page .user-avatar img{
100. height: 100%;
101. width: 100%;
102. }
103.
104. #user-page .user-header .user-summary .user-summary-menu{
105. margin: 0px;
106. padding: 0px;
107. display: inline-block;
108. }
109.
110. #user-page .user-header .user-summary .user-summary-menu li{
111. text-align: center;
112. padding: 0px;
113. display: inline-block;
114. position: relative;
115.
116. }
117.
118. #user-page .user-header .user-summary .user-summary-menu li a{
119. padding: 15px 15px 0px;
120. display: inline-block;
121. position: relative;
122. }
123.
124. #user-page .user-header .user-summary .user-summary-menu li a::before{
125. content: "";
126. display: block;
127. position: absolute;
Página | 244
128. left: 0px;
129. right: 0px;
130. height: 0px;
131. bottom: 0px;
132. background: #1B95E0;
133. transition: 0.3s;
134.
135. }
136.
137. #user-page .user-header .user-summary .user-summary-menu li a:hover::before{
138. display: block;
139. height: 5px;
140. }
141.
142. #user-page .user-header .user-summary .user-summary-
menu li.selected a::before{
143. display: block;
144. height: 5px;
145. }
146.
147. #user-page .user-header .user-summary .user-summary-menu li .summary-label{
148. font-size: 11px;
149. margin: 0px;
150. }
151.
152. #user-page .user-header .user-summary .user-summary-menu li .summary-value{
153. font-weight: bold;
154. font-size: 18px;
155. color: #666;
156.
157. }
158.
159. #user-page .user-header .user-summary .edit-button{
160. margin: 15px 0px;
161. float: right;
162. }
163.
164. .tweet-footer{
165. padding-top: 10px;
166. }
167.
168. #user-info{
169. top: -180px;
170. position: absolute;
171. display: block;
172. position: relative;
173. padding: 10px;
174. margin-left: 35px;
175. max-width: 350px;
176. width: 280px;
177. max-width: 100%;
178. float: right;
179.
180. }
181.
182. #user-info .user-info-edit{
183. padding: 10px;
184. background-color: #E8F4FB;
185.
186. }
187.
188. #user-info .user-info-edit .user-info-username{
189. color: #1B96E0;
190. margin-top: 10px;
191. }
192.
245 | Página
193. #user-info .user-info-edit textarea,
194. #user-info .user-info-edit input{
195. display: block;
196. width: 100%;
197. border: 1px solid #A3D4F2;
198. outline: none;
199. border-radius: 5px;
200. padding: 5px;
201. }
202.
203. #user-info .user-info-edit textarea{
204. resize: none;
205. height: 220px;
206. }
207.
208. #user-info .user-info-name{
209. font-size: 22px;
210. font-weight: bold;
211. margin: 0px;
212. }
213.
214. #user-info .user-info-username{
215. font-size: 14px;
216. }
217.
218. #user-info .user-info-description{
219.
220. }
221.
222. @media (min-width: 576px) {
223. #user-page .user-avatar{
224. width: 150px;
225. height: 150px;
226. }
227.
228. #user-info{
229. top: -150px;
230. }
231. }
232.
233. @media (min-width: 1200px) {
234. #user-info{
235. top: -180px;
236. }
237.
238. #user-page .user-avatar{
239. width: 200px;
240. height: 200px;
241. }
242. }
243.
244. @media (min-width: 1000px) {
245. #user-page .user-header .user-banner{
246. height: 300px;
247. }
248. }
249.
250. @media (min-width: 1400px) {
251. #user-page .user-header .user-banner{
252. height: 400px;
253. }
254. }
255.
256. @media (min-width: 1800px) {
257. #user-page .user-header .user-banner{
258. height: 500px;
Página | 246
259. }
260. }
1. <Choose>
2. <When condition={!this.state.load}>
3. <div className="tweet-detail">
4. <i className="fa fa-circle-o-notch fa-spin fa-3x fa-fw"></i>
5. </div>
6. </When>
7. <When condition={this.props.children == null
8. && this.state.profile != null}>
9. <TwitterDashboard profile={this.state.profile}/>
10. </When>
11. <Otherwise>
12. {this.props.children}
13. </Otherwise>
14. </Choose>
247 | Página
Observa como en la función render mostramos los componentes hijos enviados
por React-router (línea 12). Los componentes solo se muestran si la propiedad
this.state.load es true, lo que indica que terminamos de cargar la página. Con
ayuda de un <Choose> podemos determinar si mostrar el componente
<TwitterDashboard> o this.props.children, haciendo dinámica la página.
Edit mode
Página | 248
perfil es el mismo usuario que esta autenticado en la aplicación, si los dos
coinciden, entonces el botón para editar perfil de habilita.
1. ... </ul>
2.
3. <If condition={profile.userName === storageUserName}>
4. <button className="btn btn-primary edit-button"
5. onClick={this.changeToEditMode.bind(this)} >
6. {this.state.edit ? "Guardar" : "Editar perfil"}</button>
7. </If>
1. changeToEditMode(e){
2. if(this.state.edit){
3. let request = {
4. username: this.state.profile.userName,
5. name: this.state.profile.name,
6. description: this.state.profile.description,
7. avatar: this.state.profile.avatar,
8. banner: this.state.profile.banner
9. }
10.
11. APIInvoker.invokePUT('/secure/profile', request, response => {
12. if(response.ok){
13. this.setState(update(this.state,{
14. edit: {$set: false}
15. }))
16. }
17. },error => {
18. console.log("Error al actualizar el perfil");
19. })
20. }else{
21. let currentState = this.state.profile
22. this.setState(update(this.state,{
23. edit: {$set: true},
24. currentState: {$set: currentState}
25. }))
26. }
27. }
Esta función tiene dos propósitos, por un lado, habilita el modo edición, pero por
el otro lado, guarda los cambios si se ejecuta estando en modo edición. Veamos
cómo funciona.
249 | Página
Mediante este servicio es posible actualizar el perfil del
usuario (/secure/profile)
Se tendrá que agregar el bloque <If> que comprende de las líneas 2 a 11 dentro
del <div> que contiene el banner. Esto habilitará que el banner permita cambiar
la imagen mediante un click.
1. imageSelect(e){
2. let id = e.target.id
3. e.preventDefault();
4. let reader = new FileReader();
5. let file = e.target.files[0];
6.
7. if(file.size > 1240000){
8. alert('La imagen supera el máximo de 1MB')
9. return
10. }
11.
12. reader.onloadend = () => {
13. if(id == 'bannerInput'){
14. this.setState(update(this.state,{
15. profile: {
16. banner: {$set: reader.result}
17. }
18. }))
19. }else{
20. this.setState(update(this.state,{
21. profile: {
22. avatar: {$set: reader.result}
23. }
24. }))
25. }
26. }
27. reader.readAsDataURL(file)
28. }
Página | 250
Esta función hace exactamente lo mismo que la función de carga de imagen del
componente Reply, por lo que no nos detendremos a explicar, solo basta resumir
que la función carga una imagen seleccionada y la guarda en la propiedad avatar
o banner del estado. Esto quiere decir que la reutilizamos para cargar la foto del
banner y avatar.
NOTA: Podrías crear una función externa que pueda ser reutilizada tanto en
UserPage como en Reply y así evitar repetir código, sin embargo, eso te lo puedes
llevar de tarea si sientes confiado en poder hacer el cambio.
1. <div className="avatar-box">
2. <img src={profile.avatar} />
3. </div>
1. <Choose>
2. <When condition={this.state.edit} >
3. <div className="avatar-box">
4. <img src={profile.avatar} />
5. <label htmlFor="avatarInput"
6. className="btn select-avatar">
7. <i className="fa fa-camera fa-2x"
8. aria-hidden="true"></i>
9. <p>Foto</p>
10. </label>
11. <input href="#" id="avatarInput"
12. className="btn" type="file"
13. accept=".gif,.jpg,.jpeg,.png"
14. onChange={this.imageSelect.bind(this)}
15. />
16. </div>
17. </When>
18. <Otherwise>
19. <div className="avatar-box">
20. <img src={profile.avatar} />
21. </div>
22. </Otherwise>
23. </Choose>
Este cambio hace que existan dos posibles resultados. Si el componente está en
solo lectura, solo se verá el <img> que ya teníamos (línea 18 a 22). Por otro lado,
si estamos en modo edición, se verá la misma imagen, pero con el input file y
el label que ya conocemos. En caso de seleccionar una imagen, vamos a
reutilizar la función imageSelect.
251 | Página
El siguiente cambio es referente a los controles para capturar los datos básicos
del perfil, por lo cual, tendremos que eliminar la siguiente sección:
1. <div>
2. <p className="user-info-name">{profile.name}</p>
3. <p className="user-info-username">@{profile.userName}</p>
4. <p className="user-info-description">
5. {profile.description}</p>
6. </div>
1. <Choose>
2. <When condition={this.state.edit} >
3. <div className="user-info-edit">
4. <input maxLength="20" type="text" value={profile.name}
5. onChange={this.handleInput.bind(this)} id="name"/>
6. <p className="user-info-username">@{profile.userName}</p>
7. <textarea maxLength="180" id="description"
8. value={profile.description}
9. onChange={this.handleInput.bind(this)} />
10. </div>
11. </When>
12. <Otherwise>
13. <div>
14. <p className="user-info-name">{profile.name}</p>
15. <p className="user-info-username">@{profile.userName}</p>
16. <p className="user-info-description">
17. {profile.description}</p>
18. </div>
19. </Otherwise>
20. </Choose>
Este cambio añade una condición para agregar un input text y un textarea en
caso de estar en edición y respeta el funcionamiento anterior en caso de estar
en modo solo lectura.
1. handleInput(e){
2. let id = e.target.id
3. this.setState(update(this.state,{
4. profile: {
5. [id]: {$set: e.target.value}
6. }
7. }))
8. }
Esta función no tiene nada de especial, pues solo actualiza el campo name o
userName, según el ID del control que genera los eventos.
En este punto, el usuario podrá decidir guardar los cambios, presionando el botón
“Guardar”, el cual cambio de nombre de “Editar perfil” al momento de entrar en
modo edición.
Por otra parte, el usuario podría decidir cancelar la operación y dejar el perfil tal
y como estaba antes de la edición, para ello, tendremos que agregar un nuevo
botón:
Página | 252
1. <If condition= {this.state.edit}>
2. <button className="btn edit-button" onClick=
3. {this.cancelEditMode.bind(this)} >Cancelar</button>
4. </If>
Este fragmento de código deberá quedar después de terminar el bloque <If> para
agregar el botón Siguiendo/Seguir.
1. cancelEditMode(e){
2. let currentState = this.state.currentState
3. this.setState(update(this.state,{
4. edit: {$set: false},
5. profile: {$set: currentState}
6. }))
7. }
Quiero que pongas atención en la línea 2, pues en ella vemos que obtiene la
propiedad currentState del estado. Esta propiedad se establece en la función
changeToEditMode antes de actualizar la propiedad edit a true, a la cual le asigna
el valor del estado actual. Con esto logramos respaldar en currentState los
valores antes de ser actualizados.
253 | Página
El componente MyTweets
Página | 254
27. MyTweets.propTypes = {
28. profile: PropTypes.object
29. }
30.
31. export default MyTweets;
Podemos ver rápidamente que este nuevo componente no tiene nada nuevo que
aportar a nuestro conocimiento, pues todo lo que utilizamos aquí ya lo hemos
aprendido, por lo que explicaré rápidamente las partes claves y sin entrar en los
detalles.
Para que el componente se vea reflejado dentro del componente UserPage, hacen
falta dos cambios, el primero sería en el archivo App.js y el otro en el componente
UserPage. Iniciemos con el archivo App.js:
255 | Página
Hemos agregado un IndexRoute dentro del Route de UserPage, con la finalidad
que al activarse el componente UserPage, le envíe por default el componente
MyTweet como children.
1. render(){
2. let profile = this.state.profile
3. let storageUserName = window.localStorage.getItem("username")
4.
5. let bannerStyle = {
6. backgroundImage: 'url(' + (profile.banner) + ')'
7. }
8.
9. let childs = this.props.children
10. && React.cloneElement(this.props.children, { profile: profile })
11.
12. return(
Debido a que MyTweet y los demás elementos hijos que vamos a definir requieren
el objeto perfil, es necesario clonar los children y asignarles la propiedad
profile. La función cloneElement va a clonar el componente this.props.children
y le asignará la propiedad profile (líneas 9 y19).
Una vez clonado los elementos con las propiedades requeridas, vamos a proceder
con agregarlas a la función render, con la finalidad de que estas se vean en
pantalla:
1. </Otherwise>
2. </Choose>
3. </aside>
4. </div>
5. <div className="col-xs-12 col-sm-8 col-md-7
6. col-md-push-1 col-lg-7">
7. {childs}
8. </div>
9. </div>
10. </div>
11. </div>
12. )
13. }
14. }
15. export default UserPage;
La línea marcada deberá ser agrega hasta el final del archivo, justo en el <div>
que se ve en el código.
Página | 256
También deberemos de agregar la siguiente clase de estilo al archivo styles.css:
1. .tweet-container-header{
2. padding: 10px;
3. font-size: 19px;
4. }
En este punto hemos terminado el componente MyTweets y la página del perfil del
usuario ya se ve más completa, sin embargo, todavía tenemos un blug que
resolver. El bug se presenta cuando vemos el listado de Tweets mostrado,
primero que nada, revisemos si todos los Tweets que vemos en pantalla son solo
del usuario sobre el que estamos en el perfil (Si no tiene ninguno, creemos unos
cuantos Tweets). Luego, naveguemos al perfil de otro usuario por medio de los
usuarios sugeridos del lado derecho. A medida que cambiemos de perfil, veremos
que los Tweets se mesclan y no se muestra solo los del usuario en cuestión.
1. componentDidUpdate(prevProps, prevState) {
2. if(prevProps.profile.userName !== this.props.profile.userName){
3. let username = this.props.profile.userName
4. let onlyUserTweet = this.props.onlyUserTweet
5. this.loadTweets(username, onlyUserTweet)
6. }
7. }
8.
257 | Página
el userName previo (prevProps) es diferente a al nuevo userName (this.props), si
fueran diferentes, esto indica que debemos actualizar los Tweets al nuevo usuario
y es allí cuando llamamos de nuevo la función loadTweets.
Entender como React actualiza los componentes es de las partes más avanzadas,
pues requiere una comprensión completa del ciclo de vida de los componentes,
por lo que, si no logras entender esta parte, te recomiendo regresar al capítulo
de Ciclo de vida de los componentes para repasar este tema.
Una vez implementados estos cambios, podemos realizar la misma prueba para
validar que los Tweets no se mesclan con los de otros usuarios.
Página | 258
Resumen
Por otra parte, nos hemos apoyado de la librería react-router para gestionar la
forma en que React interpreta las URL para determinar los componentes que
debe mostrar, a la vez que hemos visto como crear URL amigables apoyándonos
de URL Params.
Con respecto al proyecto Mini Twitter hemos avanzado bastante, pues hemos
desarrollado uno de los componentes centrales de la aplicación, me refiero a
UserPage, el cual muestra el perfil de los usuarios y permite editar nuestro perfil.
259 | Página
Interfaces interactivas
Capítulo 10
Una de las características que más agradecen los usuarios además de que
funcione bien la aplicación, es que sea ve bien y que tenga algunos efectos o
animaciones que las haga más agradable a la vista, como lo son las animaciones
y las transacciones.
1. .card{
2. background-color: black;
3. transition: background-color 500ms;
4. }
5.
6. .card:hover{
7. background-color: blue;
Página | 260
8.
9. }
La clase de estilo .card, define que el color de fondo del elemento deberá ser
negro (#000) y establece la propiedad transition, la cual recibe dos parámetros:
En la imagen podemos ver como se llevaría a cabo una animación del background,
suponiendo que ponemos la clase de estilo .card a un div. Podemos apreciar que
el color cambia de negro a azul y esta transición debería de ocurrir en 500
milisegundos.
Debido a que este no es un libro de CSS, no quisiera salirme del tema central,
que es aprender React, por lo que, si no estás al tanto de CSS transition, te
recomiendo ver la documentación que nos ofrece Mozilla, la cual está muy
completa y en español.
Las animaciones son más complejas que las transiciones, pero al mismo tiempo
son mucho más potentes, pues permiten crear animaciones más sofisticadas. Las
animaciones se crean mediante la instrucción @keyframe en CSS. Los key frame
permiten definir una animación partiendo de un puto a otro o dividir la animación
en fragmentos, los cuales pueden cambiar el comportamiento de la animación.
1. @keyframes card-animation{
2. from {background-color: black;}
3. to {background-color: blue;}
4. }
261 | Página
El fragmento de CSS anterior crea una animación la cual cambia el background
de negro a azul. Este tipo de animación solo tiene un estado inicial y uno final.
1. @keyframes example {
2. 0% {background-color: black;}
3. 25% {background-color: green;}
4. 50% {background-color: red;}
5. 100% {background-color: blue;}
6. }
También podemos definir animaciones por sección como la anterior, la cual crea
una animación que cambia el background inicialmente a Negro, luego al 25% de
la animación lo cambia Verde, luego a 50% a rojo y al final queda en azul.
1. .card {
2. animation: card-animation 5s
3. }
Nuevamente, las animaciones no son el tema central de este libro, por lo que, si
quieres aprender más acerca de las animaciones, te dejo la documentación en
español de Mozilla.
Página | 262
Introducción a CSSTranstionGroup
Seguramente te estarás preguntando que tiene que ver las transiciones y las
animaciones con CSSTransactionGroup, si al final, estas dos son características de
CSS y no de React. Aunque ese argumento puede ser verdad, la realidad es que
el módulo react-transition-group se apoya de estas características de CSS para
llevar a cabo las animaciones.
1. <CSSTransitionGroup
2. transitionName="card"
3. transitionEnter={true}
4. transitionEnterTimeout={500}
5. transitionAppear={false}
6. transitionAppearTimeout={0}
7. transitionLeave={false}
8. transitionLeaveTimeout={0}>
9.
10. <AnyComponent/>
11.
12. </CSSTransitionGroup>
transitionAppear
transitionEnter
transitionLeave
263 | Página
transitionAppearTimeout
transitionEnterTimeout
transitionLeaveTimeout
Ya con todo este configurado, solo resta tener las clases de estilo
correspondientes, las cuales deben de cumplir una sintaxis muy estricta en su
nombre, de lo contrario las transiciones no se llevarán a cabo.
Lo primero que debemos de saber, es que cada transición (appear, enter y leave)
tiene dos estados (inactivo y activo), en el primero, se deberá definir como se
deberá ver el componente al iniciar la transición y en el segundo, como debería
de terminar el componente una vez que la transición termino.
Dicho esto, entonces se entiende que podría existir 3 paredes de clases de estilo,
las cuales deberá tener el siguiente formato:
{transitionName}-{appear|enter|leave}
{transitionName}-{appear|enter|leave}.{transitionName}-
{appear|enter|leave}-active.
1. .card-enter{
2. }
3.
4. .card-enter.card-enter-active{
5. }
6.
7. .card-leave {
8. }
9.
10. .card-leave.card-leave-active {
11. }
12.
13. .card-appear {
14. }
15.
16. .card-appear.card-appear-active {
17. }
Página | 264
1. .card-appear {
2. background-color: black;
3. }
4.
5. .card-appear.card-appear-active {
6. background-color: blue;
7. transition: background-color 0.5s;
8. }
Cabe resaltar no es requerido definir los estilos para todas las transiciones, si no
solo las que habilitemos desde el componente CSSTransactionGroup.
El componente UserCard
Una de las partes que no hemos abordado en la página del perfil del usuario, es
ver sus seguidores y las personas a las que seguimos. Dado que en estas dos
secciones los usuarios se representan de la misma forma. Hemos decidido crear
un componente que represente a un usuario, y ese es UserCard. Veamos como
se ve este componente:
265 | Página
2. import { Link } from 'react-router'
3. import PropTypes from 'prop-types'
4.
5. class UserCard extends React.Component{
6. constructor(props){
7. super(props)
8. }
9.
10. render(){
11.
12. let user = this.props.user
13. let css = {
14. backgroundImage: 'url(' +user.banner + ')'
15. }
16.
17. return(
18. <article className="user-card" >
19. <header className="user-card-banner" style={css}>
20. <img src={user.avatar} className="user-card-avatar"/>
21. </header>
22. <div className="user-card-body">
23. <Link to={"/" + user.userName} className="user-card-name" >
24. <p>{user.name}</p>
25. </Link>
26. <Link to={"/" + user.userName} className="user-card-username">
27. <p>@{user.userName}</p>
28. </Link>
29. <p className="user-card-description">{user.description}</p>
30. </div>
31. </article>
32. )
33. }
34. }
35.
36. UserCard.propTypes = {
37. user: PropTypes.object.isRequired
38. }
39.
40. export default UserCard;
Con tan solo observar el componente te darás cuenta que no hay nada nuevo
que analizar, pues el componente es muy simple y utiliza cosas que ya sabes
hasta este momento, por lo que solo mencionare las cosas relevantes. Primero
que nada, observemos que el componente recibe como prop el objeto user (línea
37), el cual es obligatorio.
Cabe mencionar que el objeto user es en realidad el mismo objeto profile que
ya hemos venido utilizando a lo largo de todo este proyecto, solo que lo
nombramos de esta forma para darle un nombre más acorde para el componente.
Cuando presionamos algún Link nos deberá llevar al perfil de ese usuario
(/{username}).
Para concluir, solo faltaría agregar las clases de estilo correspondientes al archivo
styles.css:
Página | 266
5. border: 1px solid #E6ECF0;
6. border-radius: 5px;
7. overflow: hidden;
8. }
9.
10. .user-card .user-card-banner{
11. background-position: center;
12. background-size: cover;
13. height: 100px;
14. border-bottom: 1px solid ##E6ECF0;
15. }
16.
17. .user-card .user-card-avatar{
18. width: 70px;
19. height: 70px;
20. border-radius: 5px;
21. position: absolute;
22. top: 70px;
23. left: 10px;
24. }
25.
26.
27. .user-card .user-card-body{
28. padding-top: 100px;
29. background-color: #FFF;
30. padding: 10px 10px 20px;
31.
32. }
33.
34. .user-card .user-card-body .user-card-username p{
35. font-size: 12px;
36. color: #66757f;
37. }
38.
39. .user-card .user-card-body .user-card-name{
40. font-weight: bold;
41. font-size: 18px;
42. display: block;
43. position: relative;
44. margin-top: 40px;
45. }
46.
47. .user-card .user-card-body .user-card-name p {
48. margin: 0px;
49. }
50.
51. .user-card .user-card-body .user-card-description{
52. font-size: 14px;
53. color: #66757f;
54. }
55.
56.
57. .user-card .user-card-body .user-card-username:hover,
58. .user-card .user-card-body .user-card-name:hover{
59. text-decoration: underline;
60. }
El componente Followings
267 | Página
Fig. 131 - Followings Component.
Ya con eso, vamos a crear el archivo Followings.js en el path /app, el cual deberá
tener la siguiente estructura:
Página | 268
32. users: response.body
33. })
34. },error => {
35. console.log("Error en la autenticación");
36. })
37. }
38.
39. render(){
40. return(
41. <section>
42. <div className="container-fluid no-padding">
43. <div className="row no-padding">
44. <CSSTransitionGroup
45. transitionName="card"
46. transitionEnter = {true}
47. transitionEnterTimeout={500}
48. transitionAppear={false}
49. transitionAppearTimeout={0}
50. transitionLeave={false}
51. transitionLeaveTimeout={0}>
52. <For each="user" of={ this.state.users }>
53. <div className="col-xs-12 col-sm-6 col-lg-4"
54. key={this.state.tab + "-" + user._id}>
55. <UserCard user={user} />
56. </div>
57. </For>
58. </CSSTransitionGroup>
59. </div>
60. </div>
61. </section>
62. )
63. }
64. }
65.
66. Followings.propTypes = {
67. profile: PropTypes.object
68. }
69.
70. export default Followings;
Con respecto a los UserCard, estos deben de recibir como parámetro el usuario
que van a representar, el cual es obtenido como un array en la función
componentWillMount. Para recuperar a las personas que siguen un usuario, se
utiliza el servicio /followings/{user} mediante el método GET.
269 | Página
El siguiente paso es crucial para que la animación se lleve a cabo, en el cual
tendremos agregar las clases de estilo para la animación en el archivo styles.css:
1. .card-enter{
2. opacity: 0;
3. }
4.
5. .card-enter.card-enter-active{
6. opacity: 1;
7. transition: opacity 500ms ease-in;
8. }
9.
10. /*.card-leave {
11. opacity: 0;
12. }
13.
14. .card-leave.card-leave-active {
15. opacity: 1;
16. transition: opacity 500ms ease-in;
17. }
18.
19. .card-appear {
20. opacity: 0;
21. }
22.
23. .card-appear.card-appear-active {
24. opacity: 1;
25. transition: opacity 500ms ease-in;
26. }*/
Para este ejemplo solo requerimos las dos primeras clases de estilo (líneas 1 a
8), sin embargo, he dejado comentadas las clases necesarias para leave y appear
en caso de que quieres realizar algunos experimentos.
Página | 270
11. render((
12. <Router history={ browserHistory }>
13. <Route path="/" component={TwitterApp} >
14. <Route path="signup" component={Signup}/>
15. <Route path="login" component={Login}/>
16.
17. <Route path=":user" component={UserPage} >
18. <IndexRoute component={MyTweets} tab="tweets" />
19. <Route path="following" component={Followings} tab="followings"/>
20. </Route>
21. </Route>
22. </Router>
23. ), document.getElementById('root'));
El componente Followers
271 | Página
29. APIInvoker.invokeGET('/followers/' + username, response => {
30. this.setState({
31. users: response.body
32. })
33. },error => {
34. console.log("Error en la autenticación");
35. })
36.
37. }
38.
39. render(){
40. return(
41. <section>
42. <div className="container-fluid no-padding">
43. <div className="row no-padding">
44. <CSSTransitionGroup
45. transitionName="card"
46. transitionEnter = {true}
47. transitionEnterTimeout={500}
48. transitionAppear={false}
49. transitionAppearTimeout={0}
50. transitionLeave={false}
51. transitionLeaveTimeout={0}>
52. <For each="user" of={ this.state.users }>
53. <div className="col-xs-12 col-sm-6 col-lg-4"
54. key={this.state.tab + "-" + user._id}>
55. <UserCard user={user} />
56. </div>
57. </For>
58. </CSSTransitionGroup>
59. </div>
60. </div>
61. </section>
62. )
63. }
64. }
65.
66. Followers.propTypes = {
67. profile: PropTypes.object
68. }
69.
70. export default Followers;
Página | 272
1. import React from 'react'
2. import { render } from 'react-dom'
3. import TwitterApp from './TwitterApp'
4. import Signup from './Signup'
5. import Login from './Login'
6. import UserPage from './UserPage'
7. import MyTweets from './MyTweets'
8. import Followings from './Followings'
9. import Followers from './Followers'
10. import { Router, Route, browserHistory, IndexRoute } from "react-router"
11.
12. render((
13. <Router history={ browserHistory }>
14. <Route path="/" component={TwitterApp} >
15. <Route path="signup" component={Signup}/>
16. <Route path="login" component={Login}/>
17.
18. <Route path=":user" component={UserPage} >
19. <IndexRoute component={MyTweets} tab="tweets" />
20. <Route path="followers" component={Followers} tab="followers"/>
21. <Route path="following" component={Followings} tab="followings"/>
22. </Route>
23. </Route>
24. </Router>
25. ), document.getElementById('root'));
Finalmente, solo quedaría guardar los cambios y ver los resultados. Mediante una
imagen es complicado ver una animación, por lo que te pido que te dirijas al
perfil del usuario que gustes y cambies entre las pestañas de Seguidores y
Siguiendo para comprobar los resultados.
273 | Página
Resumen
Sé que este capítulo no ha llegado a ser tan impresionante como pensabas, pues
a lo mejor esperabas librerías de interacción más sofisticadas, pero la realidad es
que con CSS es posible hacer que las aplicaciones luzcan realmente bien, incluso
con animaciones. Sin embargo, buscare que en próximas ediciones esta sección
se pueda ampliar con más cosas interesantes y ese es el motivo por el cual he
decidido crear esta pequeña sección por separado.
Página | 274
Componentes modales
Capítulo 11
En la actualidad existe una gran cantidad de librerías listas para ser usadas, las
cuales solo requieren de su instalación con npm y posteriormente su
implementación. Algunas librerías son fáciles de usar otras más complejas, pero
más configurables.
Debido a que estas librerías no son el punto focal de este libro y que evolucionan
constantemente, es complicado enseñarte a usar cada una de ellas, por lo que,
en su lugar, te vamos a nombrar algunas con su respectiva documentación para
que seas tú mismo quien determine que librerías se adapta mejor a tus
necesidades. La lista es:
react-modal (https://github.com/reactjs/react-modal)
react-modal-dialog (https://www.npmjs.com/package/react-modal-
dialog)
react-modal-bootstrap: (https://www.npmjs.com/package/react-modal-
bootstrap)
275 | Página
Estas son las 3 librerías más populares para la implementación de componentes
modales, pero sin duda hay muchísimos más. Te invito a que los revises y veas
si alguno te llama la atención para usarlas como parte de tu pila de librerías.
En este punto podrías pensar, si ya existen librerías para hacerlo, para que
reinventar la rueda implementando mi propio sistema de componentes modales,
y puede que tengas razón, incluso, te alentamos a elegir una librería existente,
si esta cumple a la perfección lo que requieres, sin embargo, crear tus propias
pantallas modales te da más control sobre los compontes, además que, es tan
simple que realmente no requiera mucho esfuerzo.
Como te comenté hace un momento, si decides utilizar una librería está bien, sin
embargo, como este es un libro para aprender React, queremos enseñarte a
hacerlo por ti mismo, para que de esta forma puedes dominar mejor la
tecnología. Como siempre, la decisión es solo tuya.
Página | 276
Para lograr que mi componente se vea por encima, incluso de su componente
padre, es necesario encapsular nuestro componente dentro de elemento con
posición fija en la pantalla (position: fixed), el cual, abarcara toda el área visible
con ayuda de las propiedades (top, right, left, bottom) en cero:
Para asegurarnos que el elemento fixed se encuentre por encima del resto de
componentes, será necesario utiliza la propiedad z-index en un número muy
elevado. Esta propiedad moverá nuestro elemento en el eje Z, es decir lo traerá
hasta el frente.
Y eso es todo, lo que seguirá es solo agregar nuestro componente dentro del
elemento fixed y listo. Solo una cosa más, te sugiero que el contendedor de tu
componente (primer elemento) utilice la propiedad margin: auto, con la finalidad
de realizar un centrado horizontal perfecto en la pantalla:
Si estás pensando que esto es muy complicado y que mejor optarás por una
librería, espera a ver como lo implementamos en el proyecto Mini Twitter, para
que veas lo fácil que es.
El componente TweetReply
277 | Página
inferior. La idea de este componente, es darle la oportunidad a un usuario de
contestar o agregar un comentario a un Tweet existente. Veamos cómo se ve:
Página | 278
24. }
25.
26. APIInvoker.invokePOST('/secure/tweet', request, response => {
27. this.handleClose()
28. },error => {
29. console.log("Error al cargar los Tweets");
30. })
31. }
32.
33. render(){
34.
35. let operations = {
36. addNewTweet: this.addNewTweet.bind(this)
37. }
38.
39. return(
40. <div className="fullscreen">
41. <div className="tweet-detail">
42. <i className="fa fa-times fa-2x tweet-close" aria-hidden="true"
43. onClick={this.handleClose.bind(this)}/>
44. <Tweet tweet={this.props.tweet} detail={true} />
45. <div className="tweet-details-reply">
46. <Reply profile={this.props.profile} operations={operations}/>
47. </div>
48. </div>
49. </div>
50. )
51. }
52. }
53.
54. TweetReply.propTypes = {
55. tweet: PropTypes.object.isRequired,
56. profile: PropTypes.object.isRequired,
57. }
58.
59. export default TweetReply;
Ahora bien, analicemos como se hace la magina, deberás notar que en la línea
40 hay un <div> con la clase fullscreen. Esta div es el contendor fixed, ya que
con ayuda de la clase fullscreen hacemos que tome toda la pantalla. Analicemos
la clase fullcreen:
1. .fullscreen{
2. position: fixed;
3. background-color: rgba(0,0,0,0.5);
4. top: 0;
5. right: 0;
6. left: 0;
7. height: auto;
8. z-index: 999999;
9. overflow: auto;
10. bottom: 0;
11. }
Veamos que esta clase tiene lo que ya habíamos mencionado al principio, tiene
un display: fixed, para asegurar que este elemento se posicione con respecto
a la ventana del navegador, nos aseguramos que tomo toda la pantalla mediante
279 | Página
top:0, right:0, left:0 y bottom: 0. Establecemos la altura (height) en auto,
para que se calcule el tamaña basado en el contenido, overflow en auto, para
asegurarnos de que el componente tenga scroll en caso de desbordamiento, z-
index en 999999 para que sea el elemento más arriba en toda la aplicación. Y
finalmente, un background negro con un alfa de 0.5, lo que indica que tendrá una
transparencia.
Lo que tenemos dentro del div con fullscreen es lo que queremos que se vea
en forma modal. En este caso, el div hijo (línea 41) será el componente que
vemos en pantalla. Como nota. La clase fullscreen debe de tener un
posicionamiento relativo (position: relative) de lo contrario no se verá bien:
1. .fullscreen .tweet-detail{
2. max-width: 700px;
3. overflow: hidden;
4. border-radius: 5px;
5. margin: auto;
6. margin-top: 50px;
7. margin-bottom: 100px;
8. position: relative;
9. width: 60%;
10. background-color: #fff;
11. }
Una vez explicado cómo funciona la parte modal, solo nos resta analizar el
funcionamiento del componente.
Observa que estamos reutilizando los componentes Tweet y Reply. Por una parte,
el componente recibe el objeto tweet para mostrarlo y en esta ocasión recibe una
nueva propiedad llamada detail, esta propiedad le indicará al componente Tweet
como debe comportarse y verse cuando está siendo mostrado de forma modal.
Vamos a analizar esto un poco más adelante.
Para cerrar este componente, solo nos resta agregar las clases de estilo en el
archivo styles.css:
Página | 280
1. html.modal-mode{
2. overflow-y: hidden;
3. }
4.
5. .fullscreen{
6. position: fixed;
7. background-color: rgba(0,0,0,0.5);
8. top: 0;
9. right: 0;
10. left: 0;
11. height: auto;
12. z-index: 999999;
13. overflow: auto;
14. bottom: 0;
15. }
16.
17. .fullscreen .tweet-detail{
18. max-width: 700px;
19. overflow: hidden;
20. border-radius: 5px;
21. margin: auto;
22. margin-top: 50px;
23. margin-bottom: 100px;
24. width: 60%;
25. background-color: #fff;
26. }
27.
28. .fullscreen .tweet-detail .tweet-close{
29. position: absolute;
30. display: inline-block;
31. right: 15px;
32. top: 10px;
33. }
34.
35. .fullscreen .tweet-detail .tweet-close:hover{
36. cursor: pointer;
37. }
38.
39. .tweet-detail .tweet-detail-responses{
40. list-style: none;
41. margin: 0px;
42. padding: 0px;
43. }
44.
45. .tweet-detail .tweet-details-reply{
46. border-bottom: 1px solid #E6ECF0;
47. }
Quisiéramos que ya fuera todo para ver como quedo, pero aun nos faltan algunas
cosas más por implementar en el archivo Tweet.js. Como ya mencionamos, el
componente TweetReply se muestra cuando presionamos el botón de responder
(flecha) que se encuentra debajo de cada Tweet, es por esta razón que tenemos
que regresar al componente Tweet para implementar esta funcionalidad. Para
ello agregaremos estos tres cambios:
1. handleReply(e){
2. $( "html" ).addClass( "modal-mode");
3. e.preventDefault()
281 | Página
4.
5. if(!this.props.detail){
6. render(<TweetReply tweet={this.props.tweet}
7. profile={this.state._creator} />,
8. document.getElementById('dialog'))
9. }
10. }
1. render(){
2. return (
3. <div id="mainApp">
4. <Toolbar profile={this.state.profile} />
5. <Choose>
6. <When condition={!this.state.load}>
7. <div className="tweet-detail">
8. <i className="fa fa-circle-o-notch fa-spin fa-3x fa-fw"></i>
9. </div>
10. </When>
11. <When condition={this.props.children == null
12. && this.state.profile != null}>
13. <TwitterDashboard profile={this.state.profile}/>
14. </When>
15. <Otherwise>
16. {this.props.children}
17. </Otherwise>
18. </Choose>
19. <div id="dialog"/>
20. </div>
21. )
22. }
Este div no tiene nada de especial, es un elemento más, sin embargo, el ID nos
ayuda a referenciarlo para agregarle dinámicamente contenido. En este caso, los
diálogos son introducidos como hijos de este elemento.
Página | 282
2. <a className="reply-icon"
3. onClick={this.handleReply.bind(this)}
4. data-ignore-onclick>
5. <i className="fa fa-reply " aria-hidden="true"
6. data-ignore-onclick></i> {this.state.replys}
7. </a>
8. </If>
Con esto, habremos finalizado los cambios necesarios y solo restaría guardar
todos los cambios, actualizar el navegador y ver cómo funciona. No te preocupes
si envías una respuesta y no la puedes ver, eso lo veremos en el siguiente
componente.
El componente TweetDetail
El último componente que nos falta por ver es TweetDetail y con el estaríamos
cerrando el proyecto, al menos por ahora. Así que sin más continuemos para
terminarlo.
283 | Página
1. import React from 'react'
2. import Reply from './Reply'
3. import Tweet from './Tweet'
4. import APIInvoker from './utils/APIInvoker'
5. import update from 'react-addons-update'
6. import { browserHistory } from 'react-router'
7. import PropTypes from 'prop-types'
8.
9. class TweetDetail extends React.Component{
10.
11. constructor(props){
12. super(props)
13. }
14.
15. componentWillMount(){
16. let tweet = this.props.params.tweet
17. APIInvoker.invokeGET('/tweetDetails/'+tweet, response => {
18. this.setState( response.body)
19. },error => {
20. console.log("Error al cargar los Tweets");
21. })
22. }
23.
24. addNewTweet(newTweet){
25. let oldState = this.state;
26. let newState = update(this.state, {
27. replysTweets: {$splice: [[0, 0, newTweet]]}
28. })
29. this.setState(newState)
30.
31. let request = {
32. tweetParent: this.props.params.tweet,
33. message: newTweet.message,
34. image: newTweet.image
35. }
36.
37. APIInvoker.invokePOST('/secure/tweet', request, response => {
38. },error => {
39. console.log("Error al crear los Tweets");
40. })
41. }
42.
43. handleClose(){
44. $( "html" ).removeClass( "modal-mode");
45. browserHistory.goBack()
46. }
47.
48. render(){
49. $( "html" ).addClass( "modal-mode");
50.
51. let operations = {
52. addNewTweet: this.addNewTweet.bind(this)
53. }
54.
55. return(
56. <div className="fullscreen">
57. <Choose>
58. <When condition={this.state == null}>
59. <div className="tweet-detail">
60. <i className="fa fa-circle-o-notch fa-spin fa-3x fa-fw"></i>
61. </div>
62. </When>
63. <Otherwise>
64. <div className="tweet-detail">
65. <i className="fa fa-times fa-2x tweet-close"
66. aria-hidden="true"
Página | 284
67. onClick={this.handleClose.bind(this)}/>
68. <Tweet tweet={this.state} detail={true} />
69. <div className="tweet-details-reply">
70. <Reply profile={this.state._creator}
71. operations={operations}
72. key={"detail-" + this.state._id} newReply={false}/>
73. </div>
74. <ul className="tweet-detail-responses">
75. <If condition={this.state.replysTweets != null} >
76. <For each="reply" of={this.state.replysTweets}>
77. <li className="tweet-details-reply" key={reply._id}>
78. <Tweet tweet={reply} detail={true}/>
79. </li>
80. </For>
81. </If>
82. </ul>
83. </div>
84. </Otherwise>
85. </Choose>
86. </div>
87. )
88. }
89. }
90. export default TweetDetail;
http://localhost:3000/{username}/{tweet-id}
Lo que sigue es la creación del componente como tal, aquí hay dos posibles
vistas, si el estado es null, quiere decir que no se ha terminado de cargar el
285 | Página
tweet desde el API, por lo que se muestra un ícono de “cargando”. Por otro lado,
si no es null, el tweet ya se cargó y lo mostramos. Quiero que observes que
nuevamente reutilizamos el componente Tweet y Reply, de la misma forma que
lo utilizamos en TweetReply, pero adicional, creamos una lista (línea 74) de los
tweets hijos. Si un Tweet tiene hijos, los podemos encontrar en el array
this.state.replysTweets y cada reply es representada mediante un componente
Tweet.
1. handleClick(e){
2. if(e.target.getAttribute("data-ignore-onclick")){
3. return
4. }
5. let url = "/" + this.state._creator.userName + "/" + this.state._id
6. browserHistory.push(url)
7. let tweetId = e.target.id
8. }
Página | 286
Lo primero será agregar esta función al componente Tweet.js, el cual nos
permitirá cachar el evento y lanzar el componente TweetDetail. No olvidemos el
import correspondiente al objeto BrowserHistory:
1. render(){
2. let tweetClass = null
3. if(this.props.detail){
4. tweetClass = 'tweet detail'
5. }else{
6. tweetClass = this.state.isNew ? 'tweet fadeIn animated' : 'tweet'
7. }
8.
9. return (
10. <article className={tweetClass} onClick={this.props.detail ? '' :
11. this.handleClick.bind(this)} id={"tweet-" + this.state._id}>
12. <img src={this.state._creator.avatar} className="tweet-avatar" />
13. <div className="tweet-body">
14. <div className="tweet-user">
Links en Tweet
Algo que nos faltó en el componente Tweet, es habilitar que el nombre del usuario
que creo el Tweet sea un link a su perfil, por lo que podemos remplazar las
siguientes líneas para implementarlo:
Otra funcionalidad que no implementamos, es darle like a los Tweet, por lo que
si quieres terminar de implementar esta funcionalidad solo falta hacer los
siguientes ajustes al componente Tweet.
287 | Página
1. <a className={this.state.liked ? 'like-icon liked' : 'like-icon'}
2. onClick={this.handleLike.bind(this)} data-ignore-onclick>
3. data-ignore-onclick>
4. <i className="fa fa-heart " aria-hidden="true"
5. data-ignore-onclick></i> {this.state.likeCounter}
6. </a>
1. handleLike(e){
2. e.preventDefault()
3. let request = {
4. tweetID: this.state._id,
5. like: !this.state.liked
6. }
7.
8. APIInvoker.invokePOST('/secure/like', request, response => {
9. let newState = update(this.state,{
10. likeCounter : {$set: response.body.likeCounter},
11. liked: {$apply: (x) => {return !x}}
12. })
13. this.setState(newState)
14. },error => {
15. console.log("Error al cargar los Tweets", error);
16. })
17. }
Documentación: Like/Dislike
Este servicio se utiliza para darle like a un Tweet, o en
su caso, retirar el like (/secure/like).
Página | 288
Resumen
En este punto deberíamos estar muy contentos pues hemos terminado nuestro
proyecto Mini Twitter en su totalidad. Después de un gran trabajo se ve
recompensado nuestro esfuerzo, pues hemos terminado de principio a fin un
proyecto completo y en lo personal yo diría que no es NADA amateur, pues ha
sido una aplicación completa que ha aplicado absolutamente todo lo que hemos
aprendido en el libro.
Si en este punto del libro eres capaz de comprender todo lo que hemos aprendido,
seguramente ya estás listo para enfrentarte a un proyecto real. Pues cuentas con
todas las bases y el conocimiento requerido para desarrollar una aplicación
estándar.
Si bien, en este punto hemos terminado todo el proyecto, todavía existen mejoras
que implementar, como es el caso de Redux, el cual estaremos abordando en el
siguiente capítulo.
289 | Página
Redux
Capítulo 12
Página | 290
Fig. 137 - Actualización directa de un componente.
Introducción a Redux
Dicho lo anterior, Redux es una herramienta que nos ayuda a gestionar la forma
en que accedemos y actualizamos el estado. La idea principal de Redux es
centralizar todo el estado de la aplicación en algo llamado “Store”, o también
conocido en Redux como “single source of truth” (única fuente de la verdad). Con
esto, liberamos a los componentes de gestionar un estado independiente.
291 | Página
Fig. 138 - Cambio del estado con Redux.
En este punto podrías estarte haciendo la pregunta, ¿pero que no es una mala
práctica actualizar un componente directamente sin pasar por la jerarquía de
componentes? La cual es una excelente pregunta; y la respuesta es simple, la
realidad es que no se realiza una actualización directa del estado de un
componente, si no que se actualiza el Store, el cual tiene el estado de toda la
aplicación. Además, como los componentes se registran al Store, es fácil detectar
de donde proviene los cambios al estado.
Flux como tal es patrón de diseño, por lo que nos podemos descargarlo como tal
y utilizarlo en React, sin embargo, existe un módulo llamado react-flux, el cual
implementa este patón. Ahora bien, React-flux fue desarrollado primero que
Redux y por un tiempo fue la mejor alternativa, sin embargo, hoy por hoy, Redux
es superior, tal es así, que Facebook contrato a su principal desarrollador Redux
(Dan Abramov).
Componentes de Redux
Antes que nada, quiero que sepas que Redux es una herramienta para cualquier
aplicación basada en JavaScript, por lo que puede ser utilizada en aplicaciones
100% JavaScript, Angular, aplicaciones basadas en JQuery, React, etc. Es por
Página | 292
este motivo que debemos de entender cómo funciona Redux por sí mismo, antes
de implementarlo con React.
293 | Página
1. La vista (Componentes) envía una solicitud de cambio al store mediante la
función dispatch que proporciona el mismo Store.
2. El Action es un objeto JavaScript de describe los cambios que quiere
realizar sobre el estado del Store.
3. El Store recibe el Action y lo envía a los diversos reducer para actualizar el
estado.
4. El reducer son funcionas que implementamos nosotros mismos, los cuales
tienen la lógica para actualizar el Estado basado en los Actions. El reduce
actualiza el estado y lo retorna al Store para sobrescribir el estado actual
con el nuevo.
5. Finalmente, el Store notifica a todos los componentes suscritos. Los
componentes pueden recuperar el nuevo estado para realizar las
actualizaciones correspondientes.
Redux funciona únicamente con un solo Store para toda la aplicación, es por eso
que se le conoce como la única fuente de la verdad. La estructura que manejemos
dentro del Store, dependerá totalmente de nosotros, por lo que somos libre de
diseñarla a como se acomode mejor a nuestra aplicación. Debido a esto, suele
ser una estructura con varios niveles de anidación.
Las personas que esta familiarizadas con Flux, notará que esta es una de las
principales diferencias entre Redux y Flux, pues flux permite la creación de
múltiples Stores.
Una de las restricciones de Redux, es que no existe una forma para actualizar el
estatus directamente, en su lugar, es necesario enviar un Action al Store,
describiendo las intenciones de actualizar el estado. Por su parte, el store solo
proporciona los siguientes métodos:
getState()
replaceReducer(reducer)
dispatch(action)
subscribe(listener)
Página | 294
Como se puede observar, no existe ninguna función como setState() que nos
permite actualizar el Estado, dejándonos como única alternativa, el envío de una
acción por medio de la función dispatch.
Los reducers deberán de escribirse siempre como funciones puras. Para que una
función sea pura, debe de cumplir con las siguientes características:
Estos se llaman "puros", porque no hacen más que devolver un valor basado en
sus parámetros. Además, no tienen efectos secundarios en ninguna otra parte
del sistema.
Algo que probablemente no quedo muy claro con respecto al cuarto punto, es
que, dado que el estado actual es un parámetro de entrada del reducer, no
deberíamos modificarlo, en su lugar, tendríamos que hacer una copia de él y
sobre ese agregar los nuevos cambios. Esto es exactamente lo mismo que
hacíamos con la función update del módulo react-addons-update por lo que no
debería de presentar una sorpresa para nosotros.
Otro de los puntos a tomar en cuenta, es que los Action deben de tener una
estructura mínima, en la cual debe de existir la propiedad type, seguido de esto,
puede venir lo que sea, incluso, podríamos mandar solo la propiedad type, por
ejemplo:
1. //Option 1
2. {
3. type: "LOGIN"
4. }
5.
6. //Option 2
7. {
8. type: "LOGIN",
295 | Página
9. profile: {
10. id: "1234",
11. userName: "oscar",
12. name: "oscar blancarte"
13. }
14. }
Con toda esta teoría, vamos a ver un pequeño ejemplo de cómo funciona Redux,
para esto, te pido que analices el siguiente fragmento de código:
1. const initialState = {
2. load: false,
3. profile: {
4. id: "",
5. name: "",
6. userName: "",
7. avatar: "",
8. banner: ""
9. }
10. }
11.
12. //Login Reducer
13. export const loginReducer = (state = initialState, action) => {
14. switch (action.type) {
15. case LOGIN_SUCCESS:
16. return {
17. load: true,
18. profile: action.profile
19. }
20. default:
21. return state
22. }
23. }
24. //Create Redux Store
25. var store = Redux.createStore(loginReducer)
Esta función tiene un switch que nos ayuda a identificar el tipo de action, y
vasado en él sabrá como actualizar el estado. Ahora bien, en este ejemplo
tenemos dos opciones, si el tipo es LOGIN_SUCCESS o no lo es, en caso de serlo,
se creará un nuevo objeto, indicando la propiedad load en true y el valor del
profile que es igual al valor contenido en el action. Podemos observar que este
Página | 296
nuevo objeto es retornado por el reducer, lo que implica que el Store lo reciba y
lo establezca como el nuevo estado del Store.
Por otra parte, deberemos implementar una función en la vista que ejecute el
dispatch para iniciar la actualización de la vista:
Te pido que imagines que esta función es ejecutada por un componente, por lo
que no deberíamos preocupar de donde llegaron los parámetros de entrada. Por
ahora, el parámetro dispatch es una referencia que nos permitirá enviar una
action al store, y getState es una referencia para obtener el estado actual del
store.
Ahora bien, ya que hemos expuesto todos los partes que componente una
aplicación con Redux, vamos a explicar el orden de ejecución. Tras la creación
del Store en la línea 25 del código anterior (var store =
Redux.createStore(loginReducer) ), lo primero que pasará es que el store
llamará al reducer con la finalidad de inicializar el estado, por lo que en la primera
ejecución del reducer, establecerá el estado al valor de la variable initialState
(state = initialState). Una vez hecho esto, la aplicación quedará en espera de
una nueva acción.
297 | Página
7. }
8. default:
9. return state
10. }
11. }
Por ahora no nos preocupemos como es que esta función es llamada, solo
imaginemos que está dentro de nuestro componente y es llamada en automático.
Sé que suena raro que ahora te diga que ya no vamos a utilizar el estado, pues
durante todo el libro hemos hablado de su importancia. Pero verá que todo cobra
sentido una vez que lo expliquemos.
Debido a que el principio de Redux es tener solo una fuente de la verdad, tener
estados distribuidos por todos los componentes rompería ese principio, pues cada
componente tuviera su propia fuente de la verdad a. Para prevenir esto, Redux
se apoya de funciones como la anteriores, con la finalidad de mapear el estado
del Store a propiedades del componente.
Esto quiere decir que si antes obteníamos el userName del estado de la siguiente
manera (this.state.profile.userName) ahora lo obtendremos así
(this.props.profile.userName), el nombre de las props dependerá de la forma
en que creamos el objeto en la función mapStateToProps. En este ejemplo,
tendríamos dos props, this.props.load y this.props.profile. Ahora bien, fíjate
que los valores de estas props se obtienen directamente del parámetro state, el
cual es el estado actual del Store.
Página | 298
Ahora bien, como es que cuando hay un cambio en el Store, el componente es
notificado. Para esto existe un módulo llamado react-redux, el cual nos facilita
enormemente la vida. Este módulo nos proporciona la función connect, la cual se
utiliza para conectar un componente con Redux. Este módulo funciona de la
siguiente manera:
El segundo parámetro nos permite definir un objeto o una función que regrese
un objeto (mapDispatchToProps), en este objeto deberá estar las funciones
disponibles por el componente, las cuales disparar los actions. En este caso solo
hemos pasado la función relogin que definimos más arriba. Esta función recibirá
el dispatch y getState de los cuales ya hablamos.
1. render((
2. <Provider store={ store }>
3. <Router history={ browserHistory }>
4. <Router component={TwitterApp} path="/">
5. <Route path="/signup" component={Signup}/>
6. <Route path="/login" component={Login}/>
7.
8. <Route path="/:user" component={UserPage} >
9. <IndexRoute component={MyTweets} tab="tweets" />
10. <Route path="followers" component={Followers} tab="followers"/>
11. <Route path="following" component={Followings} tab="followings"/>
12. <Route path=":tweet" component={TweetDetail}/>
13. </Route>
14. </Router>
15. </Router>
16. </Provider>
17. ), document.getElementById('root'));
299 | Página
Sé que en este punto te está por explotarte la cabeza, pues hemos cambiado
drásticamente la forma en que hemos venido trabajando. Pero te pido que no te
desesperes, pues una vez que retomemos el proyecto Mini Twitter verás que es
más fácil de implementar de lo que parece.
Hasta aquí todo está bien, sin embargo, nos faltó mencionar algo, el objeto
mapDispatchToProps no acepta por default funciones, y menos, que están tenga
una respuesta asíncrona, en pocas palabras, podríamos enviarle puros objetos.
El problema de estos, es que en aplicaciones donde todo el API se consume de
forma asíncrona, puede ser un gran inconveniente.
En este nuevo diagrama ya podemos ver más claramente en que parte se sitúa
el Middleware, la importancia que tiene en la arquitectura, es que intercepta el
mensaje del dispatch antes de que llegue a los reducers, espera que las
funciones asíncronas terminen y entonces manda llamar a los reducers.
Página | 300
1. import { createStore, applyMiddleware } from 'redux'
2. import thunk from 'redux-thunk'
3.
4. export const store = createStore(
5. reducer,
6. applyMiddleware(thunk),
7. )
8.
9. render((
10. <Provider store={ store }>
11. <Router history={ browserHistory }>
12. </Router>
13. </Provider>
14. ), document.getElementById('root'));
Debugging Redux
Para nuestra suerte, ya existe un módulo que nos ayuda a depurar en tiempo
real como es que el estado se va actualizando, a medida que las acciones lo van
cambiando. El módulo es redux-logger y para implementarlo solo se requiere
hacer un ajuste un simple:
301 | Página
El primer cambio será importar redux-loger, lo segundo será crear un array para
pasar los dos middlewares (thunk y createLogger). Luego en la línea 6,
validaremos si el ambiente NO es productivo, con la finalidad de agregar el
middlware createLogger a nuestro array de middlewares. Finalmente, creamos
el store como siempre. No es recomendado activar el log en ambiente
producción, debido a que le carga más trabajo a React.
Con este pequeño cambio podremos ver en el log del navegados como va
cambiando el store en cada action:
Página | 302
Fig. 142 - Detalle de Redux-logger
En esta nueva imagen ya podemos ver cómo podemos expandir el log para ver
la estructura completa de los objetos, así como todos sus valores. En esta imagen
podemos ver solo un nivel de profundidad, pero es posible seguir expandiendo
los objetos hasta llegar al más mínimo detalles.
En esta sección prepararemos nuestro proyecto para ser migrado a Redux, por
lo que crearemos las estructuras de carpetas necesarias y explicaremos la
migración de uno de los componentes del proyecto Mini Twitter a modo de
explicación. La intención de esta sección es que, al terminar, tengamos un
ejemplo de react-redux funcionando en nuestro proyecto.
Estrategia de migración
Debido a que migrar toda la aplicación puede llegar a ser una tarea tardada y
complicada, necesitamos tener una estrategia clara de cómo llevaremos la
migración. En esta estrategia explicaremos los pasos que es necesario realizar
para la migración y las consideraciones que debemos de tener en cuenta.
Migrar los componentes es una tarea muy repetitiva, por lo que evitaremos lo
más posible detenernos para explicar lo que estamos haciendo en cada paso, en
lugar de eso, quiero explicarte el proceso que llevaremos para todos los
compontes, dicho proceso será el mismo para todos.
303 | Página
El proceso de migración por componente implica 5 etapas las cuales son:
En la primera etapa, todas las funciones que utilizamos para actualizar el estado
o consultar servicios del API, deberá ser migradas al archivo Actions.js. Estas
nuevas funciones tendrán el siguiente formato:
Las funciones deberán ser constantes y deberá ser exportadas, pues las
consumiremos desde el componte. Las funciones podrán recibir parámetros
custom y los parámetros dispath y state, estos dos últimos son inyectados por
Redux y juegan un papel clave. El parámetro state contiene el estado actual del
store y dispatch es una referencia a la función para disparar actions al store.
Los actions, son objetos que lanzamos al store para indicar nuestras intenciones
de actualizar el estado, dichos objetos pueden tener la estructura que más nos
convenga, pero al menos debe de tener el atributo type. Veamos el siguiente
ejemplo.
Página | 304
Fig. 143 - Etapa 1: migrando funciones y creando los actions.
Para tener un mejor control de los tipos de acciones (types) que podemos lanzar
a través del dispatcher, es necesario crear una serie de constantes que
utilizaremos en la propiedad type de los actions. Estas constantes se crean en el
archivo const.js y tiene un formato como el siguiente:
En esta etapa, crearemos los archivos que atenderán los actions lanzados por
nuestras funciones del archivo Actions.js. Como ya comentamos, los reducer
son funciones puras que actualizan el estado a partir de los actions.
En esta epata tenemos que referencias las funciones que creamos en el archivo
Actions.js y mapear el estado del store en props del componente.
305 | Página
action para actualizar el estado. Cuando la actualización del estado se lleve a
cabo, el componente se actualizará para tomar los nuevos cambios
Iniciaremos con las instalaciones de los módulos necesarias para utilizar Redux
junto con los Middleware, por ello, ejecutaremos los siguientes comandos:
Debido a que Redux cuenta con elementos que antes no teníamos, es necesario
mejorar nuestra estructura de carpetas con la finalidad de tener un mejor orden.
Página | 306
Iniciaremos con la creación de dos carpetas nuevas dentro del path /app, las
cuales se llamarán actions y reducers. La idea es que actions estén los objetos
que serán despachados por el dispatcher y tengamos con constantes todos los
types para ser reutilizados donde sea necesario.
Como ya lo hablamos, los actions son objetos JavaScript simples, que representa
la intención de cambiar el estado dentro del store, también comentamos que
estos objetos pueden tener la estructura que sea, sin embargo, debe de tener al
menos la propiedad type, pues es necesario para que los reducers entienda que
acción hay que realizar sobre el estado.
1. import {
2. LOGIN_SUCCESS,
3. } from './const'
4.
5. import APIInvoker from '../utils/APIInvoker'
6. import { browserHistory } from 'react-router'
7. import update from 'react-addons-update'
8.
9. const loginSuccess = profile => ({
10. type: LOGIN_SUCCESS,
11. profile: profile
12. })
13.
307 | Página
14. const loginFail = () => ({
15. type: LOGIN_SUCCESS,
16. profile: null
17. })
La sintaxis que estamos utilizando para definir los actions son arrow functions
(funciones flecha) por lo que, si no estás familiarizado con ellas, te dejo este link
a la documentación de mozilla.
Una vez que hemos definido los actions, continuaremos desarrollando el reducer
que atenderá a los actions loginSuccess y loginFail. Para ello deberemos de
crear un nuevo archivo llamado LoginReducer.js en el path /app/reducers, el
cual deberá tener el siguiente contenido:
Página | 308
1. import {
2. LOGIN_SUCCESS
3. } from '../actions/const'
4.
5. const initialState = {
6. load: false,
7. profile: null
8. }
9.
10. //Login Reducer
11. export const loginReducer = (state = initialState, action) => {
12. switch (action.type) {
13. case LOGIN_SUCCESS:
14. return {
15. load: true,
16. profile: action.profile
17. }
18. default:
19. return state
20. }
21. }
22.
23. export default loginReducer
Otra de las cosas que podemos apreciar, es que hemos inicializado el estado con
la constante initalState (línea 10). Un error es creer que la variable state
siempre valdrá igual que initalState, sin embargo, este valor solamente lo toma
cuando se inicializa el store, después de esto, el parámetro state deberá ser
enviado por el store en cada invocación.
Una vez ejecutado el reducer, este deberá tener un switch que nos ayuda a
identificar el tipo (type) de acción, ya que a partir de él, sabrá como actualizar el
estado. Notemos que el case del switch hace uso de la constante LOGIN_SUCCES
del archivo const.js.
Una vez que el tipo de mensaje ha sido identificado hay que tomar acciones,
como podemos ver el caso (case) de LOGIN_SUCCES, se crea un nuevo objeto,
indicando que la carga ha terminado y actualizando la propiedad profile con el
valor que nos enviaron en el action.
En el caso de que el type no corresponda con ninguno de las acciones que puede
controlar el reducer es muy importante regresar el mismo estado que entro
como parámetro, pues solo así, Rudex sabe si el estado no cambios y, por ende,
no notifica a los suscriptores. Por esa misma razón es importante no mutar el
estado, pues al retornar la misma instancia, Redux lo puede interpretar como si
no hubiera cambios.
309 | Página
Debido a que la aplicación tendrá más de un reducer, es necesario crear algo
llamado combineReducers, lo cual permite a Redux trabajar con más de un
reducer. Para esto, tendremos que crear un archivo llamado index.js en el path
/app/reducers, el cual deberá tener el siguiente contenido:
De momento solo tenemos un reducer, por lo que no toma mucho sentido utilizar
un combineReducers, pero a medida que avancemos en el proyecto, esto vendrá
creciendo.
Funciones de dispatcher
En este punto ya tenemos creado los actions y los stores que los procesarán,
sin embargo, falta implementar la lógica que disparara (dispatcher) las acciones
hasta el store. Para esto, vamos a estar creando las funciones de dispatcher
dentro del archivo Actions.js.
1. componentWillMount(){
2. let token = window.localStorage.getItem("token")
3. if(token == null){
4. browserHistory.push('/login')
5. this.setState({
6. load: true,
7. profile: null
8. })
9. }else{
10. APIInvoker.invokeGET('/secure/relogin', response => {
11. this.setState({
12. load: true,
13. profile: response.profile
14. });
15. window.localStorage.setItem("token", response.token)
16. window.localStorage.setItem("username", response.profile.userName)
17. },error => {
18. console.log("Error al autenticar al autenticar al usuario " );
19. window.localStorage.removeItem("token")
20. window.localStorage.removeItem("username")
21. browserHistory.push('/login');
22. })
23. }
24. }
Página | 310
Quiero que observes algo clave en esta función, y es que el estado se está
actualizando una vez que el servicio relogin responde. Lo cual rompe con el
principio de Redux de una única fuente de la verdad, por tal motivo, es necesario
cambiar el comportamiento.
Por otra parte, podemos ver que esta nueva función recibe como parámetro las
funciones dispatch y getState. La función dispath es una referencia que inyecta
Redux para poder despachar los actions y getState es la función que nos
regresará el estado actual del Store.
Una vez lista la función relogin, es hora de entrar de frente con el componente
TwitterApp, para esto, tendremos que hacer algunos cambios:
311 | Página
8.
9. class TwitterApp extends React.Component{
10.
11. constructor(props){
12. super(props)
13. this.state = {
14. load: true,
15. profile: null
16. }
17. }
18.
19. componentWillMount(){
20. this.props.relogin()
21. let token = window.localStorage.getItem("token")
22. if(token == null){
23. browserHistory.push('/login')
24. this.setState({
25. load: true,
26. profile: null
27. })
28. }else{
29. APIInvoker.invokeGET('/secure/relogin', response => {
30. this.setState({
31. load: true,
32. profile: response.profile
33. });
34. window.localStorage.setItem("token", response.token)
35. window.localStorage.setItem("username", response.profile.userName)
36. },error => {
37. console.log("Error al autenticar al autenticar al usuario " );
38. window.localStorage.removeItem("token")
39. window.localStorage.removeItem("username")
40. browserHistory.push('/login');
41. })
42. }
43. }
44.
45. render(){
46. return (
47. <div id="mainApp">
48. <Toolbar profile={this.props.profile} /> {this.state.profile}
49. <Choose>
50. <When condition={!this.props.load}> {¡this.state.load}
51. <div className="tweet-detail">
52. <i className="fa fa-circle-o-notch fa-spin fa-3x fa-fw"></i>
53. </div>
54. </When>
55. <When condition={this.props.children == null
56. && this.props.profile != null}>
57. <TwitterDashboard profile={this.props.profile}/>
58. </When>
59. <Otherwise>
60. {this.props.children}
61. </Otherwise>
62. </Choose>
63. <div id="dialog"/>
64. </div>
65. )
66. }
67. }
68.
69. const mapStateToProps = (state) => {
70. return {
71. load: state.loginReducer.load,
72. profile: state.loginReducer.profile
73. }
Página | 312
74. }
75.
76. export default connect(mapStateToProps, { relogin })(TwitterApp);
En este caso, lo mejor será explicar de abajo hacia arriba, pues tendrá más
sentido. Por lo que lo primero que veremos es como usamos la función connect
en la línea 76, veamos que como primer parámetro le hemos mandando la
función mapStateToProps, de la cual ya hemos hablado antes, pero la
analizaremos un poco más adelante nuevamente.
Dentro de la función render, es muy importante notar que hemos cambiado todas
las dependencias del estado (this.state) por las nuevas propiedades definidas
en la mapStateToProps. Recuerda que con Redux, el estado ahora vivirá en el
Store.
El único paso que resta pare terminar de implementar Redux es crear nuestro
store, para ello, será necesario regresar al archivo App.js y realizar las siguientes
modificaciones:
313 | Página
8. import Followings from './Followings'
9. import Followers from './Followers'
10. import TweetDetail from './TweetDetail'
11. import { Router, Route, browserHistory, IndexRoute } from "react-router"
12. import { createStore, applyMiddleware } from 'redux'
13. import { Provider } from 'react-redux'
14. import thunk from 'redux-thunk'
15. import { createLogger } from 'redux-logger'
16. import reducers from './reducers'
17.
18. const middleware = [ thunk ];
19. if (process.env.NODE_ENV !== 'production') {
20. middleware.push(createLogger());
21. }
22.
23. export const store = createStore(
24. reducers,
25. applyMiddleware(...middleware)
26. )
27.
28. render((
29. <Provider store={ store }>
30. <Router history={ browserHistory }>
31. <Route path="/" component={TwitterApp} >
32. <Route path="signup" component={Signup}/>
33. <Route path="login" component={Login}/>
34.
35. <Route path=":user" component={UserPage} >
36. <IndexRoute component={MyTweets} tab="tweets" />
37. <Route path="followers" component={Followers} tab="followers"/>
38. <Route path="following" component={Followings} tab="followings"/>
39. <Route path=":tweet" component={TweetDetail}/>
40. </Route>
41. </Route>
42. </Router>
43. </Provider>
44. ), document.getElementById('root'));
Lo primero que agregaremos son todos los imports necesario para hacer
funcionar Redux.
Lo siguiente es crear nuestro store. Fíjate que lo hemos creado a partir del
reducer, el cual no es más que el combineReducers que hemos creado en el
archivo index.js. Nota que no es necesario importar con todo y el nombre, pues
al llamarse index.js se asume, por lo que sería lo mismo que:
Página | 314
Comprobando la funcionalidad de Redux.
315 | Página
Fig. 147 - Detalle del log
Una vez que hemos migrado nuestro primer componente a Redux, ya podemos
iniciar la migración de todo el proyecto, por lo que a partir de esta sección nos
dedicaremos solo a migrar los componentes a Redux.
El orden con el que migramos los componentes no ha sido aleatorio, pues hemos
pensando que cada componente que migremos pueda ser probado de inmediato
y que la aplicación siga funcionando, por lo que, al terminar de actualizar cada
componente, podrás simplemente actualizar el navegador y ver los resultados.
Página | 316
El componente Login es un poco diferente al caso del componente TwitterApp,
esto debido a que tienes un formulario, por lo que cada vez que el usuario captura
un dato, será necesario actualizar el store, para que este a su vez actualice la
vista.
Iniciaremos la creación de las funciones dispatcher y con los actions, pues están
en el mismo archivo, por lo que agregaremos las siguientes líneas al archivo
Actions.js.
También será necesario agregar los import de las nuevas constantes utilizadas:
1. import {
2. LOGIN_SUCCESS,
317 | Página
3. LOGIN_ERROR,
4. UPDATE_LOGIN_FORM_REQUEST
5. } from './const'
Constantes
Reducer
Vamos a agregar un nuevo reducer que nos ayude a gestionar solo el componente
de Login, por lo cual, crearemos un nuevo archivo llamado LoginFormReducer.js
en el path /app/reducers, el cual tendrá el siguiente contenido:
1. import {
2. UPDATE_LOGIN_FORM_REQUEST,
3. LOGIN_ERROR
4. } from '../actions/const'
5. import update from 'react-addons-update'
6.
7.
8. const initialState = {
9. username: "",
10. password: "",
11. loginError: false,
12. loginMessage: null
13. }
14.
15.
16. export const loginFormReducer = (state = initialState, action) => {
17. switch (action.type) {
18. case UPDATE_LOGIN_FORM_REQUEST:
19. if(action.field === 'username'){
20. let value = action.value.replace(' ','').replace('@','').substring(0, 15)
Página | 318
Este reducer solo puede procesar dos tipos de actions, el
UPDATE_LOGIN_FORM_REQUEST y el LOGIN_ERRROR, el primero se dispara con cada
tecla capturada por el usuario en alguno de los controles y solo actualiza la
propiedad del campo afectado, con excepción del username, en el cual se hace
dos cosas adicionales, quitar la @ y truncar a 15 caracteres. El segundo type, se
ejecuta solo cuando hay algún error al autenticar al usuario, por lo que se
actualiza el estado con el mensaje de error.
El componente Login puede resultar un poco confuso, ya que los controles del
formulario que está ligado al estado. En estos casos, tendremos que ligar los
controles a los props mapeados en mapStateProps y actualizar el estado por medio
de actions.
319 | Página
29. }))
30. }
31.
32. this.setState(update(this.state,{
33. [field] : {$set: value}
34. }))
35. }
36.
37. login(e){
38. e.preventDefault()
39. this.props.loginRequest()
40.
41. let request = {
42. "username": this.state.username,
43. "password": this.state.password
44. }
45.
46. APIInvoker.invokePOST('/login',request, response => {
47. window.localStorage.setItem("token", response.token)
48. window.localStorage.setItem("username", response.profile.userName)
49. window.location = '/'
50. },error => {
51. this.refs.submitBtnLabel.innerHTML = error.message
52. this.refs.submitBtnLabel.className = 'shake animated'
53. console.log("Error en la autenticación")
54. })
55. }
56.
57. render(){
58.
59. return(
60. <div id="signup">
61. <div className="container" >
62. <div className="row">
63. <div className="col-xs-12">
64. </div>
65. </div>
66. </div>
67. <div className="signup-form">
68. <form onSubmit={this.login.bind(this)}>
69. <h1>Iniciar sesión en Twitter</h1>
70.
71. <input type="text" value={this.props.username}
72. placeholder="usuario" name="username" id="username"
73. onChange={this.handleInput.bind(this)}/>
74. <label ref="usernameLabel" id="usernameLabel"
75. htmlFor="username"></label>
76.
77. <input type="password" id="passwordLabel"
78. value={this.props.password} placeholder="Contraseña"
79. name="password" onChange={this.handleInput.bind(this)}/>
80. <label ref="passwordLabel" htmlFor="passwordLabel"></label>
81.
82. <button className="btn btn-primary btn-lg " id="submitBtn"
83. onClick={this.login.bind(this)}>Regístrate</button>
84. <If condition={this.props.state.loginError}>
85. <label ref="submitBtnLabel" id="submitBtnLabel"
86. htmlFor="submitBtn"
87. className="shake animated hidden ">
88. {this.props.state.loginMessage}</label>
89. </If>
90. <p className="bg-danger user-test">Crea un usuario o usa el usuario
91. <strong>test/test</strong></p>
92. <p>¿No tienes una cuenta? <Link to="/signup">Registrate</Link> </p>
93. </form>
94. </div>
Página | 320
95. </div>
96. )
97. }
98. }
99.
100. const mapStateToProps = (state) => {
101. return {
102. state: {
103. username: state.loginFormReducer.username,
104. password: state.loginFormReducer.password,
105. loginError: state.loginFormReducer.loginError,
106. loginMessage: state.loginFormReducer.loginMessage
107. }
108. }
109. }
110.
111. export default connect (mapStateToProps,
112. {updateLoginForm, loginRequest})(Login)
321 | Página
22. }else{
23. let request = {
24. "name": currentState.name,
25. "username": currentState.username,
26. "password": currentState.password
27. }
28.
29. APIInvoker.invokePOST('/signup',request, response => {
30. browserHistory.push('/login');
31. },error => {
32. dispatch(signupResultFail(error.error))
33. })
34. }
35. }
36.
37. const updateSignupFormRequest = (field,value,fieldType) => ({
38. type: UPDATE_SIGNUP_FORM_REQUEST,
39. field: field,
40. value: value,
41. fieldType: fieldType
42. })
43.
44. const validateUserRequest = (userOk, userOkMessage) => ({
45. type: VALIDATE_USER_RESPONSE,
46. userOk: userOk,
47. userOkMessage: userOkMessage
48. })
49.
50. const signupResultFail = (signupFailMessage) => ({
51. type: SIGNUP_RESULT_FAIL,
52. signupFail: true,
53. signupFailMessage: signupFailMessage
54. })
1. import {
2. LOGIN_SUCCESS,
3. LOGIN_ERROR,
4. UPDATE_LOGIN_FORM_REQUEST,
5. SIGNUP_RESULT_FAIL,
6. VALIDATE_USER_RESPONSE,
7. UPDATE_SIGNUP_FORM_REQUEST
8. } from './const'
Constantes
Página | 322
Agregaremos las siguientes constantes al archivo const.js:
Reducer
1. import {
2. UPDATE_SIGNUP_FORM_REQUEST,
3. VALIDATE_USER_RESPONSE,
4. SIGNUP_RESULT_FAIL
5. } from '../actions/const'
6. import update from 'react-addons-update'
7.
8. const initialState = {
9. username: "",
10. name: "",
11. password: "",
12. license: false,
13. userOk: false,
14. userOkMessage: null,
15. signupFail: false,
16. signupFailMessage: null
17. }
18.
19.
20. export const signupFormReducer = (state = initialState, action) => {
21. switch (action.type) {
22. case UPDATE_SIGNUP_FORM_REQUEST:
23. return update(state, {
24. [action.field]: {$set: action.value}
25. })
26. case VALIDATE_USER_RESPONSE:
27. return update(state,{
28. userOk: {$set: action.userOk},
29. userOkMessage: {$set: action.userOkMessage}
30. })
31. case SIGNUP_RESULT_FAIL:
32. return update(state, {
33. signupFail: {$set: action.signupFail},
34. signupFailMessage: {$set: action.signupFailMessage}
35. })
36. default:
37. return state
38. }
39. }
40.
41. export default signupFormReducer
323 | Página
CombineReduces
Página | 324
41. [field] : {$set: e.target.checked}
42. }))
43.
44. }else{
45. this.setState(update(this.state,{
46. [field] : {$set: value}
47. }))
48. }
49. }
50.
51. validateUser(e){
52. let username = e.target.value
53. this.props.validateUser(username)
54.
55. APIInvoker.invokeGET('/usernameValidate/' + username, response => {
56. this.setState(update(this.state, {
57. userOk: {$set: true}
58. }))
59. this.refs.usernameLabel.innerHTML = response.message
60. this.refs.usernameLabel.className = 'fadeIn animated ok'
61. },error => {
62. console.log("Error al cargar los Tweets");
63. this.setState(update(this.state,{
64. userOk: {$set: false}
65. }))
66. this.refs.usernameLabel.innerHTML = error.message
67. this.refs.usernameLabel.className = 'fadeIn animated fail'
68. })
69. }
70.
71.
72. signup(e){
73. e.preventDefault()
74. this.props.signup()
75.
76. if(!this.state.license){
77. this.refs.submitBtnLabel.innerHTML =
78. 'Acepte los términos de licencia'
79. this.refs.submitBtnLabel.className = 'shake animated'
80. return
81. }else if(!this.state.userOk){
82. this.refs.submitBtnLabel.innerHTML =
83. 'Favor de revisar su nombre de usuario'
84. this.refs.submitBtnLabel.className = ''
85. return
86. }
87.
88. this.refs.submitBtnLabel.innerHTML = ''
89. this.refs.submitBtnLabel.className = ''
90.
91. let request = {
92. "name": this.state.name,
93. "username": this.state.username,
94. "password": this.state.password
95. }
96.
97. APIInvoker.invokePOST('/signup',request, response => {
98. browserHistory.push('/login');
99. alert('Usuario registrado correctamente');
100. },error => {
101. console.log("Error al cargar los Tweets");
102. this.refs.submitBtnLabel.innerHTML = response.error
103. this.refs.submitBtnLabel.className = 'shake animated'
104. })
105. }
106.
325 | Página
107. render(){
108.
109. return (
110. <div id="signup">
111. <div className="container" >
112. <div className="row">
113. <div className="col-xs-12">
114.
115. </div>
116. </div>
117. </div>
118. <div className="signup-form">
119. <form onSubmit={this.signup.bind(this)}>
120. <h1>Únite hoy a Twitter</h1>
121. <input type="text" value={this.props.state.username}
122. placeholder="@usuario" name="username" id="username"
123. onBlur={this.validateUser.bind(this)}
124. onChange={this.handleInput.bind(this)}/>
125. <If condition={!this.props.state.userOk
126. && this.props.state.userOkMessage !== null}>
127. <label ref="usernameLabel" id="usernameLabel"
128. className={this.props.state.userOk ? 'fadeIn animated ok' :
129. 'fadeIn animated fail'} htmlFor="username">
130. {this.props.state.userOkMessage}</label>
131. </If>
132.
133. <input type="text" value={this.props.state.name}
134. placeholder="Nombre"
135. name="name" id="name"
136. onChange={this.handleInput.bind(this)}/>
137. <label ref="nameLabel" id="nameLabel" htmlFor="name"></label>
138.
139. <input type="password" id="passwordLabel"
140. value={this.props.state.password} placeholder="Contraseña"
141. name="password" onChange={this.handleInput.bind(this)}/>
142. <label ref="passwordLabel" htmlFor="passwordLabel"></label>
143.
144. <input id="license" type="checkbox" ref="license"
145. value={this.props.state.license} name="license"
146. onChange={this.handleInput.bind(this)} />
147. <label htmlFor="license" >Acepto los terminos de licencia</label>
148.
149. <button className="btn btn-primary btn-lg " id="submitBtn"
150. onClick={this.signup.bind(this)}>Regístrate</button>
151. <If condition ={this.props.state.signupFailMessage !== null}>
152. <label ref="submitBtnLabel"
153. id="submitBtnLabel" htmlFor="submitBtn"
154. className="shake animated">
155. {this.props.state.signupFailMessage}</label>
156. </If>
157. <p className="bg-danger user-test">
158. Crea un usuario o usa el usuario
159. <strong>test/test</strong></p>
160. <p>¿Ya tienes cuenta? <Link to="/login">
161. Iniciar sesión</Link> </p>
162. </form>
163. </div>
164. </div>
165. )
166. }
167. }
168.
169. const mapStateToProps = (state) => {
170. return {
171. state: state.signupFormReducer
172. }
Página | 326
173. }
174.
175. export default connect(mapStateToProps,
176. {updateSignupForm, validateUser, signup})(Signup);
Los cambios que podemos observar son los de siempre, agregar los imports,
pasar la lógica de las funciones Actions.js, remplazar las referencias al estado
por props y conectar el componente a Reduce mediante connect().
327 | Página
1. import {
2. LOGIN_SUCCESS,
3. LOGIN_ERROR,
4. UPDATE_LOGIN_FORM_REQUEST,
5. SIGNUP_RESULT_FAIL,
6. VALIDATE_USER_RESPONSE,
7. UPDATE_SIGNUP_FORM_REQUEST,
8. ADD_NEW_TWEET_SUCCESS,
9. LOAD_TWEETS
10. } from './const'
Constantes
Reducer
1. import {
2. LOAD_TWEETS,
3. ADD_NEW_TWEET_SUCCESS,
4. CLEAR_TWEETS,
5. LIKE_TWEET_REQUEST
6. } from '../actions/const'
7. import update from 'react-addons-update'
8.
9. const initialState = {
10. tweets: []
11. }
12.
13. export const tweetsReducer = (state = initialState, action) => {
14. switch (action.type) {
15. case LOAD_TWEETS:
16. return {
17. tweets: action.tweets
18. }
19. case ADD_NEW_TWEET_SUCCESS:
20. return {
21. tweets: action.tweets
22. }
23. default:
24. return state
25.
26. }
27. }
28.
29. export default tweetsReducer
En este reducer no hay mucho ver, ya que tan solo se actualiza el atributo tweets
con los valores obtenidos por el action.
Combine Reduces
Página | 328
Actualizaremos el archivo index.js para agregar el nuevo reducer:
329 | Página
48.
49. let oldState = this.state;
50. let newState = update(this.state, {
51. tweets: {$splice: [[0, 0, newTweet]]}
52. })
53.
54. this.setState(newState)
55.
56. Optimistic Update
57. APIInvoker.invokePOST('/secure/tweet',newTweet, response => {
58. this.setState(update(this.state,{
59. tweets:{
60. 0 : {
61. _id: {$set: response.tweet._id}
62. }
63. }
64. }))
65. },error => {
66. console.log("Error al cargar los Tweets");
67. this.setState(oldState)
68. })
69. }
70.
71. render(){
72.
73. let operations = {
74. addNewTweet: this.addNewTweet.bind(this)
75. }
76.
77. return (
78. <main className="twitter-panel">
79. <Choose>
80. <When condition={this.props.state.onlyUserTweet} >
81. <div className="tweet-container-header">
82. Tweets
83. </div>
84. </When>
85. <Otherwise>
86. <Reply operations={operations}/>
87. </Otherwise>
88. </Choose>
89. <If condition={this.props.state.tweets != null}>
90. <For each="tweet" of={this.props.state.tweets}>
91. <Tweet key={tweet._id} tweet={tweet}/>
92. </For>
93. </If>
94. </main>
95. )
96. }
97. }
98.
99. TweetsContainer.propTypes = {
100. onlyUserTweet: PropTypes.bool
101. profile: PropTypes.object
102. }
103.
104. TweetsContainer.defaultProps = {
105. onlyUserTweet: false
106. profile: {
107. userName: ""
108. }
109. }
110.
111. const mapStateToProps = (state) => {
112. return {
113. state: {
Página | 330
114. Profile: state.userPageReducer.profile,
115. tweets: state.tweetsReducer.tweets
116. }
117. }
118. }
119.
120. export default connect(mapStateToProps,
121. {getTweet, addNewTweet})(TweetsContainer);
1. //Tweet Component
2. export const likeTweet = (tweetId, like) => (dispatch, getState) => {
3. let request = {
4. tweetID: tweetId,
5. like: like
6. }
7.
8. APIInvoker.invokePOST('/secure/like', request, response => {
9. dispatch(likeTweetRequest(tweetId, response.body.likeCounter))
10. },error => {
11. console.log("Error al cargar los Tweets");
12. })
13. }
14.
15. export const likeTweetDetail = (tweetId, like) => (dispatch, getState) => {
16. let request = {
17. tweetID: tweetId,
18. like: like
19. }
20.
21. APIInvoker.invokePOST('/secure/like', request, response => {
22. dispatch(likeTweetDetailRequest(tweetId, response.body.likeCounter))
23. },error => {
24. console.log("Error al cargar los Tweets");
25. })
26. }
27.
28. const likeTweetRequest = (tweetId, likeCounter) => ({
29. type: LIKE_TWEET_REQUEST,
30. tweetId: tweetId,
31. likeCounter: likeCounter
32. })
33.
34. const likeTweetDetailRequest = (tweetId, likeCounter) => ({
35. type: LIKE_TWEET_DETAIL_REQUEST,
36. tweetId: tweetId,
37. likeCounter: likeCounter
38. })
331 | Página
embargo, hay una ligera diferencia en su funcionamiento, la primera utiliza el
action likeTweetRequest y la segunda el action likeTweetDetailRequest, los
cuales son procesados por dos reducers diferentes, lo que hace que actualicen
una sección diferente del estado.
1. import {
2. LOGIN_SUCCESS,
3. LOGIN_ERROR,
4. UPDATE_LOGIN_FORM_REQUEST,
5. SIGNUP_RESULT_FAIL,
6. VALIDATE_USER_RESPONSE,
7. UPDATE_SIGNUP_FORM_REQUEST,
8. ADD_NEW_TWEET_SUCCESS,
9. LOAD_TWEETS,
10. LIKE_TWEET_REQUEST,
11. LIKE_TWEET_DETAIL_REQUEST
12. } from './const'
Constantes
Reducers
1. import {
2. LOAD_TWEETS,
3. ADD_NEW_TWEET_SUCCESS,
4. CLEAR_TWEETS,
Página | 332
5. LIKE_TWEET_REQUEST
6. } from '../actions/const'
7. import update from 'react-addons-update'
8.
9. const initialState = {
10. tweets: []
11. }
12.
13. export const tweetsReducer = (state = initialState, action) => {
14. switch (action.type) {
15. case LOAD_TWEETS:
16. return {
17. tweets: action.tweets
18. }
19. case ADD_NEW_TWEET_SUCCESS:
20. return {
21. tweets: action.tweets
22. }
23. case LIKE_TWEET_REQUEST:
24. let targetIndex =
25. state.tweets.map( x => {return x._id}).indexOf(action.tweetId)
26. return update(state, {
27. tweets: {
28. [targetIndex]: {
29. likeCounter : {$set: action.likeCounter},
30. liked: {$apply: (x) => {return !x}}
31. }
32. }
33. })
34. default:
35. return state
36. }
37. }
38.
39. export default tweetsReducer
Este nuevo type solo actualiza el valor de la propiedad liked, para reflejar si el
usuario dio like o dislike.
1. import {
2. LOAD_TWEET_DETAIL,
3. ADD_NEW_TWEET_REPLY,
4. LIKE_TWEET_DETAIL_REQUEST
5. } from '../actions/const'
6. import update from 'react-addons-update'
7.
8. let initialState = null
9.
10. export const tweetDetailReducer = (state = initialState, action) => {
11. switch (action.type) {
12. case LIKE_TWEET_DETAIL_REQUEST:
13. if(state._id === action.tweetId){
14. return update(state,{
15. likeCounter : {$set: action.likeCounter},
16. liked: {$apply: (x) => {return !x}}
17. })
18. }else{
19. let targetIndex =
20. state.replysTweets.map( x => {return x._id}).indexOf(action.tweetId)
21. return update(state, {
22. replysTweets: {
333 | Página
23. [targetIndex]: {
24. likeCounter : {$set: action.likeCounter},
25. liked: {$apply: (x) => {return !x}}
26. }
27. }
28. })
29. }
30. default:
31. return state
32. }
33. }
34.
35. export default tweetDetailReducer
Este reducer será ejecutado solo cuando se de like desde el componente Reply
pero cuando este dentro de TweetReply. La diferencia fundamental es que permite
darle like al Tweet principal (línea 13) o a sus hijos (Tweet de respuesta), línea
18.
Combine Reducers
Página | 334
18. this.state = props.tweet
19. }
20.
21. handleLike(e){
22. e.preventDefault()
23.
24. if(this.props.detail){
25. this.props.likeTweetDetail(
26. this.props.tweet._id, !this.props.tweet.liked)
27. }else{
28. this.props.likeTweet(this.props.tweet._id, !this.props.tweet.liked)
29. }
30.
31. let request = {
32. tweetID: this.state._id,
33. like: !this.state.liked
34. }
35.
36. APIInvoker.invokePOST('/secure/like', request, response => {
37. let newState = update(this.state,{
38. likeCounter : {$set: response.body.likeCounter},
39. liked: {$apply: (x) => {return !x}}
40. })
41. this.setState(newState)
42. },error => {
43. console.log("Error al cargar los Tweets", error);
44. })
45. }
46.
47. handleReply(e){
48. $( "html" ).addClass( "modal-mode");
49. e.preventDefault()
50.
51. if(!this.props.detail){
52. render(
53. <Provider store={ store }>
54. <TweetReply tweet={this.props.tweet}
55. profile={this.props.tweet._creator} />
56. </Provider>,
57. document.getElementById('dialog'))
58. }
59. }
60.
61. handleClick(e){
62. if(e.target.getAttribute("data-ignore-onclick")){
63. return
64. }
65. let url = "/" + this.props.tweet._creator.userName
66. + "/" + this.props.tweet._id
67. browserHistory.push(url)
68. }
69.
70. render(){
71. let tweetClass = null
72. if(this.props.detail){
73. tweetClass = 'tweet detail'
74. }else{
75. tweetClass = this.props.tweet.isNew ? 'tweet fadeIn animated' : 'tweet'
76. }
77.
78. return (
79. <article className={tweetClass} onClick={this.props.detail ? '' :
80. this.handleClick.bind(this)}
81. id={"tweet-" + this.props.tweet._id}>
82. <img src={this.props.tweet._creator.avatar}
83. className="tweet-avatar" />
335 | Página
84. <div className="tweet-body">
85. <div className="tweet-user">
86. <Link to={"/" + this.props.tweet._creator.userName} >
87. <span className="tweet-name" data-ignore-onclick>
88. {this.props.tweet._creator.name}</span>
89. </Link>
90. <span className="tweet-username">
91. @{this.props.tweet._creator.userName}</span>
92. </div>
93. <p className="tweet-message">{this.props.tweet.message}</p>
94. <If condition={this.props.tweet.image != null}>
95. <img className="tweet-img" src={this.props.tweet.image}/>
96. </If>
97. <div className="tweet-footer">
98. <a className={this.props.tweet.liked
99. ? 'like-icon liked' : 'like-icon'}
100. onClick={this.handleLike.bind(this)} data-ignore-onclick>
101. <i className="fa fa-heart " aria-hidden="true"
102. data-ignore-onclick></i> {this.props.tweet.likeCounter}
103. </a>
104. <If condition={!this.props.detail} >
105. <a className="reply-icon"
106. onClick={this.handleReply.bind(this)}
107. data-ignore-onclick>
108. <i className="fa fa-reply " aria-hidden="true"
109. data-ignore-onclick></i> {this.props.tweet.replys}
110. </a>
111. </If>
112. </div>
113. </div>
114. <div id={"tweet-detail-" + this.props.tweet._id}/>
115. </article>
116. )
117. }
118. }
119.
120. Tweet.propTypes = {
121. tweet: PropTypes.object.isRequired,
122. detail: PropTypes.bool
123. }
124.
125. Tweet.defaultProps = {
126. detail: false
127. }
128.
129. const mapStateToProps = (state) => {
130. return {}
131. }
132.
133. export default connect(mapStateToProps,
134. {likeTweet, likeTweetDetail})(Tweet);
Solo hay un detalle en el cual vale la pena hacer una pausa, y es referente a las
líneas 53 a 56. Si recordamos el archivo App.js, todos los componentes que están
conectados a Redux necesitan ser hijos del componente <Provider>, lo que no
pasa con el componente TweetReply, pues es creado directamente desde este
componente, es por ese motivo que lo envolvemos mediante un <Provider>.
Página | 336
Refactorizando el componente Reply
En este punto te podrías estar preguntando, ¿en dónde está el action para
guardar el tweet? Recuerda que eso lo delegamos al componente contenedor.
Constantes
Reducer
Lo siguiente será agregar un nuevo reducer que atenderá los nuevos actions,
para esto crearemos el archivo ReplyReducer.js en el path /app/reducers, que
se verá de la siguiente manera:
1. import {
2. UPDATE_REPLY_FORM,
3. RESET_REPLY_FORM
4. } from '../actions/const'
5. import update from 'react-addons-update'
337 | Página
6.
7. let initialState = {
8. focus: false,
9. message: '',
10. image: null
11. }
12.
13. export const replyReducer = (state = initialState, action) => {
14. switch (action.type) {
15. case UPDATE_REPLY_FORM:
16. return update(state, {
17. [action.field]: {$set: action.value}
18. })
19. case RESET_REPLY_FORM:
20. return initialState
21. default:
22. return state
23. }
24. }
25.
26. export default replyReducer
Como podemos observar, este nuevo reducer es muy simple, pues el action
UPDATE_REPLY_FORM solo actualiza el atributo del estado que llega como parámetro
(action.field). Y el action RESET_REPLY_FORM actualiza el estado a su valor de
inicio (initialState).
Combine Reducers
Página | 338
6. import { updateReplyForm, resetReplyForm } from './actions/Actions'
7.
8. const uuidV4 = require('uuid/v4');
9.
10. class Reply extends React.Component{
11.
12. constructor(props){
13. super(props)
14. this.state={
15. focus: false,
16. message: '',
17. image: null
18. }
19. }
20.
21. handleChangeMessage(e){
22. this.props.updateReplyForm(e.target.name,e.target.value)
23.
24. this.setState(update(this.state,{
25. message: {$set: e.target.value}
26. }))
27. }
28.
29. handleMessageFocus(e){
30. this.props.updateReplyForm('focus',true)
31.
32. let newState = update(this.state,{
33. focus: {$set: true}
34. })
35. this.setState(newState)
36. }
37.
38. handleMessageFocusLost(e){
39. if(this.props.state.reply.message.length=== 0){
40. this.props.resetReplyForm()
41.
42. this.reset();
43. }
44. }
45.
46. handleKeyDown(e){
47. //Scape key
48. if(e.keyCode === 27){
49. this.reset();
50. }
51. }
52.
53. reset(){
54. this.props.resetReplyForm()
55. this.refs.reply.blur();
56.
57. let newState = update(this.state,{
58. focus: {$set: false},
59. message: {$set: ''},
60. image: {$set:null}
61. })
62. this.setState(newState)
63. }
64.
65. newTweet(e){
66. e.preventDefault();
67.
68. let tweet = {
69. isNew: true,
70. _id: uuidV4(),
71. _creator: {
339 | Página
72. _id: this.props.state.profile._id,
73. name: this.props.state.profile.name,
74. userName: this.props.state.profile.userName,
75. avatar: this.props.state.profile.avatar
76. },
77. date: Date.now,
78. message: this.props.state.reply.message,
79. image: this.props.state.reply.image,
80. liked: false,
81. likeCounter: 0
82. }
83.
84. this.props.operations.addNewTweet(tweet)
85. this.reset()
86. this.props.resetReplyForm()
87. }
88.
89. imageSelect(e){
90. e.preventDefault();
91. let reader = new FileReader();
92. let file = e.target.files[0];
93. if(file.size > 1240000){
94. alert('La imagen supera el máximo de 1MB')
95. return
96. }
97.
98. reader.onloadend = () => {
99. let newState = update(this.state,{
100. image: {$set: reader.result}
101. })
102. this.setState(newState)
103.
104. this.props.updateReplyForm('image', reader.result)
105. }
106. reader.readAsDataURL(file)
107. }
108.
109.
110. render(){
111. let randomID = uuidV4();
112.
113. let reply = this.props.state.reply
114.
115. return (
116. <section className="reply">
117. <img src={this.props.state.profile.avatar}
118. className="reply-avatar"/>
119. <div className="reply-body">
120. <textarea
121. ref="reply"
122. name="message"
123. type="text"
124. maxLength = {config.tweets.maxTweetSize}
125. placeholder="¿Qué está pensando?"
126. className={reply.focus ? 'reply-selected' : ''}
127. value={reply.message}
128. onKeyDown={this.handleKeyDown.bind(this)}
129. onBlur={this.handleMessageFocusLost.bind(this)}
130. onFocus={this.handleMessageFocus.bind(this)}
131. onChange={this.handleChangeMessage.bind(this)}
132. />
133. <If condition={reply.image != null} >
134. <div className="image-box">
135. <img src={reply.image}/>
136. </div>
137. </If>
Página | 340
138. </div>
139. <div className={reply.focus ?
140. 'reply-controls' : 'hidden'}>
141. <label htmlFor={"reply-camara-" + randomID}
142. className={reply.message.length===0 ?
143. 'btn pull-left disabled' : 'btn pull-left'}>
144. <i className="fa fa-camera fa-2x" aria-hidden="true"></i>
145. </label>
146.
147. <input href="#"
148. className={reply.message.length===0 ?
149. 'btn pull-left disabled' : 'btn pull-left'}
150. accept=".gif,.jpg,.jpeg,.png"
151. type="file"
152. onChange={this.imageSelect.bind(this)}
153. id={"reply-camara-" + randomID}>
154. </input>
155.
156. <span ref="charCounter" className="char-counter">
157. {config.tweets.maxTweetSize - reply.message.length }
158. </span>
159.
160. <button className={reply.message.length===0 ?
161. 'btn btn-primary disabled' : 'btn btn-primary '}
162. onClick={this.newTweet.bind(this)}
163. >
164. <i className="fa fa-twitch" aria-hidden="true"></i> Twittear
165. </button>
166. </div>
167. </section>
168. )
169. }
170. }
171.
172. Reply.propTypes = {
173. profile: PropTypes.object,
174. operations: PropTypes.object.isRequired
175. }
176.
177. const mapStateToProps = state => {
178. return {
179. state:{
180. reply: state.replyReducer,
181. profile: state.loginReducer.profile
182. }
183. }
184. }
185.
186. export default connect( mapStateToProps,
187. {updateReplyForm, resetReplyForm} )(Reply);
Migrar este componente es muy simple, pues no requiere del store ni de ningún
action para funcionar.
341 | Página
1. import React from 'react'
2. import { Link } from 'react-router'
3. import PropTypes from 'prop-types'
4. import { connect } from 'react-redux'
5.
6. class Profile extends React.Component{
7.
8. constructor(props){
9. super(props)
10. this.state = {}
11. }
12.
13.
14. render(){
15. let bannerStyle = {
16. backgroundImage: (this.props.profile.banner!=null ?
17. 'url('+this.props.profile.banner +')' : 'none')
18. }
19. return(
20. <aside id="profile" className="twitter-panel">
21. <div className="profile-banner">
22. <Link to={"/" + this.props.profile.userName}
23. className="profile-name" style={bannerStyle}/>
24. </div>
25. <div className="profile-body">
26. <img className="profile-avatar" src={this.props.profile.avatar}/>
27. <Link to={"/" + this.props.profile.userName}
28. className="profile-name">
29. {this.props.profile.name}
30. </Link>
31. <Link to={"/" + this.props.profile.userName}
32. className="profile-username">
33. @{this.props.profile.userName}
34. </Link>
35. </div>
36. <div className="profile-resumen">
37. <div className="container-fluid">
38. <div className="row">
39. <div className="col-xs-3">
40. <Link to={"/"+this.props.profile.userName}>
41. <p className="profile-resumen-title">TWEETS</p>
42. <p className="profile-resumen-value">
43. {this.props.profile.tweetCount}</p>
44. </Link>
45. </div>
46. <div className="col-xs-4">
47. <Link to={"/"+this.props.profile.userName + "/following"}>
48. <p className="profile-resumen-title">SIGUIENDO</p>
49. <p className="profile-resumen-value">
50. {this.props.profile.following}</p>
51. </Link>
52. </div>
53. <div className="col-xs-5">
54. <Link to={"/"+this.props.profile.userName + "/followers"}>
55. <p className="profile-resumen-title">SEGUIDORES</p>
56. <p className="profile-resumen-value">
57. {this.props.profile.followers}</p>
58. </Link>
59. </div>
60. </div>
61. </div>
62. </div>
63. </aside>
64. )
65. }
66. }
Página | 342
67.
68. Profile.propTypes = {
69. profile: PropTypes.object.isRequired
70. }
71.
72. const mapStateToProps = (state) => {
73. return {
74. profile: state.loginReducer.profile
75. }
76. }
77.
78. export default connect(mapStateToProps, {}) (Profile);
Constantes
Reducer
Este componente hace uso de un nuevo reducer, por lo que será necesario
agregar un nuevo archivo llamado SugestedUserReducer.js el cual deberá tener
el siguiente contenido:
343 | Página
1. import {
2. LOAD_SUGESTED_USERS
3. } from '../actions/const'
4.
5. const initialState = {
6. load: false,
7. users: []
8. }
9.
10.
11. export const sugestedUserReducer = (state = initialState,action) => {
12. switch (action.type) {
13. case LOAD_SUGESTED_USERS:
14. return {
15. load: true,
16. users: action.users
17. }
18. default:
19. return state
20. }
21. }
22.
23. export default sugestedUserReducer
Podemos apreciar claramente que este reducer solo actualiza la propiedad users
con los usuarios enviados en el action y actualiza la propiedad load en true.
Combiner Reducers
Página | 344
4.
5. class SuggestedUser extends React.Component{
6.
7. constructor(){
8. super(...arguments)
9. this.state = {
10. load: false
11. }
12. }
13.
14. componentWillMount(){
15. this.props.getSuggestedUsers()
16.
17. APIInvoker.invokeGET('/secure/suggestedUsers', response => {
18. this.setState({
19. load: true,
20. users: response.body
21. })
22. },error => {
23. console.log("Error al actualizar el perfil", error);
24. })
25. }
26.
27. render(){
28. return(
29. <aside id="suggestedUsers" className="twitter-panel">
30. <span className="su-title">A quién seguir</span>
31. <If condition={this.props.load} >
32. <For each="user" of={this.props.users}>
33. <div className="sg-item" key={user._id}>
34. <div className="su-avatar">
35. <img src={user.avatar} alt="Juan manuel"/>
36. </div>
37. <div className="sg-body">
38. <div>
39. <a href={"/" + user.userName}>
40. <span className="sg-name">{user.name}</span>
41. <span className="sg-username">@{user.userName}</span>
42. </a>
43. </div>
44. <a href={"/" + user.userName}
45. className="btn btn-primary btn-sm">
46. <i className="fa fa-user-plus" aria-hidden="true"></i>
47. Ver perfil</a>
48. </div>
49. </div>
50. </For>
51. </If>
52. </aside>
53. )
54. }
55. }
56.
57. const mapStateToProps = (state) => {
58. return {
59. load: state.sugestedUserReducer.load,
60. users: state.sugestedUserReducer.users
61. }
62. }
63.
64. export default connect(mapStateToProps, {getSuggestedUsers})(SuggestedUser);
Solo será necesario remplazar las referencias al estado por las nuevas props
definidas en mapStateToProps.
345 | Página
Refactorizando el componente TwitterDashboard
Quiero que observes que no hemos agregado una solo línea de código, al
contrario, hemos quitado todas las referencias a las pros. A medida que
migramos a Redux, va perdiendo el sentido pasar props de un padre a los
componentes hijos, pues estos podrán recuperar la información directamente del
store, sin embargo, siempre habrá escenarios donde si vamos a requerir pasar
props.
Página | 346
Refactorizando el componente Toolbar
Constantes
Reducer
347 | Página
16. window.localStorage.removeItem("username")
17. this.props.logout()
18. window.location = '/login';
19. }
20.
21. render(){
22.
23. return(
24. <nav className="navbar navbar-default navbar-fixed-top">
25. <span className="visible-xs bs-test">XS</span>
26. <span className="visible-sm bs-test">SM</span>
27. <span className="visible-md bs-test">MD</span>
28. <span className="visible-lg bs-test">LG</span>
29.
30. <div className="container-fluid">
31. <div className="container-fluid">
32. <div className="navbar-header">
33. <a className="navbar-brand" href="#">
34. <i className="fa fa-twitter" aria-hidden="true"></i>
35. </a>
36. <ul id="menu">
37. <li id="tbHome" className="selected">
38. <Link to="/">
39. <p className="menu-item"><i
40. className="fa fa-home menu-item-icon" aria-hidden="true">
41. </i> <span className="hidden-xs hidden-sm">Inicio</span>
42. </p>
43. </Link>
44. </li>
45. </ul>
46. </div>
47. <If condition={this.props.state.profile != null} >
48. <ul className="nav navbar-nav navbar-right">
49. <li className="dropdown">
50. <a href="#" className="dropdown-toggle"
51. data-toggle="dropdown"
52. role="button" aria-haspopup="true" aria-expanded="false">
53. <img className="navbar-avatar"
54. src={this.props.state.profile.avatar}
55. alt={this.props.state.profile.userName}/>
56. </a>
57. <ul className="dropdown-menu">
58. <li><a href={"/"+this.props.state.profile.userName}>
59. Ver perfil</a></li>
60. <li role="separator" className="divider"></li>
61. <li><a href="#" onClick={this.logout.bind(this)}>
62. Cerrar sesión</a></li>
63. </ul>
64. </li>
65. </ul>
66. </If>
67. </div>
68. </div>
69. </nav>
70. )
71. }
72. }
73.
74. Toolbar.propTypes = {
75. profile: PropTypes.object
76. }
77.
78. const mapStateToProps = (state) => {
79. return {
80. state: {
81. profile: state.loginReducer.profile
Página | 348
82. }
83. }
84. }
85.
86. export default connect(mapStateToProps,{logout})(Toolbar);
Antes de migrar este componente a Redux, tan solo era necesario eliminar del
LocalStorage el token y el usuario. Sin embargo, ahora que todo el estado vive
en el store, eso ya no es suficiente, por lo que hay que ejecutar la función logut
para limpiar los datos del perfil.
Constantes
349 | Página
Las siguientes constantes deberá ser agregadas al archivo const.js:
Reducer
Continuaremos con el reducer que nos ayudará con la actualización del estado,
para ello, crearemos un nuevo archivo llamado FollowerReducer.js en el path
/app/reducers, el cual quedará de la siguiente manera:
1. import {
2. FIND_FOLLOWERS_FOLLOWINGS_REQUEST,
3. RESET_FOLLOWERS_FOLLOWINGS_REQUEST
4. } from '../actions/const'
5.
6. const initialState = {
7. users: []
8. }
9.
10. export const followerReducer = (state = initialState, action) => {
11. switch (action.type) {
12. case FIND_FOLLOWERS_FOLLOWINGS_REQUEST:
13. return {
14. users: action.users
15. }
16. case RESET_FOLLOWERS_FOLLOWINGS_REQUEST:
17. return initialState
18. default:
19. return state
20.
21. }
22. }
23.
24. export default followerReducer
Combine Reduces
Dado que hemos creado un nuevo reducer, será necesario registrar el reducer en
nuestro combinedReducers. Para esto, vamos a modificar el archivo index.js para
quedar de la siguiente manera:
Página | 350
15. tweetsReducer,
16. tweetDetailReducer,
17. replyReducer,
18. sugestedUserReducer,
19. followerReducer
20. })
351 | Página
49. return(
50. <section>
51. <div className="container-fluid no-padding">
52. <div className="row no-padding">
53. <CSSTransitionGroup
54. transitionName="card"
55. transitionEnter = {true}
56. transitionEnterTimeout={500}
57. transitionAppear={false}
58. transitionAppearTimeout={0}
59. transitionLeave={false}
60. transitionLeaveTimeout={0}>
61. <For each="user" of={ this.props.state.users }>
62. <div className="col-xs-12 col-sm-6 col-lg-4"
63. key={this.props.route.tab + "-" + user._id}>
64. <UserCard user={user} />
65. </div>
66. </For>
67. </CSSTransitionGroup>
68. </div>
69. </div>
70. </section>
71. )
72. }
73. }
74.
75. Followers.propTypes = {
76. profile: PropTypes.object
77. }
78.
79. const mapStateToProps = (state) => {
80. return {
81. state: state.followerReducer
82. }
83. }
84.
85. export default connect(mapStateToProps,
86. {findFollowersFollowings, resetFollowersFollowings})(Followers);
Página | 352
9.
10. class Followings extends React.Component{
11.
12. constructor(props){
13. super(props)
14. this.state={
15. users: []
16. }
17. }
18.
19. componentWillMount(){
20. // this.findUsers(this.props.profile.userName)
21. this.props.findFollowersFollowings(this.props.params.user,'followings')
22. }
23.
24. componentWillUnmount(){
25. this.props.resetFollowersFollowings()
26. }
27.
28. componentWillReceiveProps(props){
29. this.setState({
30. tab: props.route.tab,
31. users: []
32. })
33. this.findUsers(props.profile.userName)
34. }
35.
36. findUsers(username){
37. APIInvoker.invokeGET('/followings/' + username, response => {
38. this.setState({
39. users: response.body
40. })
41. },error => {
42. console.log("Error en la autenticación");
43. })
44. }
45.
46. render(){
47. return(
48. <section>
49. <div className="container-fluid no-padding">
50. <div className="row no-padding">
51. <CSSTransitionGroup
52. transitionName="card"
53. transitionEnter = {true}
54. transitionEnterTimeout={500}
55. transitionAppear={false}
56. transitionAppearTimeout={0}
57. transitionLeave={false}
58. transitionLeaveTimeout={0}>
59. <For each="user" of={ this.props.state.users }>
60. <div className="col-xs-12 col-sm-6 col-lg-4"
61. key={this.props.route.tab + "-" + user._id}>
62. <UserCard user={user} />
63. </div>
64. </For>
65. </CSSTransitionGroup>
66. </div>
67. </div>
68. </section>
69. )
70. }
71. }
72.
73. Followings.propTypes = {
74. profile: PropTypes.object
353 | Página
75. }
76.
77. const mapStateToProps = (state) => {
78. return {
79. state: state.followerReducer
80. }
81. }
82.
83. export default connect(mapStateToProps,
84. {findFollowersFollowings, resetFollowersFollowings})(Followings);
Dado que el componente UserCard solo trabaja con los props de entrada y no
realiza ninguna acción, no es necesario realizar ningún cambio, por lo que así
como esta nos servirá para trabajar con Redux.
Página | 354
28.
29. MyTweets.propTypes = {
30. profile: PropTypes.object
31. }
32.
33. export default MyTweets;
Dado que el perfil del usuario lo podemos obtener del store, ya no será necesario
enviarlo como prop. Debido a esto, podemos eliminar los propTypes, así como
tampoco es necesario enviar el perfil al componente TweetContainer, pues él
también puede obtener el perfil del store.
1. componentDidUpdate(prevProps, prevState) {
2. if(prevProps.state.profile.userName !==
3. this.props.state.profile.userName){
4. let username = this.props.state.profile.userName
5. let onlyUserTweet = this.props.onlyUserTweet
6. this.props.getTweet(username, onlyUserTweet)
7. }
8. }
9.
10. componentWillMount(){
11. let username = this.props.state.profile.userName
12. let onlyUserTweet = this.props.onlyUserTweet
13. this.props.getTweet(username, onlyUserTweet)
14. }
355 | Página
9. const getUserProfileResponse = (profile) => ({
10. type: USER_PROFILE_REQUEST,
11. edit: false,
12. profile: profile
13. })
14.
15. export const chageToEditMode = () => (dispatch, getState) => {
16. let currentProfile = getState().userPageReducer.profile
17. dispatch(changeToEditModeRequest(currentProfile))
18. }
19.
20. const changeToEditModeRequest = (currentState) => ({
21. type: CHANGE_TO_EDIT_MODE_REQUEST,
22. edit: true,
23. profile: currentState,
24. currentState: currentState
25. })
26.
27. export const cancelEditMode = () => (dispatch, getState) => {
28. dispatch(cancelEditModeRequest())
29. }
30.
31. const cancelEditModeRequest = () => ({
32. type: CANCEL_EDIT_MODEL_REQUEST
33. })
34.
35. export const updateUserPageForm = (event) => (dispatch, getState) => {
36. dispatch(updateUserPageFormRequest(event.target.id, event.target.value))
37. }
38.
39. const updateUserPageFormRequest = (field, value) => ({
40. type: UPDATE_USER_PAGE_FORM_REQUEST,
41. field: field,
42. value: value
43. })
44.
45. export const userPageImageUpload = (event) => (dispatch, getState) => {
46. let id = event.target.id
47. let reader = new FileReader();
48. let file = event.target.files[0];
49.
50. if(file.size > 1240000){
51. alert('La imagen supera el máximo de 1MB')
52. return
53. }
54.
55. reader.onloadend = () => {
56. if(id == 'bannerInput'){
57. dispatch(userPageBannerUpdateRequest(reader.result))
58. }else{
59. dispatch(userPageAvatarUpdateRequest(reader.result))
60. }
61. }
62. reader.readAsDataURL(file)
63. }
64.
65. const userPageBannerUpdateRequest = (img) => ({
66. type: USER_PAGE_BANNER_UPDATE,
67. img: img
68. })
69.
70. const userPageAvatarUpdateRequest = (img) => ({
71. type: USER_PAGE_AVATAR_UPDATE,
72. img: img
73. })
74.
Página | 356
75. export const userPageSaveChanges = () => (dispatch, getState) => {
76. let state = getState().userPageReducer
77. let request = {
78. username: state.profile.userName,
79. name: state.profile.name,
80. description: state.profile.description,
81. avatar: state.profile.avatar,
82. banner: state.profile.banner
83. }
84.
85. APIInvoker.invokePUT('/secure/profile', request, response => {
86. dispatch(userPageSaveChangesRequest())
87. },error => {
88. console.log("Error al actualizar el perfil");
89. })
90. }
91.
92. const userPageSaveChangesRequest = () => ({
93. type: USER_PAGE_SAVE_CHANGES
94. })
95.
96. export const followUser = (username) => (dispatch,getState) => {
97. let request = {
98. followingUser: username
99. }
100.
101. APIInvoker.invokePOST('/secure/follow', request, response => {
102. dispatch(followUserRequest(!response.unfollow))
103. },error => {
104. console.log("Error al actualizar el perfil");
105. })
106. }
107.
108. const followUserRequest = (follow) => ({
109. type: USER_PAGE_FOLLOW_USER,
110. follow: follow
111. })
357 | Página
Cuando hemos terminado de actualizar los datos del usuario, ejecutamos el botón
de guardar, lo que dispara la función userPageSaveChanges el cual se encargará
de ejecutar el API REST para actualizar definitivamente los datos del perfil del
usuario. Cuando los datos son actualizados, es lanzado el action
userPageSaveChangesRequest, el cual cambiara el componente en modo solo
lectura.
La última acción que puede realizar el usuario es seguir a otro usuario, mediante
el botón de seguir, o dejar de seguirlo, para esto, nos apoyamos de la función
followUser, la cual se encarga de la comunicación con el API. Recordemos que,
si no lo estamos siguiendo, entonces lo empezaremos a seguir, por otro lado, si
ya lo seguimos, entonces lo dejaremos de seguir.
Constantes
Reducer
1. import {
2. USER_PROFILE_REQUEST,
3. CHANGE_TO_EDIT_MODE_REQUEST,
4. CANCEL_EDIT_MODEL_REQUEST,
5. UPDATE_USER_PAGE_FORM_REQUEST,
6. USER_PAGE_AVATAR_UPDATE,
7. USER_PAGE_BANNER_UPDATE,
8. USER_PAGE_SAVE_CHANGES,
9. USER_PAGE_FOLLOW_USER
10. } from '../actions/const'
11. import update from 'react-addons-update'
12.
13. const initialState = {
14. edit: false,
15. profile:{
16. name: "",
17. description: "",
18. avatar: null,
19. banner: null,
20. userName: ""
21. }
22. }
23.
24. export const userPageReducer = (state = initialState, action) => {
Página | 358
25. switch (action.type) {
26. case USER_PROFILE_REQUEST:
27. return {
28. edit: false,
29. profile: action.profile
30. }
31. case CHANGE_TO_EDIT_MODE_REQUEST:
32. return {
33. edit: action.edit,
34. profile: action.profile,
35. currentState: action.currentState
36. }
37. case CANCEL_EDIT_MODEL_REQUEST:
38. return {
39. edit: false,
40. profile: state.currentState,
41. currentState: null
42. }
43. case UPDATE_USER_PAGE_FORM_REQUEST:
44. return update(state, {
45. profile: {
46. [action.field]: {$set: action.value}
47. }
48. })
49. case USER_PAGE_BANNER_UPDATE:
50. return update(state,{
51. profile: {
52. banner: {$set: action.img}
53. }
54. })
55. case USER_PAGE_AVATAR_UPDATE:
56. return update(state,{
57. profile: {
58. avatar: {$set: action.img}
59. }
60. })
61. case USER_PAGE_SAVE_CHANGES:
62. return update(state,{
63. edit: {$set: false}
64. })
65. case USER_PAGE_FOLLOW_USER:
66. return update(state, {
67. profile:{
68. follow: {$set: action.follow}
69. }
70. })
71. default:
72. return state
73. }
74. }
75.
76. export default userPageReducer
Este reducer puede parecer imponente por la gran cantidad de actions que puede
procesar, pero si vamos analizando uno por uno, veremos que no hay nada nuevo
que ver, todos los actions son simples actualizaciones al estado.
Combine Reduces
359 | Página
1. import { combineReducers } from 'redux'
2. import loginReducer from './LoginReducer'
3. import loginFormReducer from './LoginFormReducer'
4. import signupFormReducer from './SignupFormReducer'
5. import tweetsReducer from './TweetReducer'
6. import tweetDetailReducer from './TweetDetailReducer'
7. import replyReducer from './ReplyReducer'
8. import sugestedUserReducer from './SugestedUserReducer'
9. import followerReducer from './FollowerReducer'
10. import userPageReducer from './UserPageReducer'
11.
12. export default combineReducers({
13. loginReducer,
14. loginFormReducer,
15. signupFormReducer,
16. tweetsReducer,
17. tweetDetailReducer,
18. replyReducer,
19. sugestedUserReducer,
20. followerReducer,
21. userPageReducer
22. })
A continuación, los cambios que hay que realizar para migrar el componente a
Redux
Página | 360
36.
37. APIInvoker.invokeGET('/profile/' + user, response => {
38. this.setState({
39. edit:false,
40. profile: response.body
41. });
42. },error => {
43. console.log("Error al cargar los Tweets");
44. window.location = '/'
45. })
46. }
47.
48. imageSelect(e){
49. let id = e.target.id
50. this.props.userPageImageUpload(e)
51.
52. e.preventDefault();
53. let reader = new FileReader();
54. let file = e.target.files[0];
55.
56. if(file.size > 1240000){
57. alert('La imagen supera el máximo de 1MB')
58. return
59. }
60.
61. reader.onloadend = () => {
62. if(id == 'bannerInput'){
63. this.setState(update(this.state,{
64. profile: {
65. banner: {$set: reader.result}
66. }
67. }))
68. }else{
69. this.setState(update(this.state,{
70. profile: {
71. avatar: {$set: reader.result}
72. }
73. }))
74. }
75. }
76. reader.readAsDataURL(file)
77. }
78.
79. handleInput(e){
80. this.props.updateUserPageForm(e)
81.
82. let id = e.target.id
83. this.setState(update(this.state,{
84. profile: {
85. [id]: {$set: e.target.value}
86. }
87. }))
88. }
89.
90. cancelEditMode(e){
91. this.props.cancelEditMode()
92.
93. let currentState = this.state.currentState
94. this.setState(update(this.state,{
95. edit: {$set: false},
96. profile: {$set: currentState}
97. }))
98. }
99.
100. changeToEditMode(e){
101. if(this.props.state.edit){
361 | Página
102. this.props.userPageSaveChanges()
103. this.props.relogin()
104. }else{
105. this.props.chageToEditMode()
106. }
107.
108. if(this.state.edit){
109. let request = {
110. username: this.state.profile.userName,
111. name: this.state.profile.name,
112. description: this.state.profile.description,
113. avatar: this.state.profile.avatar,
114. banner: this.state.profile.banner
115. }
116.
117. APIInvoker.invokePUT('/secure/profile', request, response => {
118. if(response.ok){
119. this.setState(update(this.state,{
120. edit: {$set: false}
121. }))
122. }
123. },error => {
124. console.log("Error al actualizar el perfil");
125. })
126. }else{
127. let currentState = this.state.profile
128. this.setState(update(this.state,{
129. edit: {$set: true},
130. currentState: {$set: currentState}
131. }))
132. }
133. }
134.
135. follow(e){
136. this.props.followUser(this.props.params.user)
137.
138. let request = {
139. followingUser: this.props.params.user
140. }
141. APIInvoker.invokePOST('/secure/follow', request, response => {
142. if(response.ok){
143. this.setState(update(this.state,{
144. profile:{
145. follow: {$set: !response.unfollow}
146. }
147. }))
148. }
149. },error => {
150. console.log("Error al actualizar el perfil");
151. })
152. }
153.
154. render(){
155. let profile = this.props.state.profile
156. let storageUserName = window.localStorage.getItem("username")
157.
158. let bannerStyle = {
159. backgroundImage: 'url(' + (profile.banner) + ')'
160. }
161.
162. let childs = this.props.children
163. && React.cloneElement(this.props.children, { profile: profile })
164.
165. return(
166. <div id="user-page" className="app-container">
167. <header className="user-header">
Página | 362
168. <div className="user-banner" style={bannerStyle}>
169. <If condition={this.props.state.edit}>
170. <div>
171. <label htmlFor="bannerInput" className="btn select-banner">
172. <i className="fa fa-camera fa-2x" aria-hidden="true"></i>
173. <p>Cambia tu foto de encabezado</p>
174. </label>
175. <input href="#" className="btn"
176. accept=".gif,.jpg,.jpeg,.png"
177. type="file" id="bannerInput"
178. onChange={this.imageSelect.bind(this)} />
179. </div>
180. </If>
181. </div>
182. <div className="user-summary">
183. <div className="container-fluid">
184. <div className="row">
185. <div className="hidden-xs col-sm-4 col-md-push-1
186. col-md-3 col-lg-push-1 col-lg-3" >
187. </div>
188. <div className="col-xs-12 col-sm-8 col-md-push-1
189. col-md-7 col-lg-push-1 col-lg-7">
190. <ul className="user-summary-menu">
191. <li className={this.props.route.tab === 'tweets' ?
192. 'selected':''}>
193. <Link to={"/" + profile.userName}>
194. <p className="summary-label">TWEETS</p>
195. <p className="summary-value">
196. {profile.tweetCount}
197. </p>
198. </Link>
199. </li>
200. <li className={this.props.route.tab === 'followings' ?
201. 'selected':''}>
202. <Link to={"/" + profile.userName + "/following" }>
203. <p className="summary-label">SIGUIENDO</p>
204. <p className="summary-value">{profile.following}</p>
205. </Link>
206. </li>
207. <li className={this.props.route.tab === 'followers' ?
208. 'selected':''}>
209. <Link to={"/" + profile.userName + "/followers" }>
210. <p className="summary-label">SEGUIDORES</p>
211. <p className="summary-value">{profile.followers}</p>
212. </Link>
213. </li>
214. </ul>
215.
216. <If condition={profile.userName === storageUserName}>
217. <button className="btn btn-primary edit-button"
218. onClick={this.changeToEditMode.bind(this)} >
219. {this.props.state.edit ? "Guardar" : "Editar perfil"}
220. </button>
221. </If>
222.
223. <If condition={profile.follow != null &&
224. profile.userName !== storageUserName} >
225. <button className="btn edit-button"
226. onClick={this.follow.bind(this)} >
227. {profile.follow
228. ? (<span><i className="fa fa-user-times"
229. aria-hidden="true"></i> Siguiendo</span>)
230. : (<span><i className="fa fa-user-plus"
231. aria-hidden="true"></i> Seguir</span>)
232. }
233. </button>
363 | Página
234. </If>
235.
236. <If condition= {this.props.state.edit}>
237. <button className="btn edit-button" onClick=
238. {this.cancelEditMode.bind(this)} >Cancelar</button>
239. </If>
240. </div>
241. </div>
242. </div>
243. </div>
244. </header>
245. <div className="container-fluid">
246. <div className="row">
247. <div className="hidden-xs col-sm-4 col-md-push-1 col-md-3
248. col-lg-push-1 col-lg-3" >
249. <aside id="user-info">
250. <div className="user-avatar">
251. <Choose>
252. <When condition={this.props.state.edit} >
253. <div className="avatar-box">
254. <img src={profile.avatar} />
255. <label htmlFor="avatarInput"
256. className="btn select-avatar">
257. <i className="fa fa-camera fa-2x"
258. aria-hidden="true"></i>
259. <p>Foto</p>
260. </label>
261. <input href="#" id="avatarInput"
262. className="btn" type="file"
263. accept=".gif,.jpg,.jpeg,.png"
264. onChange={this.imageSelect.bind(this)}
265. />
266. </div>
267. </When>
268. <Otherwise>
269. <div className="avatar-box">
270. <img src={profile.avatar} />
271. </div>
272. </Otherwise>
273. </Choose>
274. </div>
275. <Choose>
276. <When condition={this.props.state.edit} >
277. <div className="user-info-edit">
278. <input maxLength="20" type="text" value={profile.name}
279. onChange={this.handleInput.bind(this)} id="name"/>
280. <p className="user-info-username">
281. @{profile.userName}
282. </p>
283. <textarea maxLength="180" id="description"
284. value={profile.description}
285. onChange={this.handleInput.bind(this)} />
286. </div>
287. </When>
288. <Otherwise>
289. <div>
290. <p className="user-info-name">{profile.name}</p>
291. <p className="user-info-username">
292. @{profile.userName}
293. </p>
294. <p className="user-info-description">
295. {profile.description}</p>
296. </div>
297. </Otherwise>
298. </Choose>
299. </aside>
Página | 364
300. </div>
301. <div className="col-xs-12 col-sm-8 col-md-7
302. col-md-push-1 col-lg-7">
303. {this.props.children}
304. </div>
305. </div>
306. </div>
307. </div>
308. )
309. }
310. }
311.
312. const mapStateToProps = (state) => {
313. return {
314. state: state.userPageReducer
315. }
316. }
317.
318. export default connect(mapStateToProps,
319. {getUserProfile, chageToEditMode, cancelEditMode, updateUserPageForm,
320. userPageImageUpload, userPageSaveChanges, followUser, relogin})(UserPage)
365 | Página
27. this.handleClose()
28. },error => {
29. console.log("Error al cargar los Tweets");
30. })
31. }
32.
33. render(){
34.
35. let operations = {
36. addNewTweet: this.addNewTweet.bind(this)
37. }
38.
39. return(
40. <div className="fullscreen">
41. <div className="tweet-detail">
42. <i className="fa fa-times fa-2x tweet-close" aria-hidden="true"
43. onClick={this.handleClose.bind(this)}/>
44. <Tweet tweet={this.props.tweet} detail={true} />
45. <div className="tweet-details-reply">
46. <Reply profile={this.props.profile} operations={operations}/>
47. </div>
48. </div>
49. </div>
50. )
51. }
52. }
53.
54. TweetReply.propTypes = {
55. tweet: PropTypes.object.isRequired,
56. profile: PropTypes.object.isRequired
57. }
58.
59. export default TweetReply;
Página | 366
18. message: newTweetReply.message,
19. image: newTweetReply.image
20. }
21.
22. APIInvoker.invokePOST('/secure/tweet', request, response => {
23. newTweetReply._id = response.tweet._id
24. dispatch(addNewTweetReplyRequest(newTweetReply))
25. },error => {
26. console.log("Error al cargar los Tweets");
27. })
28. }
29.
30. const addNewTweetReplyRequest = (newTweetReply) => ({
31. type: ADD_NEW_TWEET_REPLY,
32. newTweetReply: newTweetReply
33. })
1. import {
2. LOGIN_SUCCESS,
3. LOGIN_ERROR,
4. UPDATE_LOGIN_FORM_REQUEST,
5. SIGNUP_RESULT_FAIL,
6. VALIDATE_USER_RESPONSE,
7. UPDATE_SIGNUP_FORM_REQUEST,
8. ADD_NEW_TWEET_SUCCESS,
9. LOAD_TWEETS,
10. LIKE_TWEET_REQUEST,
11. LIKE_TWEET_DETAIL_REQUEST,
12. UPDATE_REPLY_FORM,
13. RESET_REPLY_FORM,
14. LOAD_SUGESTED_USERS,
15. LOGOUT_REQUEST,
16. FIND_FOLLOWERS_FOLLOWINGS_REQUEST,
17. RESET_FOLLOWERS_FOLLOWINGS_REQUEST,
18. USER_PAGE_FOLLOW_USER,
19. USER_PAGE_SAVE_CHANGES,
20. USER_PAGE_AVATAR_UPDATE,
21. USER_PAGE_BANNER_UPDATE,
22. UPDATE_USER_PAGE_FORM_REQUEST,
23. CANCEL_EDIT_MODEL_REQUEST,
24. CHANGE_TO_EDIT_MODE_REQUEST,
25. USER_PROFILE_REQUEST,
26. LOAD_TWEET_DETAIL,
27. ADD_NEW_TWEET_REPLY
28. } from './const'
367 | Página
Constantes
Reducer
En esta ocasión no será necesario crear un nuevo store, pues podemos utilizar
el TweetDetailReducer que habíamos creado para el componente Tweet. Solo que
será necesario agregar algunos actions para procesar las solicitudes del
componente TweetDetails.
1. import {
2. LOAD_TWEET_DETAIL,
3. ADD_NEW_TWEET_REPLY,
4. LIKE_TWEET_DETAIL_REQUEST
5. } from '../actions/const'
6. import update from 'react-addons-update'
7.
8. let initialState = null
9.
10. export const tweetDetailReducer = (state = initialState, action) => {
11. switch (action.type) {
12. case LOAD_TWEET_DETAIL:
13. return action.tweetDetails
14. case ADD_NEW_TWEET_REPLY:
15. return update(state, {
16. replysTweets: {$splice: [[0, 0, action.newTweetReply]]}
17. })
18. case LIKE_TWEET_DETAIL_REQUEST:
19. if(state._id === action.tweetId){
20. return update(state,{
21. likeCounter : {$set: action.likeCounter},
22. liked: {$apply: (x) => {return !x}}
23. })
24. }else{
25. let targetIndex =
26. state.replysTweets.map( x => {return x._id}).indexOf(action.tweetId)
27. return update(state, {
28. replysTweets: {
29. [targetIndex]: {
30. likeCounter : {$set: action.likeCounter},
31. liked: {$apply: (x) => {return !x}}
32. }
33. }
34. })
35. }
36. default:
37. return state
38. }
39. }
40.
41. export default tweetDetailReducer
Página | 368
El action LOAD_TWEET_DETAIL solo actualizará el estado para agregar los tweets
recuperados por el API, mientras que ADD_NEW_TWEET_REPLY agrega el nuevo
Tweet a la lista de tweets de respuesta del tweet original.
369 | Página
51. componentWillUnmount(){
52. $( "html" ).removeClass( "modal-mode");
53. }
54.
55. handleClose(){
56. $( "html" ).removeClass( "modal-mode");
57. browserHistory.goBack()
58. }
59.
60. render(){
61. $( "html" ).addClass( "modal-mode");
62.
63. let operations = {
64. addNewTweet: this.addNewTweet.bind(this)
65. }
66.
67. return(
68. <div className="fullscreen">
69. <Choose>
70. <When condition={this.props.state == null}>
71. <div className="tweet-detail">
72. <i className="fa fa-circle-o-notch fa-spin fa-3x fa-fw"></i>
73. </div>
74. </When>
75. <Otherwise>
76. <div className="tweet-detail">
77. <i className="fa fa-times fa-2x tweet-close"
78. aria-hidden="true"
79. onClick={this.handleClose.bind(this)}/>
80. <Tweet tweet={this.props.state} detail={true} />
81. <div className="tweet-details-reply">
82. <Reply profile={this.props.state._creator}
83. operations={operations}
84. key={"detail-" + this.props.state._id} newReply={false}/>
85. </div>
86. <ul className="tweet-detail-responses">
87. <If condition={this.props.state.replysTweets != null} >
88. <For each="reply" of={this.props.state.replysTweets}>
89. <li className="tweet-details-reply" key={reply._id}>
90. <Tweet tweet={reply} detail={true}/>
91. </li>
92. </For>
93. </If>
94. </ul>
95. </div>
96. </Otherwise>
97. </Choose>
98. </div>
99. )
100. }
101. }
102.
103. const mapStateToProps = (state) => {
104. return {
105. state: state.tweetDetailReducer
106. }
107. }
108.
109. export default connect(mapStateToProps,
110. {loadTweetDetail, addNewTweetReply})(TweetDetail);
Página | 370
cerramos la pantalla modal. El resto del componente tiene los cambios de
siempre, por lo que no entraremos en los detalles.
Últimas observaciones
371 | Página
Resumen
Redux es sin duda una de las herramientas más potentes a la hora de desarrollar
aplicaciones con React, pues permite tener un control mucho más estricto del
estado y nos evitamos la necesidad de pasar una gran cantidad de propiedades
a los componentes hijos, haciendo mucho que el desarrollo y el mantenimiento
sea mucho menos complejo. De la misma forma, logramos desacoplar a los
componentes con su dependencia de los padres.
Redux puede llegar a ser un reto la primera vez que lo utilizamos, pero a medida
que nos acostumbramos a utilizarlo, resulta cada vez más difícil desarrollar sin
él.
Quiero felicitarte si has llegado hasta aquí, quiere decir que ya has aprendido
prácticamente todo lo necesario para crear aplicaciones con React. Si bien,
todavía no vemos la parte del backend con NodeJS, quiero recordarte que React
es una librería totalmente diseñada para el FrontEnd, por lo que en este punto,
ya podrías llamarte un FrontEnd developer.
Página | 372
Introducción a NodeJS
Capítulo 13
Hasta este momento, hemos construido una aplicación completa utilizando React
y conectándola a un API REST, sin embargo, poco o nada hemos visto acerca de
NodeJS y como este juega un papel crucial en el desarrollo de aplicaciones web.
Ahora bien, puede que sea una persona que no piensa emprender en este
momento y lo que busques es aprender una nueva tecnología para buscar un
mejor trabajo. En ese caso, déjame decirte que NodeJS es ya hoy en día una de
las tecnologías más buscada por las grandes empresas como Google, Amazon,
Microsoft, etc. Esto quiere decir que aprender NodeJS es sin duda una de las
mejores inversiones que puedes hacer.
373 | Página
Fig. 148 - Posiciones abiertas
Sin embargo, por muy extraño que parezca, NodeJS no es utilizado para eso,
pues no es un navegador, por lo que no puede renderizar elementos en pantalla,
lo que sí, es que mediante NodeJS podemos servir páginas web al navegador.
Pero no solo queda allí la cosa, puede ser utilizado para aplicaciones no web y
ser montado en sistemas embebidos, o en nuestro caso, nos puede servidor como
base para crear todo un API REST.
En nuestro caso, usaremos NodeJS con dos propósitos, el primero y más claro
hasta el momento, es crear nuestra API REST y el segundo, lo utilizamos para
servir nuestra aplicación Mini Twitter al cliente como ya lo hemos estado haciendo
hasta el momento.
Página | 374
NodeJS es un mundo
NodeJS junto con su todo su mundo de librerías que ofrece NPM puede llegar a
ser abrumador, pues NPM es el repositorio de librerías Open Source más grande
del mundo. Debido a esto, es imposible hablar de todo lo que nos tiene por
ofrecer NodeJS o al menos lo más importante. Por esta razón, nos centraremos
exclusivamente en el desarrollo de API’s con NodeJS y explicaremos alguna que
otra curiosidad que sea necesaria a medida que sea requerido.
Introducción a Express
En la actualidad existe más librerías para el desarrollo web y API’s con NodeJS,
lo que quiere decir que Express no es la única opción que tenemos. En realidad,
existen tantas opciones que es muy fácil perderse. Solo para nombrar algunas
alternativas están:
Koa (http://koajs.com/)
Hapi (https://hapijs.com/)
Restify (http://mcavage.me/node-restify/)
Sailsjs (https://sailsjs.com/)
Strapi (https://strapi.io/)
Estos son tan solo algunos ejemplos rápidos, pero existe una infinidad de librerías
más que nos puede servir para este propósito, por lo que alguien recién llegado
a NodeJS simplemente no sabría cual elegir
375 | Página
Ahora bien, ¿Por qué deberíamos utilizar Express en lugar de cualquier otra?, la
respuesta es simple, Express es la librería más ampliamente utilizada y con la
mayor comunidad de desarrolladores, esto hace que este en constante evolución
y madure a una velocidad más rápida que las demás. Ahora bien, nada está
escrito en esta vida, por lo que siempre hay que estar atento a las cosas que
pasan, pues cualquier día de estos, otra librería pueda superar a Express, pero
por ahora, Express es la más conveniente.
Instalando Express
Una vez que te he convencido de usar Express (o al menos eso creo) pasaremos
a la instalación. Dado que Express es una librería más de NodeJS, esta puede ser
instalada mediante NPM como lo hemos estado haciendo para el resto te librerías
que hemos estado utilizando hasta el momento. Para ello, solo basta con instalar
usando el siguiente comando:
El archivo package.json
Muchas personas creen que el archivo package.json, es solo para colocar las
dependencias y configurar algunos scripts para ejecutar el programa, sin
embargo, esto va más allá. Este archivo está pensado para ser un identificador
de nuestro proyecto, pues este, al ser compilado, pasa a ser una librería, y como
toda librería, es posible subirla a los repositorios de NPM. Espera un momento
¿Me estás diciendo que yo puedo crear y subir mi proyecto como una librería a
NPM?, es correcto, nosotros podríamos ahora mismo crear una nueva librería, ya
sea un proyecto completo o una simple utilidad y publicarla para que todo el
mundo la pueda descargar, y por qué no, contribuir en su desarrollo.
name
Página | 376
Las reglas para el nombre (name) son las siguientes:
version
En teoría, los cambios en el paquete deben venir junto con los cambios en la
versión, esto significa que cada vez que realicemos un cambio en el módulo, por
más simple que parezca, tendremos que aumentar la versión.
La versión es un valor numérico, que puede estar separado por secciones, estas
secciones se marcan con un punto “.”, de tal forma que podemos tener versiones
como las siguientes:
1
1.0
1.0.1
1.1.0.1
Cualquier combinación es válida, pero es importante entender cómo administrar
las versiones. Por ese motivo, aquí te enseñaremos la nomenclatura más
utilizada.
377 | Página
Cambios mayores: Son cambios en la librería que tiene un gran
impacto y que por lo general rompen con la compatibilidad con versiones
anteriores. Estos tipos de cambios incluyen cambios en el
funcionamiento de algunos de sus componentes, cambios en las
interfaces expuestas o incluso, un cambio en la tecnología utilizada.
Cambios menores: Son cambios o adición de nuevos features que se
agregan a los ya existentes, sin romper la compatibilidad. Dentro de
estos cambios podemos encontrar, la adición de nuevos métodos o
funciones, nuevas capacidades de la librería, optimizaciones o
reimplementacion de funcionalidades encapsuladas que no afectan al
usuario final.
Bugs: Esta sección la incrementamos cada vez que corregimos uno o
una serie de bugs, pero sin agregar o remplazar funcionalidad,
simplemente son correcciones. La clave en esta sección es que no
debemos incluir nuevos features, si no correcciones a los existentes.
description
Este es solo un campo que nos permite poner una breve descripción de lo hace
nuestro paquete. Es utilizado como una guía para que las personas puedan saber
qué hace tu proyecto y es utilizado por NPM para realizar búsquedas.
Keywords
bugs
Aquí es posible definir una URL que nos lleve a la página de seguimiento de bugs
y también es posible definir un email para contactar al equipo de soporte.
1. {
2. "url" : "https://github.com/owner/project/issues",
3. "email" : "[email protected]"
4. }
autor
Campo que nos permite poner el nombre del autor de la librería, puede ser el
nombre del desarrollador o la empresa que lo está desarrollando.
Página | 378
license
scripts
Este es un campo muy importante, pues nos permite crear scripts para compilar,
construir, deployar y ejecutar la aplicación, sin embargo, este campo es complejo
y requiere de un entendimiento más avanzado para comprender todas sus
posibilidades. Para ver la documentación completa acerca de cómo construir
script puedes ir a la siguiente URL (https://docs.npmjs.com/misc/scripts).
devDependencies
Aquí se enlistan todos los módulos que son requeridos para ejecutar la aplicación
en modo desarrollo, estos módulos estarán disponibles solo cuando la aplicación
no se ejecute en modo productivo.
Campo dependencies
Aquí definimos las librerías indispensables para correr nuestra aplicación, ya sea
en modo desarrollo o productivo. Esto quiere decir que estas librerías son
indispensables en todo momento.
Documentación de package.json
Node Mudules
A estas alturas del libro, seguramente ya entiendes a la perfección que son los
módulos de Node, pero solo para dejarlo claro. Los módulos son todos aquellos
paquetes que descargamos con ayuda del comando install de NPM.
379 | Página
opciones para visualizar esta carpeta, la primera y más simple, es verla
directamente sobre el explorar de archivos de tu sistema operativo, es decir,
diriges físicamente a la carpeta del proyecto y la podrás ver:
Página | 380
Tras hacer esto, en automático podrás ver la carpeta node_modules en el árbol
del proyecto:
Yo por lo general prefiero tenerlo oculto, pues es raro que necesite ver la carpeta,
además, al hacer eso, también me muestra la carpeta .get, en la cual se guarda
todo el historial de cambios del repositorio de GIT.
Como sea, lo interesante es que en esta carpeta podremos ver todos los módulos
que vamos instalando en nuestro proyecto y que por ende, estarán disponibles
en tiempo de ejecución. Solo por nombrar algunos ejemplos, podrás encontrar
las carpetas React, React-redux, React-router, jsx-control-statements, etc.
Ya con un entendimiento más claro de lo que es NodeJS y como los paquetes son
administrados, pasaremos a implementar nuestro primero servidor con NodeJS.
En el pasado ya habíamos creado el archivo server.js con la intención de
soportar las reglas de route de React-router, pero no habíamos entrado en los
detalles.
Ahora bien, para aprender desde cero, crearemos un nuevo archivo llamado
express-server.js con la intención de hacer algunas pruebas y no perder el
381 | Página
archivo server.js que ya tenemos funcionando. La intención es usar este nuevo
archivo solo durante este capítulo, después de esto, podremos borrarlo.
Tan solo con estas pocas líneas hemos creado un servidor Express que responde
en el puerto 8181. Hemos cambiado de puerto para evitar que choque con el que
ya tenemos configurado por el proyecto Mini Twitter.
Página | 382
Fig. 152 - Hello world con Express.
Express Verbs
Cuando una aplicación se comunica con un servidor HTTP, este le tiene que
indicar que método utilizará para la comunicación, pues cada uno de ellos tiene
una estructura diferente. La principal diferencia que radica entre cada uno de
ellos, es la interpretación que le da el servidor a cada uno de ellos.
El protocolo HTTP así como Express, soportan una gran cantidad de método, sin
embargo, los más utilizados son 4, y el resto es utilizado para cosas más
específicas que no tendría caso comentar ahora. Si quieres ver la lista completa,
puedes verla en la documentación oficial de Express.
Método GET
Este es el método más utilizado por la WEB, pues es el que usa el navegador
para consultar una página a un servidor. Cuando entramos a cualquier página,
ya sea Facebook, Google o Amazon, el navegador lanza una petición GET al
servidor y este le regresa el HTML correspondiente a la página.
383 | Página
Las peticiones no llevan un payload asociado a la petición, si no que la URL es lo
único necesario para que el servidor HTTP sepa qué hacer con ella. En resumidas
cuentas, el método GET es utilizado como un método de consulta.
Método POST
El método POST es el segundo método más utilizado por la WEB, pues permite
enviarle información al servidor, como puede ser el formulario de una página,
una imagen, un JSON, XML, etc. Mediante la barra del navegador es imposible
enviar peticiones POST, pero si es posible mediante programación, formularios o
programas especializados para probar recursos web como es el caso de SoapUI.
Método PUT
Método DELETE
Consideraciones adicionales.
HTTP hace una serie de recomendación de cómo los métodos se deben utilizar,
sin embargo, esto no es garantía que se cumpla, pues perfectamente podrías
mandar una petición DELETE para crear un nuevo registro, o un POST para
actualizar o un GET para borrar. Cuando entremos de lleno a la creación de
nuestros servicios REST retomaremos este tema y veremos las mejores prácticas
para la creación de servicios. Por ahora, basta con que comprendas teóricamente
como deberían de funcionar.
Página | 384
Implementemos algunos métodos.
Para comprender un poco cómo funcionan los métodos, crearemos un router que
procese las solicitudes de los métodos que analizamos hace un momento, para
ello, agregaremos las siguientes líneas a nuestro archivo express-server.js:
Lo que hemos hecho es muy simple, hemos agregado 3 nuevos routers que
procesan las solicitudes para POST (línea 8), PUT (línea 12), DELETE (línea 16),
lo que significa que cuando entre cualquier petición al servidor por cualquiera de
estos métodos, será procesado por el router apropiado.
Observa que tan solo es necesario utilizar app.<method> para crear un router para
cada método, y el * indica que puede procesar cualquier URL entrante, y no solo
la raíz.
Ahora bien. Para probar esto, podemos utilizar SoapUI y ejecutar la URL
http://localhost:8181 en los cuatro métodos disponibles:
385 | Página
Fig. 153 - Probando el método GET
Página | 386
Fig. 155 - Probando el método PUT.
387 | Página
Como hemos visto en las imágenes anteriores, ante la misma URL pero con
método diferente, obtenemos un resultado diferente, pues el router que procesa
la solicitud es diferente.
Con esto, nos debe quedar claro que podemos atender la misma URL pero con
distintos comportamientos, porque una misma URL podría hacer cosas diferentes
con tan solo cambiar el método, y es allí donde radica la magia del API REST.
Cuando la WEB nació a principio de los 80’s, jamás se imaginó el alcance que
tendría y como esta evolucionaría para crea aplicaciones tan complejas como lo
son hoy en día. En sus inicios, todas las URL a los recursos de internet, eran
meramente un enlace a un documento alojado en otro servidor o directorio del
mismo servidor, y la URL como tal, era irrelevante, por lo que nos encontrábamos
links como los siguientes:
http://server.com/?page=index
http://server.com/121202/134%2023.html
Estas URL, si bien, funcionan, la realidad es que no son nada descriptivas, pues
no te da ninguna idea de lo que va hace o donde te van a enviar.
Query params
http://api.com/?param1=val1¶m2=val2¶m3=val3
Cada parámetro debe estar separado con un ampersand (&) y debe de anteponer
un signo de interrogación (?) antes de iniciar con los parámetros.
Para recuperar un query param en Express solo tenemos que ejecutar la siguiente
instrucción req.query.<param-name>, veamos el siguiente ejemplo:
Página | 388
3. const lastname = req.query.lastname
4. res.send("Hello world GET => " + name + ' ' + lastname);
5. });
http://localhost:8181/?name=Oscar&lastname=Blancarte
URL params
Los URL params, son parámetros que se pueden pasar por medio de la misma
URL y no estamos hablando de los Query params, en su lugar, las mismas
secciones de una URL se pueden convertir en parámetros, por ejemplo, en el
proyecto Mini Twitter, usamos un servicio para consultar el perfil de un usuario,
para esto, ejecutar una URL como la siguiente: http://api.com/profile/test, en
esta URL, test, es un URL Param, y puede ser recuperado para ser utilizado como
un parámetro.
389 | Página
Es muy importante que este nuevo router este por arriba del router GET que ya
teníamos, pues como el otro acepta todas las peticiones, no dejará que esta
nueva se procese.
Ahora bien, vemos que hemos cambiado el path para aceptar dos url params, los
cuales son name y lastname, luego estos son recuperados mediante
req.params.name y req.params.lastname.
http://localhost:8181/Oscar/Blancarte
Body params
Para superar este problema, tendríamos que definir un Middleware, el cual cache
el mensaje primero que los Router y luego procese el payload para el final dejarlo
en el objeto request. Esto quedaría de la siguiente manera:
Página | 390
4. req.on('end', function(){
5. req.body = data;
6. next();
7. })
8. })
Más adelante veremos qué es esto de los Middleware. Ahora bien, eso que
estamos viendo en pantalla no lo vamos a requerir, porque existe una librería
que nos facilita la vida y que adicional, nos convierte el mensaje a JSON o al tipo
que necesitemos.
Body-parse module
391 | Página
30. app.put('*', function (req, res) {
31. res.send("Hello world PUT");
32. });
33.
34. app.delete('*', function (req, res) {
35. res.send("Hello world DELETE");
36. });
37.
38. app.listen(8181, function () {
39. console.log('Example app listening on port 8181!');
40. });
Middleware
Página | 392
Los Middleware son funciones comunes y corrientes que tiene la particularidad
de ejecutarse antes que los routings de los métodos tradicionales. Los Middlware
los podemos ver como interceptores que toman la ejecución antes que los demás,
hacen cualquier cosa y luego pasan la ejecución al siguiente middlware, al
termina la cadena de Middleware, la ejecución pasa al routing apropiado para
atender la petición.
393 | Página
Existe 5 tipos de middleware soportados por Express:
Página | 394
de reglas de ruteo en un solo objeto, con la finalidad de facilitar su administración
a medida que crece el número de routeos.
Más adelante veremos cómo este tipo de middleware nos ayudará a separar los
routeos que van a la aplicación Mini Twitter de las que van al API.
En el ejemplo anterior vemos ver cómo estamos creando una serie de routeos
pero por medio del objeto router, luego, este objeto es utilizando para crear un
nuevo middleware (línea 12) que atiende en la URL /api.
Middleware de terceros
Middleware incorporado
395 | Página
separados en módulos independientes. Como es el caso de body-parser que
anteriormente venía incorporado, pero hoy en día es un módulo independiente.
1. app.use(express.static(path.join(__dirname, 'public')));
Error Handler
Un error handler son un tipo especial de Middleware, pues permiten gestionar los
errores producidos en tiempo de ejecución. Un error handlers se definen
exactamente igual que los middlewares a nivel de aplicación, pero con la
diferencia de que estos reciben 4 parámetros, donde el primero es el error (err),
el segundo el request (req), el tercero el response (res) y el cuarto es el next.
Ahora bien, es posible tener más de un handler global y más de uno específico
para el mismo path, lo que pasará es que se ejecutarán en el orden en que fueron
definidos, pero siempre y cuando, el handler anterior ejecute next().
Página | 396
Resumen
Hemos aprendido las distintas formas que tiene express para recibir parámetros,
mediante url params, query params y body param o payload.
Sin duda, este capítulo nos deja listos para empezar a construir el API, pero antes
de iniciar con eso, nos introduciremos a MongoDB para conocer cómo funciona y
aprender a conectarnos desde NodeJS.
397 | Página
Introducción a MongoDB
Capítulo 14
Como siempre, me gusta explicarle por qué MongoDB es una tecnología que vale
la pena aprender y cómo es que esta está ganando popularidad rápidamente. A
pesar de que MongoDB ha venido subiendo rápidamente en popularidad, la
realidad es que mucha gente todavía se siente desconfiada de usar esta base de
datos, pues rompe completamente con los paradigmas que durante años se nos
ha enseñado.
Página | 398
Fig. 161 - Grafica de intereses 2017
La gráfica anterior, hace una comparación con los diversos tópicos o temas más
relevantes que la gente ha manifestado con mayor interés de aprender, por lo
que no solo se habla de bases de datos, si no que se compara con cosas como
Realidad Virtual, Internet de las cosas (IoT), Machine Learning, DevOps, Cloud
computing, etc. Lo que a mí me llama la atención, es que las bases de datos
NoSQL (entre las que se incluye MongoDB) representa el segundo lugar de
popularidad, solo sobrepasada por la Arquitectura de Software.
Esta gráfica es muy reveladora, pues no dice que la gran mayoría de personas,
están muy interesadas en aprender Bases de datos NoSQL, lo que sin duda
también disparará la demanda de esta base de datos.
Ahora bien, con estos datos, quiero que veas la siguiente gráfica:
399 | Página
La siguiente gráfica ilustra las posiciones abiertas en enero de 2016, en las cuales
podemos apreciar las principales bases de datos del mercado. Primero que nada,
quiero que observes, como mongo a final de la gráfica, se logra posicionar como
la tercera base de datos más solicitada, solo superada por Oracle y SQL Server.
Sé que la diferencia entre Oracle y SQL Server es abismal. Sin embargo, hay que
recordar que estas dos bases de datos son el estatus quo del momento, es decir,
es donde todas las empresas están actualmente y en muchas de ellas, ya están
empezando a mirar partes de sus aplicaciones a MongoDB, por lo que se espera
que, en los próximos años, las bases de datos NoSQL tomen mucho más fuerza
y empiece a desplazar a las SQL.
Ahora bien. MongoDB no está diseñado para todo tipo de aplicaciones, por lo que
sin duda SQL seguirá teniendo su lugar.
Como conclusión a todo este análisis, cerraría diciendo que MongoDB es sin duda
unas de las tecnologías con mayor potencial en los años que siguen y que la
oferta de trabajo para gente con este perfil va a subir drásticamente, por lo que
es buen momento para empezar a aprender.
Solo por poner un ejemplo, me toco conocer de un proyecto para inducir a los
niños a la tecnología, mediante el lanzamiento e globos orbitales, estos globos
son construidos con dispositivos ultra económicos, como tarjetas Arduino y todo
tipo de sensores compatibles. La idea del proyecto es armar globos que suban
hasta la atmosfera y en su camino vallan registrando temperatura, altura,
posición, humedad y tiene una cámara para tomar fotos cada minuto. Lo
interesante es que este globo, tiene un programa en NodeJS el cual es el
encargado de gestionar la comunicación con los sensores, para finalmente
guardar los registros en una base de datos MongoDB.
Mientras el globo vuela, los alumnos pueden seguir la trayectoria por el GPS para
recuperar la capsula (donde está el hardware). El globo se revienta al entrar a la
atmosfera y empieza su caída en picada. A cierta altura se activa un paracaídas
que hace que la capsula descienda suavemente y va fotografiando su caída. Al
Página | 400
final los alumnos pueden saber dónde cayo exactamente por el GPS y las últimas
fotos que tomo antes de tocar tierra. A y se me olvida, los alumnos se encargan
de programar todo lo necesario para el funcionamiento
Ahora bien, esto es sin hablar de la robótica, wearables (ropa, relojes, etc.),
dispositivos electrónicos que requiere almacenar información, etc. Lo triste, es
que cuando hablamos de aplicaciones y bases de datos, siempre se nos viene a
la mente los sistemas de información, sin embargo, existen muchísimas más
cosas que requieren de una base de datos.
Con este contexto, parece que queda claro que el contexto de MongoDB en una
aplicación, puede ser en cualquier lugar que se requiera almacenamiento y que
este no requiera de gran cantidad de actualizaciones sobre un mismo objeto.
Para comprender como funciona MongoDB, es necesario conocer que son las
Colecciones y los documentos, pues son el equivalente que existe entre Tablas y
Columnas.
Las colecciones, es la forma que tiene Mongo para agrupar los documentos,
por ejemplo, podemos tener una colección para los usuarios y otra para los
Tweet. Estas colecciones no restringen la estructura que un registro puede tener,
si no que ayuda solo a agruparlos. Por ejemplo, veamos como guardamos la
información del proyecto Mini Twitter:
401 | Página
Fig. 163 - Colecciones en MongoDB.
Por muy impresionante que parezca, en toda la aplicación solo utilizamos dos
colecciones, una para los usuarios (profiles) y otra para los tweets (tweets), lo
cuales podemos ver del lazo izquierdo. Estas dos colecciones las utilizamos para
agrupar los usuarios y los tweets por separado, pero en ningún momento,
definimos la estructura que puede tener una colección.
1. {
2. “_id”: “59f9f12317247f48f13367b3”,
3. “_creator”: “59f90ca2de72f70dd9a8d819”,
4. “message”: “Mi primer Tweet”,
5. “image”: null,
6. “replys”: 0,
7. “likeCounter”: 0,
8. “date”: “2017-11-01 10:06:59.036”
9. }
Página | 402
MongoDB también permite la creación de índices, con lo cual, podemos aumentar
el performance en las búsquedas. Los índices son apuntadores, que permite a la
Mongo encontrar de forma más eficiente un documento. Aunque esto es posible
hacerlo desde la terminal o desde un cliente, dejaremos esta responsabilidad a
la librería que utilizaremos para conectarnos a Mongo desde NodeJS.
Lo primero que aprenderemos será a crear colección, para ello, nos dirigiremos
a Compass y nos colocaremos en nuestra base de datos, en nuestro caso sería
“test”, la cual podemos ubicar del lado izquierdo. Una vez allí, nos mostrará el
listado de todas las colecciones existentes hasta el momento, por lo que, si ya
hemos trabajado con el proyecto Mini Twitter, ya deberías de tener creadas las
colecciones profiles y tweets.
Una vez allí, presionamos el botón “CREATE COLLECTION” y nos arrojará una nueva
pantalla:
403 | Página
En nuestro caso no queremos que sea Capped, por lo que solo le podremos el
nombre “animals” para realizar pruebas. Presionamos nuevamente “CREATE
COLLECTION” y listo, habremos creado nuestra primera colección.
Una vez echo este paso, la nueva colección deberá aparecer del lado derecho y
damos click en ella para ver su contenido. Lógicamente, estará vacía, pero más
adelante insertaremos algunos registros.
Insertar un documento
Una vez que ya estamos dentro de la colección, podremos observar el botón que
dice “INSER DOCUMENT” el cual presionaremos. Una nueva pantalla nos
aparecerá y no solicitará los valores para nuestro documento:
Como verá, por default nos va a crear un campo llamado “_id”, el cual no
podremos eliminar, pues es el único valor obligatorio. Lo siguiente será empezar
a capturar los valores de nuestro documento.
Página | 404
Y repetiremos lo mismo para otros 4 animales, puedes poner lo que sea para que
practiques. Una vez que termines de capturar los datos, solo presionar el botón
“INSERT” para guardar el documento.
El botón “FIND” actualiza la pantalla para ver todos los registros guardados.
Como vez, el número y nombre de los campos no está limitado, incluso
podríamos crear un nuevo animal que tenga un valor que el resto no, por
ejemplo, voy a crear otro animal que se llame “zapo” y le voy a poner un campo
nuevo llamado “patas:4”:
Así de fácil es crear un documento, claro que estos documentos son simples, pero
cualquier valor podrá contener otro objeto dentro.
405 | Página
Actualizar un documento
Actualizar un documento es todavía más fácil que crearlo, pues solo hace falta
ponernos sobre el registro que queremos actualizar y presionar el pequeño lápiz
que sale del lado derecho.
Eliminar un documento
Página | 406
Consultar un documento
407 | Página
Filter: corresponde a la sección WHERE de SQL, en ella ponemos en
formato {clave: valor} los elementos a filtrar.
Projection: En esta sección ponemos los campos que esperamos que
nos regrese la búsqueda, es similar a la sección (SELECT columna1,
columna2). La proyección se escribe en formato {clave: valor}.
Sort: Esta sección se utiliza para ordenar los elementos de la respuesta
y correspondería a la instrucción ORDER BY de SQL. Esta sección se
escribe en formato {clave: valor}, donde la clave es el nombre del
campo a ordenar y el valor solo puede ser 1 o -1 para búsquedas
ascendentes y descendentes.
Skip: Permite indicar a partir de que registro se regresen los resultados,
es utilizado con frecuencia para la paginación.
Limit: Se utiliza para establecer el número máximo de registros que
debe de regresar la consulta. Se utiliza en conjunto con Skip para lograr
la paginación. Es similar a la instrucción TOP o LIMIT de SQL.
Filter
Página | 408
Fig. 172 - Busca de animales de color blanco.
Como puedes apreciar, solo hace falta indicar el campo y valor en formato {clave:
valor}.
Operadores lógicos
Los operadores lógicos son todas aquellas expresiones entre dos o más
operadores donde su evaluación da como resultado un booleano, y que por lo
tanto siguen la teoría del algebra de Boole (Tabla de la verdad). Los operadores
disponibles son los siguientes:
Operados Descripción
AND Retorna true si todas las condiciones son verdaderas
409 | Página
Operador AND
si queremos que adicional al color, la edad sea igual a 5. Solo deberemos agregar
ese nuevo campo en el filtro:
Solo hace falta separar los valores con una coma, entre cada campo a filtrar. En
este caso, la condición se está evaluando como un AND, por lo que los dos
criterios se deberán cumplir para que el resultado sea retornado.
En este formato, el clave deberá ser el operador $and y como valor, se pasa un
array, en donde cada posición corresponde a una condición. El array puede tener
2 o más condiciones.
Operador OR
Página | 410
Fig. 174 - Búsqueda mediante el operador OR.
Como puedes observar, es necesario iniciar con el operador $or, y como valor,
deberemos enviarle un array, en donde cada posición del array, será una
condición a evaluar mediante el operador OR, en este arreglo puede haber de 2
a N condiciones.
Este operador funciona prácticamente igual que OR, no la diferencia de que este
niega la expresión. Utilizamos el operador NOR para indicar expresiones como
“Selecciona todos los animales donde el nombre NO sea Perro o la edad NO sea
1”:
411 | Página
Fig. 175 - Operador NOR
De los 6 registros que tenemos, solo nos arroja 3, pues dos de ellos tiene edad
= 1 y uno tiene como nombre = Perro. En este operador con una sola condición
que se cumpla será suficiente para que el registro no se muestre.
Operador NOT
El operador NOT se utiliza para negar una expresión, como parámetro recibe un
boolean o una expresión que lo retorne, para finalmente negar el valor.
Página | 412
Fig. 176: operador NOT
Operadores de comparación
Operador Descripción
$eq Valida si un campo es igual a un valor determinado, su
nombre proviene de “equals”.
413 | Página
1. { <field>: { $ne: <value> } }
Caso 1:
Busquemos todos los animales que tenga más de 1 años y que sean de color sea
diferente de café:
Página | 414
Fig. 177 - Operadores de comparación $gt y $ne.
Vemos que, en esta consulta, hemos utilizado los operadores de comparación $gt
(mayor que un año) y $ne (Diferente de Café), adicional, nos hemos apoyado de
operador lógico $and para unir las dos condiciones.
Caso 2:
Encontremos todos los animales que sean “Perro, Conejo y Ratón” o animales
que tenga 4 patas:
415 | Página
Algo interesante en este query, es que solo el Zapo tiene la propiedad patas, lo
que comprueba la versatilidad que tiene MongoDB para crear estructuras
dinámicas.
En estos dos simples, pero prácticos ejemplos, hemos aprendido a utilizar los
operadores de comparación. Yo te invito que adicional a los ejemplos que hemos
planteado, te pongas un rato a jugar con el resto de operadores para que
compruebes por ti mismo como funcionan y aprender incluso a combinarlos con
los operadores lógicos.
Operador Descripción
$exists Valida si un documento tiene o no un campo determinado, si el
valor del operador es true, entonces buscará todos los documentos
que si cuenten con el campo, por otra parte, si el valor se establece
en false, entonces buscará todos los documentos que no cuente
con el campo.
$all Este operador valida si un array del documento contiene todos los
valores solicitados.
Caso 1:
Página | 416
Fig. 179 - Utilizando el operador de elementos $exists
Zapo es el único animal retornado, pues es el único que cuenta con la propiedad
“patas”. Observa que al operador le hemos puesto el valor true. Esto indica que
buscamos los que SI tengan el atributo, pero también le pudimos haber puesto
false, lo que cambiaría el resultado, pues buscaría solo los documentos que no
tuvieran el atributo. El operador $exists sol valida que el campo exista, sin
importar su valor
Caso 2:
Para probar el operador $type va a ser necesario crear un nuevo registro, el cual
será el siguiente:
Quiero que prestes atención en el campo edad, pues a diferencia del resto, ha
este le he puesto que la edad es de tipo String, mientras que al resto les puse
Int32:
417 | Página
Caso 3:
Otro ejemplo sería buscar sobre los elementos de un array con ayuda del el
operador $all, para realizar una prueba con este operador deberemos crear dos
nuevos registros, los cuales tenga una lista de “apodos”. Yo he creado los
siguientes dos registros:
Observa que los dos nuevos registros tienen el array “apodos”, sin embargo, no
tiene los mismos valores, el tercer valor es diferente entre los dos documentos.
Ahora bien, si yo quisiera recuperar los animales que tangan como apodos los
valores “Fido, Cachorro y Huesos” tendría que hacer lo siguiente:
Como resultado, solo nos trae un registro de los dos, pues solo uno tiene los tres
valores indicamos en el operador $all.
Vamos a dejar hasta aquí las operaciones que nos da MongoDB, pues son las
más importantes que necesitaremos para el desarrollo de nuestra API. Si quieres
conocer el listado completo de operadores que soporta MongDB, te invito a que
te des una vuelta por la documentación oficial:
Página | 418
Project
La sección Project se utiliza para determinar los campos que debe de regresar
nuestra consulta, algo muy parecido cuando hacemos un “SELECT campo1,
campo2” a la base de datos. Esta sección es especialmente útil debido a que ayuda
a reducir en gran medida la información que retorna la base de datos, ahorrando
una gran cantidad de transferencia de datos y por lo tanto un aumento en el
performance.
Debido a que el viaje por la red es uno de los principales factores de degradación
de performance, es especialmente importante cuidar este aspecto. En MongoDB,
es muy fácil determinar los campos que queremos y no queremos en la
respuesta, pues tan solo falta listas los campos en formato {clave: val, calve:
val, …. } donde la clave es el nombre del campo en el documento y el val solo
puede tener 1 o 0, donde 1 indica que si lo queremos y 0 que no lo queremos.
Veamos un ejemplo para hacer esto más claro, imaginemos que queremos
recuperar el nombre y la edad de todos los animales:
Ahora bien, si queremos que no nos regrese el _id, hay que decirle explícitamente
de la siguiente manera:
419 | Página
Fig. 185 - Eliminando el _id del resultado.
Observemos que hemos puesto el campo _id con valor a cero (0). El cero
MongoDB lo interpreta como que no lo queremos.
Ahora bien, así como le hemos dicho que campos queremos, también le podemos
indicar simplemente cuales no queremos y el resto si los retornará. Esta sintaxis
es muy cómoda cuando queremos todos los campos con excepción de unos
cuantos, lo que nos ahorra tener que escribir todos los campos que tiene. Veamos
un ejemplo de esto, imaginemos que queremos todos los campos, excepto, el
nombre:
Página | 420
El último tipo de selección que veremos será sobre un objeto, para esto
tendremos que hacer algunos cambios. He editado el último registro de la
colección para agregarle una nueva propiedad llamada propiedades la cual es de
tipo Object, y dentro de ella he puesto 3 nuevos campos, alto, largo y peso,
todos estos de tipo Int32.
421 | Página
Sort
La sección sort es utilizada para ordenar los resultados, es sin duda de las
secciones más simples, pues solo hace falta indicar los campos a ordenar y el
sentido de la ordenación (ascendentes = 1 o descendente = -1).
Esta sección se define en el formato {key: {1|-1} }, donde key es el nombre del
campo a ordenar.
Veamos unos ejemplos, imaginemos que queremos ordenar los animales por
nombre de forma ascendente:
Ahora bien, imaginemos que queremos ordenar por nombre ascendente y color
descendente:
Página | 422
Fig. 190 - Ordenamiento ascendente y descendente.
Podemos ver qué cambio el cuarto registro, pues el orden decreciente del color
provoco un reordenamiento.
Tanto el campo Skip y Limit reciben un valor numérico, es decir que no requieren
un objeto JSON {key:value}. Limit permite determinar cuántos registros
máximos debe de regresar la consulta y Skip indica a partir de que elemento de
empieza a regresar los valores.
423 | Página
Fig. 191 - Limitando el número de registros con limit.
Ahora bien, si a esto le sumamos la propiedad skipe para que salte el primero
resultado, esto recorrerá la búsqueda en un registro, de tal forma que el segundo
registro se convertirá en el primero y el segundo que veamos, corresponderá al
3 resultado de la búsqueda:
En este momento tenemos 9 registros, por lo que podríamos paginar por bloques
de 3, realizando la siguiente combinación:
Skip = 0 y Limit = 3
Skip = 3 y Limit = 3
Skip = 6 y Limit = 3
Skip = 9 y Limit = 3 (ya no tendría más resultados)
La técnica es muy simple, primero que nada, ponemos en limit el tamaño de los
bloques que queremos consultar y luego en Skipe debemos ejecutar en múltiplos
del valor colocado en limit, pero siempre empezando en 0.
Página | 424
Fig. 193 - Ejemplo de paginación en bloques de 3.
NodeJS y MongoDB
Tras una larga platica de cómo funciona MongoDB por sí solo, ha llegado el
momento de aprender cómo debemos usarlo en conjunto con NodeJS.
425 | Página
5. var opts = {
6. useNewUrlParser: true,
7. appname: "AnimalMongoDBTest",
8. poolSize: 10,
9. autoIndex: false,
10. bufferMaxEntries: 0,
11. reconnectTries: Number.MAX_VALUE, // Never stop trying to reconnect
12. reconnectInterval: 500,
13. autoReconnect: true,
14. loggerLevel: "error", //error / warn / info / debug
15. keepAlive: 120,
16. validateOptions: true
17. }
18.
19. mongoose.connect(connectString, opts, function(err){
20. if (err) throw err;
21. console.log("==> Conexión establecida con MongoDB");
22. })
Propiedad Descripción
useMongoClient Es una propiedad propia de Mongoose, la cual habilita una
nueva estrategia de conexión a partir de la versión 4.11 de
Mongoose. Sin dar muchas vueltas, solo hay que ponerla
en true.
Página | 426
autoReconnect Propiedad booleana, que le indica a Mongoose si debe de
reconectarse de forma automática en caso de una
desconexión.
427 | Página
Que son los Schemas de Mongoose
Una schema es muy sencillo de definir, tan solo es necesario crearlo mediante el
objeto schema proporcionado por mongoose. Veamos cómo quedaría un schema
para la colección animals un ejemplo:
Una vez que hemos creado la estructura del schema, solo falta registrarlo en
Mongoose. Esto lo hacemos mediante el método mongoose.model, al cual se le
pasen dos parámetros, el primero corresponde al nombre del modelo, que por
default también corresponde con el nombre de la colección, el segundo
parámetro es la definición del schema. Con solo eso, hemos definido un schema.
Página | 428
definir todas las propiedades requeridas separadas por coma (,) como en el
primero ejemplo.
En este nuevo ejemplo, podemos ver que creamos una estructura en blanco y
pasar como segundo argumento la propiedad strict en falso. Con tan solo poner
strinc = false estamos diciendo que el schema puede soportar campos no
definidos en el schema.
Schemas avanzados
Los eschemas no solo sirven para definir la estructura de documento, sino que,
además, permiten crear funciones para validaciones, consultas, propiedades
virtuales, etc, etc. Hablar de toda la capacidad que tiene Mongoose se podría
extender demasiado, por lo que te contare rápidamente algunas de las cosas más
interesantes para que te des una idea de su capacidad.
Statics methods
429 | Página
Las funciones estáticas permiten definir funciones a nivel del schema, que pueden
ser invocadas sin necesidad de tener una instancia del Schema
Para definir una función estática, solo tendremos que crearla sobre el Schema,
el formato es el siguiente: <schema>.statics.<method-name>.
Para ejecutar el método estático, solo tendremos que recuperar el Schema (línea
5) y después ejecutar directamente la función.
Instance methods
Los métodos de instancia funcionan exactamente igual que los métodos estáticos,
pero estos se definen por medio de la propiedad methods.
Query Helpers
Los query helpers son parecidos a los métodos estáticos, con la diferencia que
están diseñador para implementar funcionalidad custom, como buscar por
nombre, obtener todos los documentos de un tipo, etc.
1. animalSchema.query.byName = function(name) {
2. return this.find({ name: new RegExp(name, 'i') });
3. };
4.
Página | 430
5. var Animal = mongoose.model('Animal', animalSchema);
6. Animal.find().byName('fido').exec(function(err, animals) {
7. console.log(animals);
8. });
Virtuals
Los virtuals permiten agregar propiedades virtuales, las cuales son calculadas
por medio de otras propiedades del mismo modelo, como concatenar el nombre
y el apellido para obtener el nombre completo.
1. personSchema.virtual('fullName').get(function () {
2. return this.name.first + ' ' + this.name.last;
3. });
4.
5. console.log(person.fullName);
Plugins
1. //Global plugin
2. mongoose.plugin(<plugin>, <params>)
3.
4. //Schema plugin
5. Animal.plugin(<plugin>, <params>)
Instalar un plugin se realiza mediante el método plugin el cual recibe uno o dos
parámetros, el primero corresponde al plugin como tal y el segundo es un objeto
con configuraciones para el plugin.
Dado que cada plugin tiene una forma distinta de trabajar, no es posible hablar
acerca de su funcionamiento sin tener que hacer referencia un plugin en concreto.
431 | Página
Por lo voy a dejar una liga a todos los plugin que tenemos disponibles y más
adelante veremos cómo implementar un plugin.
Una vez que hemos aprendido como a crear schemas con Mongoose, vamos a
practicar creando los modelos que utilizaremos en el proyecto Mini Twitter, sin
embargo, esta parte ya no será parte de la aplicación, si no del API.
Tweet Scheme
El schema Tweet lo utilizaremos para guardar los Tweets de todos los usuarios.
En la aplicación Mini Twitter, tratamos las respuesta de los Tweet, como un nuevo
Tweet, con la única diferencia que tiene una referencia al Tweet padre, esta
referencia se lleva a cabo mediante el campo tweetParent. Este schema deberá
ser creado en el archivo Tweet.js en el path /api/models.
Página | 432
likeRef: Es un arreglo con el ID de los usuarios que dieron like al Tweet.
Image: Imagen asociada al Tweet, siempre y cuando tenga una imagen.
Replys: Contador de respuestas que tiene el Tweet. Es decir, número de
Tweet hijos.
Para poder tener un contador de los likes que tiene el tweet, creamos un campo
virtual (línea 14), el cual retorna el número de posiciones que tiene el campo
likeRef.
Este schema es ligado a la colección Tweet, como podemos ver en la línea 17.
Finalmente exportamos el schema para poder ser utilizado más adelante en el
API.
Profile Scheme
433 | Página
Los campos que podemos apreciar son:
Debido a que una de las búsquedas más frecuentes, es la búsqueda por nombre
de usuario (userName), hemos creado un método helper (línea 23), el cual nos
retornará un usuario que corresponda con el nombre de usuario solicitado.
Página | 434
Los schemas no solo sirven para definir la estructura de los documentos, sino
que, además, proporcionan una serie de operaciones para insertar, actualizar,
borrar y consultar los documentos.
Save
Save
Create
435 | Página
Cabe mencionar, que los dos ejemplos que mostramos a continuación, dan el
mismo resultado.
Find
Método find
Método findOne
El método findOne funciona exactamente igual que find, con la única diferencia
de que este solo regresa un objeto, por lo que, si más de un documento
concuerdan con el query, entonces será retornado el primer documento
encontrado. Por otra parte, si no se encuentra ningún documento, entonces se
regresa null.
Los operadores skip, limit y sort, solo pueden ser utilizados en conjunto de la
operación find. La forma de utilizarlos es la siguiente:
1. find({_id: tweet.id},{message:1,image:1})
2. .sort( { date: 1 } )
3. .limit( 10 )
4. .skip( 5 )
5. .limit(5)
6. .exec(callback(err, returns){
Página | 436
7. //Any action
8. })
NOTA: Para los query podemos utilizar todos los operadores lógicos, de
elementos y comparación que vimos al inicio de este capítulo.
Update
Método update
El método estático update es una instrucción que nos permite actualizar solo el
primer documento que concuerden con un filtro sin retornarlo. Como retorno
obtendremos el número de registros seleccionados y el número de documentos
actualizados.
1. Profile.update(
2. {userName: 'oscar'},
3. {name: 'Oscar Blancarte, description: 'Nuevo en Twitter'}
4. function( err, response){
5. //Any action
6. })
1. {
2. n: 1,
3. nModified: 1,
4. opTime:
5. { ts: Timestamp { _bsontype: 'Timestamp', low_: 2, high_: 1510367840 }, t: 1 },
6. electionId: 7fffffff0000000000000001,
7. ok: 1
8. }
437 | Página
n: número de documentos seleccionados por el query.
nModified: Número de documentos realmente actualizados
ok: indica si la operación termino correctamente.
Por lo general, siempre nos fiamos en el campo nModified para asegurar que la
operación actualizo al menos un documento.
Método updateMany
1. Profile.updateMany(
2. {userName: 'oscar'},
3. {name: 'Oscar Blancarte, description: 'Nuevo en Twitter'}
4. function( err, response){
5. //Any action
6. })
Método save
Cómo podemos ver en este ejemplo, buscamos a un usuario (línea 1), luego
actualizamos la descripción (línea 2) directamente sobre el objeto retornado.
Finalmente, guardamos los cambios mediante el método de instancia save.
Remove
Página | 438
Remove es la operación que permite borrar uno más documentos de la base de
datos. Esta operación elimina todos los documentos que coinciden con una
operación. El formato para invocar es el siguiente:
En el ejemplo pasado estamos eliminando todos los usuarios que tenga como
nombre de usuario, oscar.
Population
Population es una de las operaciones más poderosas que tiene Mongoose, pues
permite simular la instrucción JOIN de SQL.
1. Tweet.find({})
2. .populate("_creator")
3. .exec(function(err, tweets){
4. //Any action
5. })
439 | Página
El siguiente documento corresponde a un tweet, tal cual se guarda en la colección
de la base de datos. Ahora bien, observa el campo _creator, el cual tiene como
valor el ObjectId del usuario que creo el Tweet.
1. {
2. "_id": "5a0657ad3ccd98529d83a9b9",
3. "_creator": ObjectId('5a05286db5371dffe40bafae'),
4. "date": "2017-11-11T01:51:41.421Z",
5. "message": "test",
6. "likeCounter": 0,
7. "replys": 0,
8. "image": null
9. }
1. {
2. "_id": "5a0657ad3ccd98529d83a9b9",
3. "_creator": {
4. "_id": "5a05286db5371dffe40bafae",
5. "name": "Jaime",
6. "userName": "jaime",
7. "avatar": "<img base64>"
8. },
9. "date": "2017-11-11T01:51:41.421Z",
10. "message": "test",
11. "likeCounter": 0,
12. "replys": 0,
13. "image": null
14. }
Página | 440
Resumen
441 | Página
Desarrollo de API REST con
NodeJS
Capítulo 15
La arquitectura REST se rige por una serie de principios que son clave para el
entendimiento entre el cliente y el servidor. Estos principios son los siguientes:
Página | 442
Toda la comunicación por medio de HTTP deberá utilizar los verbos o métodos
definidos por el protocolo, por ejemplo, GET, POST, PUT, DELETE, etc. Ya
hablamos acerca de los verbos en el pasado.
Un mismo recurso puede tener múltiples representaciones, lo que quiere
decir que, es posible enviar la misma información en diferente formato, por
ejemplo, es posible enviar un documento en formato JSON o en XML o una
imagen en formato JPEG o PNG.
Toda la comunicación que se realiza por medio de HTTP es sin estado
(Stateless), lo que significa que cada petición es tratada de forma
independiente y todas las ejecuciones con los mismos parámetros de entrada
deberá arrojar el mismo resultado.
Por otra parte, tenemos RESTful, el cual son los servicios web que se crean
siguiente la arquitectura REST y los principios fundamentales.
REST vs SOA
443 | Página
Para responder esta pregunta, es necesario entender que es SOA. SOA es un tipo
de arquitectura de software que promueve el desarrollo de servicios como la
unidad más pequeña de un software, y que a partir de los servicios es posible
crear cosas más complejas.
Ahora bien, tanto en SOA como en REST es posible crear servicios, pero existe
una diferencia importante, REST se limita a la comunicación mediante el
protocolo HTTP, mientras que SOA es una arquitectura de más alto nivel, en
donde no se habla de una tecnología específica para el transporte de mensajes,
como lo es HTTP. Esto quiere decir que con SOA es posible tener servicios con
HTTP, pero también es posible utilizar otras tecnologías como Colas de mensajes,
correo electrónico, FTP, TCP, etc.
Página | 444
Lo primero que haremos será analizar el archivo server.js tal cual lo tenemos
hasta el momento. El archivo se debería ver de la siguiente manera:
En las líneas 19 a 21 creamos un router para atender todas las peticiones (/*)
sin importar el path, el cual regresará el archivo index.html. Con este router nos
aseguramos que la aplicación pueda responder en cualquier URL y no solo en la
raíz del dominio.
445 | Página
servidor se inicia, en el cual se puede hacer cualquier cosa, aunque por lo general
se escribe en el log para informar al usuario que el servicio ya está activo.
Página | 446
41. res.sendFile(path.join(__dirname, 'index.html'))
42. });
43.
44. app.listen(8080, function () {
45. console.log('Example app listening on port 8080!')
46. });
En este punto solo faltaría guardar los cambios para que la aplicación se actualice,
de tal forma que, deberemos ver el siguiente resultado en la consola:
Una de las cuestiones más importantes a la hora de publicar un API por internet,
es definir la URL por medio de la cual el API atenderá las solicitudes. Dicha URL
debe de ser significativa, de tal forma que con tan solo ver la URL podremos
identificar que API es y a qué ambiente pertenece (desarrollo, pruebas,
producción).
Por lo general, las API son publicadas sobre el mismo domino de la aplicación
principal, y existen dos formas de hacer esto, la primera y más simple es que
reservemos un path del dominio para atender solicitudes del API, por ejemplo:
http://test.com/api/*. Esto quiere decir que cualquier cosa que llegue con el
path /api, será atendida por el API. Esta estrategia puede resultar atractiva, sin
embargo, no es lo más recomendable, por varias razones:
447 | Página
la primer y más importante, es que tanto la aplicación como el API
compartirían la misma IP, pues a nivel DNS, es el mismo dominio, lo que
complica la gestión.
En segundo lugar, tenemos que reservar paths para el API, lo que nos
quita toda la gama de URL para la aplicación.
En tercer lugar, el ser una subsección del dominio principal, Google o los
demás buscadores podrían indexar los servicios o las páginas de
documentación, como parte de la aplicación, lo cual no siempre es
deseado.
Otra de las ventajas, es que ya no es necesario reservar ninguna URL para el API
y como es un subdominio es posible instalarle el mismo certificado de seguridad
del dominio principal.
Exponer el API como un subdominio es una mejor estrategia, y una muestra clara
de esto, son el API de:
Paypal: https://api.paypal.com/
Uber: https://api.uber.com
Facebook: https://graph.facebook.com
Twitter: https://api.twitter.com
Por los motivos que hemos explicado, en nuestro proyecto Mini Twitter, hemos
decidido crear un subdominio para nuestra API, de tal forma que la URL quedaría
de la siguiente manera http://api.<domain>. Ahora bien, Dado que estamos
desarrollando de forma local, <domain> se remplaza por localhost, de tal forma
que el API lo configuraremos para trabar en http://api.localhost:8080.
Mediante un Virtual Host es posible distinguir entre las solicitudes que entran al
dominio principal (localhost) y de los que entra a un subdominio (api.localhost)
y en NodeJS es extremadamente fácil realizar esta configuración, pero antes de
eso, vamos a necesitar instalar el módulo vhost mediante el siguiente comando:
Página | 448
Y vamos a instalar un motor de plantillas de NodeJS llamado Pug, que nos
ayudará a crear páginas web más fácil, del cual hablaremos más adelante,
cuando empezásemos a documentar el API.
Una vez que tenemos las dependencias, vamos a necesitar crear el archivo api.js
en el path /api, en el cual vamos procesar todas las solicitudes que lleguen al
API. El archivo se deberá ver de la siguiente manera:
Por otra parte, tenemos un router que escucha en cualquier URL del subdominio.
Este router es importante, porque se ejecutará cuando ninguna regla de route se
cumpla y de esta forma, es posible enviarle un mensaje de error al usuario.
1. html
2. head
3. link(href='https://fonts.googleapis.com/css?family=Roboto', rel='stylesheet')
4. link(
5. rel='stylesheet'
6. href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css'
7. integrity='sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u'
8. crossorigin='anonymous'
9. )
10. link(rel='stylesheet', href='/public/apidoc/api-styles.css')
11. body
12. .api-alert
449 | Página
13. p.title Mini Twitter API REST
14. p.body
15. | Esta API es provista como parte de la aplicación Mini Twitter,
16. | la cual es parte del libro
17. a(href='#')
18. strong "Desarrollo de aplicaciones reactivas con React, NodeJS y MongoDB"
19. | , por lo que su uso es únicamente con fines educativos y por
20. | ningún motivo deberá ser empleada para aplicaciones productivas.
21. .footer
22. button.btn.btn-warning(data-toggle='modal', data-target='#myModal')
23. | Terminos de uso
24. a.btn.btn-primary(href='/catalog') Ver documentación
25.
26. #myModal.modal.fade(
27. tabindex='-1', role='dialog', aria-labelledby='myModalLabel',
28. aria-hidden='true')
29. .modal-dialog
30. .modal-content
31. .modal-header
32. button.close(type='button', data-dismiss='modal',
33. aria-hidden='true') ×
34. h4#myModalLabel.modal-title Terminos de uso
35. .modal-body
36. p El API de Mini Twitter es provista por Oscar Blancarte, autor del libro
37. strong "Desarrollo de aplicaciones reactivas con React, NodeJs y MongoDB"
38. | con fines exclusivamente educativos.
39. p Esta API es provista
40. strong "tal cual"
41. | esta, y el autor se deslizanda de cualquier problema o falla
42. | resultante de su uso. En ningún momento el autor será responsable
43. | por ningún daño directo o indirecto por la pérdida o publicación
44. | de información sensible.
45. strong El usuario es el único responsable por el uso y la información
46. | que este pública.
47. .modal-footer
48. button.btn.btn-primary(type='button', data-dismiss='modal') Cerrar
49.
50.
51. script(src='https://code.jquery.com/jquery.js')
52. script(src='//netdna.bootstrapcdn.com/bootstrap/3.0.3/js/bootstrap.min.js')
1. body {
2. background-color: #fafafa;
3. background: #1E5372;
4. background: -webkit-linear-gradient(left, #1C3744 , #1E5372); /
5. background: -o-linear-gradient(right, #1C3744, #1E5372);
6. background: -moz-linear-gradient(right, #1C3744, #1E5372);
7. background: linear-gradient(to right, #1C3744 , #1E5372);
8. }
9.
10. *{
11. font-family: 'Roboto', sans-serif;
12. color: #333;
13. }
14.
15. .hljs {
Página | 450
16. display: block;
17. overflow-x: auto;
18. padding: 0.5em;
19. color: #abb2bf;
20. background: #282c34;
21. }
22.
23. .hljs-comment,
24. .hljs-quote {
25. color: #5c6370;
26. font-style: italic;
27. }
28.
29. .hljs-doctag,
30. .hljs-keyword,
31. .hljs-formula {
32. color: #c678dd;
33. }
34.
35. .hljs-section,
36. .hljs-name,
37. .hljs-selector-tag,
38. .hljs-deletion,
39. .hljs-subst {
40. color: #e06c75;
41. }
42.
43. .hljs-literal {
44. color: #56b6c2;
45. }
46.
47. .hljs-string,
48. .hljs-regexp,
49. .hljs-addition,
50. .hljs-attribute,
51. .hljs-meta-string {
52. color: #98c379;
53. }
54.
55. .hljs-built_in,
56. .hljs-class .hljs-title {
57. color: #e6c07b;
58. }
59.
60. .hljs-attr,
61. .hljs-variable,
62. .hljs-template-variable,
63. .hljs-type,
64. .hljs-selector-class,
65. .hljs-selector-attr,
66. .hljs-selector-pseudo,
67. .hljs-number {
68. color: #d19a66;
69. }
70.
71. .hljs-symbol,
72. .hljs-bullet,
73. .hljs-link,
74. .hljs-meta,
75. .hljs-selector-id,
76. .hljs-title {
77. color: #61aeee;
78. }
79.
80. .hljs-emphasis {
81. font-style: italic;
451 | Página
82. }
83.
84. .hljs-strong {
85. font-weight: bold;
86. }
87.
88. .hljs-link {
89. text-decoration: underline;
90. }
91.
92. .badge{
93. background-color: #E74C3C;
94. }
95.
96. api-alert p{
97. margin: 0px;
98. }
99.
100. .api-alert{
101. margin: auto;
102. width: 40%;
103. margin-top: 50px;
104. background-color: #fafafa;
105. padding: 20px;
106. border-top: 5px solid #FEB506;
107. border-radius: 5px;
108. }
109.
110. .method-templete{
111. margin: auto;
112. width: 80%;
113. margin-top: 50px;
114. margin-bottom: 50px;
115. background-color: #fafafa;
116. padding: 20px;
117. border-top: 5px solid #FEB506;
118. border-radius: 5px;
119. }
120.
121. .secure-icon{
122. font-size: 50px;
123. float: right;
124. }
125.
126. .api-alert .title{
127. font-size: 28px;
128. text-align: center;
129. margin-bottom: 20px;
130. }
131.
132. .api-alert .body{
133. font-size: 20px;
134. text-align: justify;
135. }
136.
137. .api-alert .body strong{
138. font-style: italic;
139. color: #3498DB;
140. }
141.
142. .api-alert .footer{
143. margin-top: 20px;
144. text-align: center;
145. }
146.
147. .api-alert .footer a{
Página | 452
148. margin-left: 20px;
149. }
150.
151. .label{
152. margin-left: 20px;
153. font-size: 12px;
154. }
Una vez hecho esto, tendremos que regresar al archivo server.js y agregar solo
las líneas marcadas:
Lo primero que haremos será importar el módulo vhost (línea 10) y el archivo
api.js (línea 11) que acabos de crear.
El segundo paso será crear el Virtual Host mediante la línea 30. Lo que estamos
haciendo en esta línea es crear un Middleware con vhost, el cual tomará todas
peticiones que lleguen al path api.* y las envíe al archivo api.js, más
precisamente, al Router definido dentro.
453 | Página
Con estos simples pasos, hemos terminado de crear nuestro Virtual Host, y no
solo eso, sino que, además, se ve muy bien. Para probarlo, entra a la URL
http://api.localhost:8080 en tu navegador para que veas los resultados:
Configurar subdominio
Recuerda configurar tu sistema operativo para que
redireccione las peticiones del subdominio
api.localhost a la ip 127.0.0.1, de lo contrario no
podrá ser accedido.
Página | 454
Para solucionar este problema, es necesario agregar un header a nuestra
respuesta del servidor, permitiendo que el navegador aprueba la ejecución de
recursos externos, este header es el Access-Control-Allow-Origin. Por suerte,
hay un módulo que nos puede ayudar con eso, de tal forma que agregue por
nosotros el header en todas las respuestas. El módulo es cors y lo podemos
instalar mediante el comando:
Con este simple paso, el API ya podrá ser invocado desde la aplicación.
Diseñar un API REST no se trata solo de crear servicios que cumplan su función,
sino que, además, las URL de los servicios deben de darla al usuario una idea
bastante clara de lo que hace un servicio. Un servicio con una URL bien diseña,
puede decirle al usuario que hace sin necesidad de leer la documentación.
Utilizar correctamente los métodos es sin duda, una de las principales cosas que
debemos de respetar, pues una mala implementación puede llegar a ser
sumamente confuso. Recordemos que los métodos más utilizados son:
455 | Página
GET: Solo lectura, y se utiliza para consultar información
POST: Se utiliza para creación de un nuevo registro.
PUT: Remplazar actualizar/remplazar un registro
DELETE: Se utiliza para eliminar un registro.
Una URL por sí sola, no nos dice que operación va a realizar sobre un registro,
por ejemplo, veamos la siguiente URL:
/user/juan
Uno de los grandes errores al definir las URL de nuestros servicios, es no definir
URL params, y en su lugar, esperar que los parámetros requeridos vengan como
parte del payload. En la práctica, recibirlo en el payload o como URL param, dará
el mismo resultado, sin embargo, para el usuario siempre será más claro definir
los parámetros como parte de la URL.
Camel Case
Página | 456
Por ejemplo, imagina que tienes un servicio de búsqueda de órdenes de ventas
y compras, un servicio con el path /ordes/:id sería un poco confuso, pues no
sabrías que tipo de orden estas buscando, en este caso podrías agregar
/salesOrders y /purchaseOrders.
Códigos de respuesta
Todos los servicios REST por el simple hecho de trabajar sobre HTTP, retornan
un código de respuesta, este código de respuesta es un indicativo para el cliente
sobre lo que paso durante la ejecución. Los códigos de respuesta más utilizados
son:
Debido a que ya vamos a empezar a trabajar con nuestra propia API REST, será
necesario redirigir las llamadas del proyecto Mini Twitter a nuestra nueva API.
Para lograr eso, tendremos que entrar al archivo config.js que se encuentra en
la raíz del proyecto y cambiar el host y el puerto para apuntar a nuestra API.
1. module.exports = {
2. debugMode: true,
3. server: {
4. port: 8080,
5. host: "http://api.localhost"
6. },
7. tweets: {
8. maxTweetSize: 140
9. },
10. mongodb: {
11. development: {
12. connectionString: "<dev conneción string>"
13. },
14. production: {
15. connectionString: "<prod connection string>"
16. }
17. }
18. }
457 | Página
En el momento en que realicemos este cambio, la aplicación Mini Twitter dejará
de funcionar por completo, pues no estará disponible ninguno de los servicios
necesarios para funcionar. Por ese motivo, vamos a ir desarrollando los servicios
más importantes para el correcto funcionamiento y nos iremos adentrando a los
menos necesario al final.
En esta sección pasaremos de lleno a implementar los servicios REST que le dan
vida a la aplicación Mini Twitter.
Servicio - usernameValidate
1. {
2. "ok": true,
3. "message": "Usuario disponible"
4. }
Una vez que hemos analizado la ficha, ya sabemos los detalles básicos para su
implementación, como la URL, el método y el formato del request/response.
Lo primero que haremos será crear la carpeta controllers, la cual deberá estar
dentro del path /api, en esta nueva carpeta, vamos a crear el archivo
UserController.js. Dentro de este archivo vamos a crear todos los servicios que
involucran a los usuarios.
Página | 458
Nuevo concepto: Controller
Los controllers son clases diseñadas para encapsular
todas las operaciones que afectan al modelo de datos,
recibiendo las órdenes del usuario, procesando la
solicitud y regresando un resultado.
Dado que vamos a trabajar con los modelos de Mongoose, tendremos que
importar el modelo Profile en la línea 1.
Finalmente, exportamos la función (línea 21) para que pueda ser referencia
desde fuera.
459 | Página
El paso final, será agregar la regla de routeo /usernameValidate a nuestra API,
de tal forma que, cuando llegue una solicitud, este la delegue a nuestro
controller. Para lograr esto, tendremos que editar nuestro archivo api.js para
agregar las líneas marcadas:
Página | 460
Servicio - Signup
1. {
2. "name": "Juan Perez ",
3. "username": "juan",
4. "password": "1234"
5. }
Response
Ok: booleana que indica si la operación fue
exitosa o no.
Profile: contiene los datos del usuario creado,
entre los que destacan
o _id: ID único del usuario
o date: fecha de creación
1. {
2. "ok": true,
3. "body": {
4. "profile": {
5. "__v": 0,
6. "name": "Juan Perez",
7. "userName": "juan",
8. "password": "",
9. "_id": "5a1f40142aab1e2318d65e98",
10. "date": "2017-11-29T23:17:40.508Z",
11. "followersRef": [],
12. "followingRef": [],
13. "tweetCount": 0,
14. "banner": null,
15. "avatar": null,
16. "description": "Nuevo en Twitter"
17. }
18. }
19. }
461 | Página
Una vez analizada la ficha, iniciaremos agregando la función signup dentro del
archivo UserController.js el cual procesará las solicitudes para el alta de
usuarios, la función se verá de la siguiente manera:
Una vez creado el objeto Profile, solo resta guardarlo en la base de datos
mediante la función save (línea 9) que proporciona el Schema. Si todo sale bien,
el perfil retornado por Mongoose es retornado con el ID generado. Por otra parte,
si hay algún error, lo regresamos al cliente.
Página | 462
Cabe mencionar que en la línea 19 validamos si hay un error de userName
duplicado, esto con ayuda del plugin mongoose-unique-validator. Los errores los
encontramos en err.errors.{field}, donde field es el campo que tiene algún
error.
Para concluir con este archivo, solo faltaría exportar la nueva función al final del
archivo.
Para validar que todo funciona correctamente, podemos intentar crear un nuevo
usuario desde la página de signup y comprobar el registro está en MongoDB.
Una de las grandes características de un API, es que debe de ser seguro, por eso,
implementar un sistema de autenticación para el API es indiscutible. La
importancia de implementar seguridad del lado del servidor, es impedir que
usuarios no autenticados puedan acceder a recursos restringidos.
De todas las opciones que existen, el uso de token es una de las más populares,
pues permite la autenticación sin necesidad de tener que enviar nuestras
credenciales cada vez que necesitamos consumir el API. Por otra parte, el Token
permite guardar datos adicionales, los cuales pueden ser descifrado y
aprovechados del lado del API, como es el caso de la fecha de vigencia, la cual
permite invalidar un Token que tiene cierto tiempo de haber sido creado.
En el artículo Autenticación con JSON Web Token explico con mucho más detalle
acerca de este tema, por si quiere profundizar en el aprendizaje de esta fantástica
herramienta.
463 | Página
Antes de iniciar con la implementación de JWT tenemos que entender que existen
servicios que no requieren autenticación y otros que sí, por eso motivo, tenemos
que tener una lógica para identificar cuáles serán protegidos y cuáles no. Para
facilitar las cosas, nos vamos a apoyarnos en la URL para realizar esa
diferenciación, de tal forma que todos los servicios que inicien en /secure/* será
sujetos a autenticación, mientras que el resto no.
A pesar de que solo las URL /secure/* son protegidas, siempre es bueno solicitar
el token (si lo tiene) para saber quién nos está invocando, por ese motivo,
empezaremos desarrollando un Middleware que recupere el header
“authorization” que corresponde al token, lo descifre y lo agregue a nuestro
objeto request, con la intención que esté disponible para el resto de los servicios.
Página | 464
En este punto, será necesario instalar el módulo jsonwebtoken mediante el
siguiente comando:
1. module.exports = {
2. debugMode: true,
3. server: {
4. port: 8080,
5. host: "http://api.localhost"
6. },
7. tweets: {
8. maxTweetSize: 140
9. },
10. mongodb: {
11. development: {
12. connectionString: "<Connection string>"
13. },
14. production: {
15. connectionString: "<Connection string>"
16. }
17. },
18. jwt: {
19. secret: "#$%EGt2eT##$EG%Y$Y&U&/IETRH45W$%whth$Y$%YGRT"
20. }
21. }
El siguiente paso será denegar el acceso a los servicios restringidos, por lo cual,
agregaremos el siguiente middleware al archivo api.js justo debajo del
middleware que acabamos de agregar:
Este nuevo middleware solo escucha las llamadas en el path /secure, lo que nos
permite realizar acciones solo para los servicios protegidos. Este middleware
valida si la propiedad req.user es null, si es null significa que no se presentó un
token o el que se presento es inválido. En tal caso, un código 401 es retornado
junto con la leyenda “Token inválido”. Por otra parte, si el token es correcto,
entonces, simplemente permitimos que continúe la ejecución con la llamada a
next.
En este momento, solo nos falta agregar la lógica para crear los Tokens, pero
esto lo veremos en el servicio de login.
465 | Página
Servicio - Login
El servicio login nos permite autenticarnos ante el API, para lo cual es necesario
mandar las credenciales user/password, y como respuesta, nos retornará el
Token generado.
Nombre Autenticación
URL /login
Método POST
Request
name: nombre de la persona que crea la
cuenta.
username: nombre de usuario.
Ejemplo:
6. {
7. "username": "juan",
8. "password": "1234"
9. }
Response
Ok: valor booleano que indica si la operación
fue exitosa o no.
Profile: contiene los datos del usuario creado.
Token: Token generado para autenticarse en el
API.
20. {
21. "ok": true,
22. "body": {
23. "profile": {
24. "__v": 0,
25. "name": "Juan Perez",
26. "userName": "juan",
27. "password": "",
28. "_id": "5a1f40142aab1e2318d65e98",
29. "date": "2017-11-29T23:17:40.508Z",
30. "followersRef": [],
31. "followingRef": [],
32. "tweetCount": 0,
33. "banner": null,
34. "avatar": null,
35. "description": "Nuevo en Twitter"
36. }
37. },
38. "token": "<Token>"
39. }
Página | 466
1. var jwt = require('jsonwebtoken')
2. var configuration = require('../../config')
3.
4. function generateToken(user) {
5. var u = {
6. username: user.username,
7. id: user.id
8. }
9. return token = jwt.sign(u, configuration.jwt.secret, {
10. expiresIn: 60 * 60 * 24 // expires in 24 hours
11. })
12. }
13.
14. module.exports = {
15. generateToken
16. }
467 | Página
33. avatar: profile.avatar || '/public/resources/avatars/0.png',
34. banner: profile.banner || './public/resources/banners/4.png',
35. tweetCount: profile.tweetCount,
36. following: profile.following,
37. followers: profile.followers
38. },
39. token: token
40. })
41. });
42. }).catch(err => {
43. res.send({
44. ok: false,
45. message: "Error al validar el usuario"
46. })
47. })
48. }
Imagen por default para el Imagen por default para el banner, la cual
avatar, la cual deberemos deberá estar en la carpeta
guardar en el path /public/resources/banners/ con el nombre
/public/resources/avatars 4.png
/ con el nombre 0.png
Para comprobar que las hemos creado correctamente, deberemos poder ver las
imágenes en las siguientes URL, de lo contrario algo hemos hecho mal:
http://localhost:8080/public/resources/avatars/0.png
http://localhost:8080/public/resources/banners/4.png
Página | 468
El último paso es agregar el router al archivo api.js, el cual se verá de la siguiente
manera:
Servicio - Relogin
El servicio relogin es muy parecido al servicio de login, pues también sirve para
autenticar al usuario, sin embargo, tiene una pequeña diferencia, este servicio,
se utiliza para autenticar a los usuarios que ya tiene un token.
40. {
41. "ok": true,
42. "body": {
43. "profile": {
44. "__v": 0,
45. "name": "Juan Perez",
46. "userName": "juan",
47. "password": "",
48. "_id": "5a1f40142aab1e2318d65e98",
469 | Página
49. "date": "2017-11-29T23:17:40.508Z",
50. "followersRef": [],
51. "followingRef": [],
52. "tweetCount": 0,
53. "banner": null,
54. "avatar": null,
55. "description": "Nuevo en Twitter"
56. }
57. },
58. "token": "<Token>"
59. }
Una de las cosas que llama la atención de este servicio, es que se ejecuta por el
método GET y que no tiene request, esto se debe a que solo requiere del token
en el header “authorization” para validar al usuario. Como respuesta regresa lo
mismo del servicio login pero nos retorna un token actualizado con nueva fecha
de vencimiento.
Página | 470
Dado que este servicio está expuesto en /secure/relogin, quiere decir que antes
de llegar a este servicio, deberá pasar por los middlewares de autenticación que
definimos en api.js, lo que nos garantiza que, si llega hasta aquí, es porque es
un usuario autenticado.
El siguiente paso es generarle un nuevo token con los datos del token viejo
(líneas 2 a 6). Seguido, buscamos al usuario por medio del ID y retornar los datos
del perfil junto con el nuevo token.
Restaría exportar la función al final del archivo para poder accederlo de forma
externa.
Finalmente, solo nos falta agregar el router en el archivo api.js, el cual quedaría
de la siguiente manera:
Este servicio es el que nos permite recuperar los últimos 10 tweets publicados
por todos los usuarios. Normalmente una red social utiliza IA para determinar los
471 | Página
tweets que deberás ver en tu home, sin embargo, nosotros no tenemos esas
capacidades, por lo que optamos por los 10 últimos tweets.
1. {
2. "ok": true,
3. "body": [
4. {
5. "_id": "5a0657ad3ccd98529d83a9b9",
6. "_creator": {
7. "_id": "5a05286db5371dffe40bafae",
8. "name": "Juan",
9. "userName": "Juan",
10. "avatar": "<Base64 img>"
11. },
12. "date": "2017-11-11T01:51:41.421Z",
13. "message": "test",
14. "likeCounter": 0,
15. "replys": 0,
16. "image": null
17. },
18. ...
19. ]
20. }
Como podemos observar, este servicio regresa un array dentro del body, donde
cada posición corresponde a un Tweet.
Página | 472
Todos los servicios relacionados con Tweets, los vamos a crear en otro controller,
por lo cual, crearemos el archivo TweetController.js en el path
/api/controllers, el cual se verá de la siguiente manera:
473 | Página
Lo siguiente es buscar todos los Tweet donde el campo tweetParent sea null
(línea 8), para asegurar de no recuperar Tweet que correspondan a respuestas.
Lo siguiente es hacer un populate (join) con los Perfiles (línea 9), con la intención
de remplazar el ID del usuario por el objeto en sí, con todos sus datos, además,
le mandamos banner:0 para indicarle que NO queremos el banner. Lo siguiente
es ordenar (línea 10) los Tweets de forma descendiente y solicitamos los primeros
10 tweet (línea 11). Terminamos ejecutando la consulta con la instrucción exec
(línea 12).
Para concluir este servicio, faltaría realizar dos acciones en el archivo api.js, la
primera es realizar el import a TweetController.js y agregar el routeo siguiente:
Con estos últimos cambios, ya podremos ver los tweets en nuestro proyecto:
Página | 474
El siguiente servicio tiene como propósito mostrar al usuario una lista de 3
usuarios sugeridos para que siga. Ahora bien, Las redes sociales actuales utilizan
algoritmos con Inteligencia Artificial para determinar que usuarios son buenos
prospectos para ser sugeridos, capacidad que desde luego no tenemos en este
mini proyecto, por lo que nos limitaremos a mostrar los últimos 3 usuario
registros en el proyecto.
60. {
61. "ok": true,
62. "body": [
63. {
64. "_id": "5a204cdca0738b612c9c9f5f",
65. "name": "marco",
66. "description": "Nuevo en Twitter",
67. "userName": "marco",
68. "avatar": "<base64 img>",
69. "banner": "<base64 img>",
70. "tweetCount": 0,
71. "following": 0,
72. "followers": 0
73. },
74. ...
75. ]
76. }
77.
475 | Página
15. avatar: x.avatar || '/public/resources/avatars/0.png',
16. banner: x.banner || '/public/resources/banners/4.png',
17. tweetCount: x.tweetCount,
18. following: x.following,
19. followers: x.followers
20. }
21. })
22. res.send({
23. ok: true,
24. body: response
25. })
26. })
27. .catch(err => {
28. res.send({
29. ok: false,
30. message: err.message,
31. error: err
32. })
33. })
34. }
Este servicio es bastante simple, pues solo realiza la búsqueda de Perfiles (línea
4), los ordena por fecha de creación en forma descendente (línea 5) y luego
recupera los 3 primeros registros (línea 6).
Tras agregar estos cambios, ya podremos ver los perfiles sugeridos en la página
de inicio:
Página | 476
Fig. 203 - Probando los usuarios sugeridos.
477 | Página
Response
Ok: valor booleano que indica si la operación
fue exitosa o no.
Body: contiene todos los datos del perfil
1. _id: identificador único del usuario.
2. Name: Nombre completo del usuario
3. Description: descripción acerca del
usuario.
4. userName: nombre de usuario.
5. Avatar: Foto de perfil
6. Banner: Imagen del banner
7. tweetCount: Conteo de tweet
publicados
8. Followings: número de personas que
sigue
9. Followers: número de seguidores.
10. Follow: indica si el consumir esta
siguiente al usuario.
78. {
79. "ok": true,
80. "body": {
81. "_id": "5a012b1486b5c864a4fe6223",
82. "name": "Oscar Blancarte",
83. "description": "Nuevo en Twitter",
84. "userName": "oscar",
85. "avatar": "<base64 img>",
86. "banner": "<base64 img>",
87. "tweetCount": 76,
88. "following": 1,
89. "followers": 2,
90. "follow": false
91. }
92. }
93.
Página | 478
13. res.send({
14. ok: false,
15. message: "Usuario no existe"
16. })
17. return
18. }
19.
20. var token = req.headers['authorization'] || ''
21. token = token.replace('Bearer ', '')
22. jwt.verify(token, configuration.jwt.secret, function(err, userToken) {
23. let follow = false
24.
25. if (!err) {
26. follow = user.followersRef.find(
27. x => x.toString() === userToken.id.toString()) != null
28. }
29. res.send({
30. ok:true,
31. body: {
32. _id: user._id,
33. name: user.name,
34. description: user.description,
35. userName: user.userName,
36. avatar: user.avatar || '/public/resources/avatars/0.png',
37. banner: user.banner || '/public/resources/banners/4.png',
38. tweetCount: user.tweetCount,
39. following: user.following,
40. followers: user.followers,
41. follow: follow
42. }
43. })
44. })
45. }).catch(err => {
46. res.send({
47. ok: false,
48. message: err.message || "Error al obtener los datos del usuario",
49. error: err
50. })
51. })
52. }
Esta función requiere que se le envíe como URL param el nombre del usuario al
cual se va a consultar, de lo contrario, un error será retornado (línea 4).
479 | Página
El último paso será agregar el router correspondiente al archivo api.js:
Una vez que terminamos los cambios, es posible dirigirse a la sección del perfil
de nuestro usuario o el de cualquier otro:
Si nos dirigimos en este momento a la sección del perfil de usuario, verá que se
están mostrando los tweets de todos los usuarios, esto es posible debido a que
este servicio y el de los 10 últimos tweets (/tweets) son compatibles en la URL.
Pero una vez que implementemos este, Express podrá determinar que este es el
correcto para el path /tweets/:username.
Página | 480
Veamos la ficha del servicio:
21. {
22. "ok": true,
23. "body": [
24. {
25. "_id": "5a0657ad3ccd98529d83a9b9",
26. "_creator": {
27. "_id": "5a05286db5371dffe40bafae",
28. "name": "Juan",
29. "userName": "Juan",
30. "avatar": "<Base64 img>"
31. },
32. "date": "2017-11-11T01:51:41.421Z",
33. "message": "test",
34. "likeCounter": 0,
35. "replys": 0,
36. "image": null
37. },
38. ...
39. ]
94. }
481 | Página
3.
4. Profile.findOne({userName: username}, function(err, user){
5. if(err){
6. res.send({
7. ok: false,
8. message: "Error al consultar los tweets",
9. error: err
10. })
11. return
12. }
13.
14. if(user == null){
15. res.send({
16. ok: false,
17. message: "No existe el usuarios"
18. })
19. return
20. }
21.
22. Tweet.find({_creator: user._id,tweetParent : null})
23. .populate("_creator")
24. .sort('-date')
25. .exec(function(err, tweets){
26. if(err){
27. res.send({
28. ok: false,
29. message: "Error al cargar los Tweets",
30. error: err
31. })
32. return
33. }
34.
35. let response = tweets.map( x => {
36. return{
37. _id: x._id,
38. _creator: {
39. _id: x._creator._id,
40. name: x._creator.name,
41. userName: x._creator.userName,
42. avatar: x._creator.avatar || '/public/resources/avatars/0.png'
43. },
44. date: x.date,
45. message: x.message,
46. liked: x.likeRef.find(
47. likeUser => likeUser.toString() === user.id || null),
48. likeCounter: x.likeCounter,
49. replys: x.replys,
50. image: x.image
51. }
52. })
53.
54. res.send({
55. ok: true,
56. body: response
57. })
58. })
59. })
60. }
Página | 482
por usuario (línea 22). El resto del servicio es exactamente igual al anterior.
Terminamos aquí exportando la función al final del archivo.
Si actualizamos la pantalla de perfil, podremos ver que solo salen Tweet del
usuario en cuestión.
1. {
2. "username":"oscar",
3. "name":"Oscar Blancarte.",
4. "description":"User description",
5. "avatar":"<Base 64 Image>",
6. "banner":"<Base 64 Image>"
7. }
483 | Página
Response
Ok: booleana que indica si la operación fue
exitosa o no.
Body: contiene todos los datos actualizados del
perfil.
1. "ok": true,
2. "body": {
3. "_id": "5a012b1486b5c864a4fe6223",
4. "name": "Oscar Blancarte",
5. "description": "Nuevo en Twitter",
6. "userName": "oscar",
7. "avatar": "<base64 image>",
8. "banner": "<base64 image>",
9. "tweetCount": 76,
10. "following": 1,
11. "followers": 2,
12. "follow": false
13. }
14. }
Este es un método bastante simple, pues solo se crea un objeto con los cambios
(línea 3) y luego se procese con la actualización del perfil mediante el método
update del schema Profile. Se define el nombre de usuario como filtro para solo
actualizar el registro correcto, solo que el nombre de usuario no lo tomamos del
request, si no del token. De esta forma nos aseguramos que no puedan actualizar
más que su propio perfil.
Página | 484
1. //Private access services (security)
2. router.get('/secure/relogin',userController.relogin)
3. router.get('/secure/suggestedUsers',userController.getSuggestedUser)
4. router.put('/secure/profile', userController.updateProfile)
5.
6. //Public access services
7. router.get('/tweets/:user', tweetController.getUserTweets)
8. router.get('/tweets',tweetController.getNewTweets)
9. router.get('/usernameValidate/:username', userController.usernameValidate)
10. router.get('/profile/:user',userController.getProfileByUsername)
11. router.post('/signup', userController.signup)
12. router.post('/login', userController.login)
Para comprobar los resultados, solo restaría editar tu perfil de usuario, cambiar
tu nombre, descripción, avatar y banner, guardar los cambios y actualizar la vista
para asegurarnos de que los cambios se guardaron correctamente.
Este servicio se utiliza para recuperar el perfil de todas las personas a las que
seguimos. En el proyecto mini Twitter se utiliza en la sección del perfil del usuario.
Veamos la ficha:
1. {
2. "ok":true,
3. "body":[
4. {
5. "_id":"5938bdd8a4df2379ccabc1aa",
6. "userName":"emmanuel",
7. "name":"Emmauel Lopez",
8. "description":"Nuevo en Twitter",
9. "avatar":"<Base 64 Image>",
10. "banner":"<Base 64 Image>"
11. },
12. ...
13. ]
14. }
485 | Página
Crearemos la función getFollowing dentro del archivo UserController.js el cual
se verá de la siguiente manera:
Para obtener las personas que sigue un determinado usuario, es tan simple como,
consultar el perfil deseado y luego realizar un populate (join) mediante el campo
followingRef (línea 4), el cual es un array de ID de las personas que sigue. Ya
con eso, solo falta iterar los resultados para generar la respuesta (línea 9).
Para comprobar los resultados, solo basta con ir a la sección de “Siguiendo” del
perfil del usuario:
Página | 486
Fig. 205 - Probando la sección de "siguiendo".
Este servicio se utiliza para recuperar el perfil de todas las personas que siguen
a un determinado usuario. En el proyecto mini Twitter se utiliza en la sección del
perfil del usuario.
Veamos la ficha:
15. {
16. "ok":true,
17. "body":[
18. {
19. "_id":"5938bdd8a4df2379ccabc1aa",
20. "userName":"emmanuel",
21. "name":"Emmauel Lopez",
22. "description":"Nuevo en Twitter",
23. "avatar":"<Base 64 Image>",
24. "banner":"<Base 64 Image>"
25. },
487 | Página
26. ...
27. ]
28. }
Para obtener las personas que siguen al usuario, es tan simple como, consultar
el perfil deseado y luego realizar un populate (join) mediante el campo
followersRef (línea 4), el cual es un array de ID de las personas que lo siguen.
Ya con eso, solo falta iterar los resultados para generar la respuesta (línea 9).
Página | 488
Para comprobar los resultados, solo basta con ir a la sección de “Seguidores” del
perfil del usuario:
Servicio – Seguir
Este servicio es un poco más complicado que el resto, pues implica transaccionar
el documento de los dos perfiles involucrados. A un documento hay que agregarle
un seguidor (followersRef) y al otro hay que agregarle que lo seguimos
(followingsRef). Pero si lo dejamos de seguir, hay que hacer exactamente lo
contrario.
Nombre Seguir
URL /secure/follow
URL N/A
params
Método GET
Headers authorization: token del usuario
Request
followingUser: nombre de usuario del perfil
que deseamos seguir/dejar de seguir
1. {
2. "followingUser":"jperez"
3. }
489 | Página
Response
Ok: booleana que indica si la operación fue
exitosa o no.
Unfollow: booleano que indica si seguimos o
dejamos de seguir, false indica que lo seguimos
y true que lo dejamos de seguir
1. {
2. "ok": true,
3. "unfollow": false
4. }
Página | 490
47. })
48. .catch(err => {
49. res.send({
50. ok: false,
51. message: err.message || "Error al ejecutar la operación",
52. err: err.error || err
53. })
54. })
55. }
Lo primero que haremos será identificar el usuario que solicita la acción de seguir
y el usuario que vamos a seguir, para ello, guardamos en la variable username
(línea 2) el usuario que solicita la acción, el cual recuperamos del token. Por otra
parte, guardamos el usuario al que vamos a seguir en la variable followingUser
(línea 3).
Lo siguiente es consultar los dos perfiles (el que solicita y al que seguiremos)
(línea 5), y utilizamos el operador $in, para consultar los dos perfiles. Si el
resultado regresa menos de dos documentos, quiere decir que uno de los dos
perfiles no existe y regresamos un error.
Una vez que tenemos los dos perfiles, tenemos que identificar en qué posición
del arreglo se encuentra el solicitante y al que seguiremos, para ello, guardamos
el index de su posición en las variables my y other (líneas 8 y 9).
Los siguientes pasos son más fáciles, pues solo tendremos que actualizar los dos
perfiles mediante los comandos ya preparados, las actualizaciones las
realizaremos en las líneas 22 y 24.
Para probar los cambios, solo tenemos que presionar el botón se “seguir” o
“siguiendo" del perfil de cualquier usuario.
491 | Página
Este servicio nos permitirá crear un nuevo Tweet o crear una respuesta a un
Tweet existente. En el proyecto Mini Twitter es utilizado por el componente
Reply.js, el cual se utiliza desde la página principal o como parte del detalle de
un Tweet para realizar una respuesta.
1. {
2. "message": "¡hola mundo! este es mi primer Tweet",
3. "image": "<base64 img>"
4. }
Response
Ok: valor booleano que indica si la operación fue
exitosa o no.
tweet: Objecto con todos los datos del Tweet,
entre los que están:
o _id: ID asociado al Tweet.
o date: fecha de creación
o message: mensaje asociado al tweet
o Image: Imagen asociada al tweet.
1. {
2. "ok": true,
3. "tweet": {
4. "__v": 0,
5. "_creator": "593616dc3f66bd6ac4596328",
6. "message": "hola mundo",
7. "image": "<base64 img>",
8. "_id": "59f66ea3ceb9f6a00c7b3143",
9. "replys": 0,
10. "likeCounter": 0,
11. "date": "2017-10-30T00:13:23.293Z"
12. }
13. }
Lo primero que haremos será crear la función addTweet dentro del archivo
TweetController.js:
Página | 492
6. }
7. }
Dado que la lógica para crear un Tweet y una respuesta es distinta, hemos
separado la funcionalidad en dos funciones. La función createNewTweet (línea 5)
la utilizaremos para crear un nuevo Tweet, mientras que la función
createReplyTweet (línea 3) es para crear las respuestas.
493 | Página
1. function createReplyTweet(req, res, err){
2. let user = req.user
3. const newTweet = new Tweet({
4. _creator: user.id,
5. tweetParent: req.body.tweetParent,
6. message: req.body.message,
7. image: req.body.image
8. })
9.
10. Tweet.update({_id: req.body.tweetParent},{$inc:{replys:1}})
11. .then(update => {
12. if((!update.ok) || update.nModified == 0 )
13. throw {message: "No existe el Tweet padre"}
14. newTweet.save()
15. .then(saveTweet => {
16. res.send({
17. ok: true,
18. tweet: saveTweet
19. })
20. })
21. .catch(err => {
22. res.send({
23. ok: false,
24. message: "Error al guardar el Tweet",
25. error: err
26. })
27. })
28. })
29. .catch(err => {
30. res.send({
31. ok: false,
32. message: "Error al actualizar al usuario",
33. error: err
34. })
35. })
36. }
Para crear una respuesta, la lógica cambia un poco, lo primero será crear el
objeto del Tweet correspondiente la de respuesta (línea 3). El segundo paso será
actualizar el Tweet padre, al cual hay que incrementarle en 1 el contador de
respuestas (línea 10). Si todo sale bien y hemos actualizado al menos un registro
(línea 12), entonces procedemos con guardar el Tweet de respuesta mediante el
método save (línea 14). Finalmente retornamos el nuevo Tweet creado en la línea
16.
Ahora solo nos restaría agregar únicamente la función addTweet a los exports y
agregar el router correspondiente en el archivo api.js:
Para probar los cambios, solo basta con crear un nuevo tweet desde la pantalla
principal, un que las respuestas será difícil probar, pues de momento no
podremos ver el detalle del Tweet para comprobar el resultado:
Página | 494
Fig. 207 –Probando la creación de un nuevo Tweet.
Servicio – Like
El servicio like nos permite indicar que un Tweet es de nuestro agrado, con lo
cual, el Tweet va incrementando un contador de likes. Aunque también le
podemos indicar que algo ya no nos gusta. Este servicio se utiliza desde el
componente Tweet.js cuando presionamos el botón del corazón.
Nombre Like
URL /secure/like
URL N/A
params
Método POST
Headers authorization: token del usuario
Request
tweetID: ID del Tweet al que deseamos dar like
like : Valor booleano que indica si queremos darle
like (true) o dislike (false).
1. {
2. "tweetID": "59ed5728022307a950b3c756",
3. "like": true
4. }
495 | Página
Response
Ok: booleano que indica si la operación fue exitosa
o no.
body: Objeto que contiene los datos actualizados
del Tweet.
1. {
2. "ok": true,
3. "body": {
4. "_id": "59ed5728022307a950b3c756",
5. "_creator": "593616dc3f66bd6ac4596328",
6. "message": "Mi libro de \"Patrones de diseño\"",
7. "image": "<Base 64 Image>",
8. "__v": 0,
9. "replys": 4,
10. "likeCounter": 1,
11. "date": "2017-10-23T02:42:48.679Z"
12. }
13. }
Página | 496
Darle like a un Tweet es una tarea muy simple, pues solo es necesario agregar
el ID del usuario dentro del array likeRef del objeto Tweet. Por otra parte, si lo
que buscamos es quitar el like, solo tenemos que eliminar el ID del array.
Finalmente, solo restaría agregar esta nueva función a los exports y agregar el
router correspondiente al archivo api.js:
Para probar los cambios solo presionemos el botón del corazón en cualquier
Tweet:
El último servicio que nos resta para terminar el API es de la consulta del detalle
de un Tweet, el cual nos permite recuperar todas las respuestas asociadas a un
Tweet.
Este servicio es utilizado al momento de dar click sobre cualquier Tweet, donde
de forma modal, podemos ver todo el Tweet con su detalle.
497 | Página
Veamos la ficha del servicio:
Nombre Like
URL /tweetDetails/:tweetID
URL tweetID: ID del Tweet a consular el detalle.
params
Método GET
Headers N/A
Request N/A
Response
Ok: booleano que indica si la operación fue exitosa
o no.
body: Objeto que contiene un Tweet con todo su
detalle
o _id: identificador único del Tweet.
o _creator: Perfil del usuario que creo el
Tweet.
o _date: fecha de creación del Tweet.
o Message: Texto del Tweet.
o Liked: indica si le dimos like al Tweet.
o likeCounter: contador de likes
o image: Imagen asociada al Tweet.
o Replys: números de respuestas
o reploysTweets: Arreglo de Tweets
correspondientes a las respuestas
1. {
2. "ok": true,
3. "body": {
4. "_id": "59ed5728022307a950b3c756",
5. "_creator": {
6. "_id": "593616dc3f66bd6ac4596328",
7. "name": "Oscar Blancarte.",
8. "userName": "oscar",
9. "avatar": "<Base 64 Image>"
10. },
11. "date": "2017-10-23T02:42:48.679Z",
12. "message": "Mi libro de \"Patrones de diseño\"",
13. "liked": false,
14. "likeCounter": 0,
15. "image": "<Base 64 Image>",
16. "replys": 1,
17. "replysTweets": [
18. {
19. "_id": "59f51e49830f6ac1c4c841a2",
20. "_creator": {
21. "_id": "593616dc3f66bd6ac4596328",
22. "name": "Oscar Blancarte.",
23. "userName": "oscar",
24. "avatar": "<Base 64 Image>"
25. },
26. "date": "2017-10-29T00:18:17.071Z",
27. "message": "dgnfdgh",
28. "liked": false,
29. "likeCounter": 0,
Página | 498
30. "replys": 0,
31. "image": null
32. },
33. ...
34. ]
35. }
36. }
499 | Página
52. },
53. date: tweet.date,
54. message: tweet.message,
55. liked: tweet.likeRef.find(
56. likeUser => likeUser.toString() === user.id || null),
57. likeCounter: tweet.likeCounter,
58. image: tweet.image,
59. replys: tweet.replys,
60. replysTweets: replys
61. }
62. })
63. }).catch(err => {
64. res.send({
65. ok:false,
66. message: err.message || "Error al consultar el detalle",
67. error: err.error || err
68. })
69. })
70. }).catch(err => {
71. res.send({
72. ok:false,
73. message: err.message || "Error al cargar el Tweet",
74. error: err.error || err
75. })
76. })
77. }
La consulta del detalle del Tweet puede aparentar complicada, pero en realidad
es muy simples y solo se requiere de dos pasos para obtener la información
necesaria. El primero es consular al Tweet del cual se requiere el detalle, para
eso, recuperamos el ID desde los URL params (línea 3) y luego realizar la
búsqueda del Tweet por medio del ID (línea 12), aprovechamos para realizar un
populate (join) con el perfil del usuario (línea 13).
En este punto, solo tenemos que exportar la nueva función y agregar el router
correspondiente:
Para probar los cambios, solo tendremos que dar click sobre cualquier Tweet y
un popup debería emerger con todo el detalle del Tweet:
Página | 500
Fig. 209 - Probando el detalle del Tweet.
Por esa razón, vamos aprender a crear una documentación simple para nuestra
API, la cual podrás utilizar más adelante para documentar cualquier otra.
501 | Página
la página oficial de Pug, en donde podrás encontrar toda la información y
documentación actualizada.
Esta sección busca explicar los conceptos más básicos de Pug, ya que lo
utilizaremos para desarrollar la documentación del API, sin embargo, si se quiere
profundizar en el aprendizaje de este motor de plantillas, es recomendable
dirigirse a la documentación oficial o buscar una lectura especifica del tema.
Básicamente, Pug nos permite crear una plantilla escrita en su propio lenguaje y
este compila la plantilla para entregarnos un HTML puro y compatible con el
navegador.
1. <!DOCTYPE html>
2. <html lang="es">
3. <head>
4. <title>Pug</title>
5. <script type="text/javascript">
6. foo = true;
7. bar = function () {};
8. if (foo) {
9. bar(1 + 5)
10. }
11. </script>
12. </head>
13. <body>
14. <h1>Pug - node template engine</h1>
15. <div id="container" class="col">
16. <p>You are amazing</p>
17. <p>Jade is a terse and simple.</p>
18. </div>
19. </body>
20. </html>
Este mismo documento que acabamos de ver se puede simplificar con Pug, de
tal forma que el siguiente documento da como resultado el mismo documento
HTML que acabo de ver:
1. doctype html
2. html(lang='es')
3. head
4. title Pug
5. script(type='text/javascript').
6. foo = true;
7. bar = function () {};
8. if (foo) {
9. bar(1 + 5)
10. }
11. body
12. h1 Pug - node template engine
13. #container.col
14. p You are amazing
15. p Jade is a terse and simple.
Página | 502
Solo a simple vista, podemos ver una reducción considerable de líneas (20 vs
15), es decir, nos hemos ahorrado una cuarta parte y este rango incrementa con
documentos más grandes.
Pug utiliza los “tabs” o espacios para identificar que elemento va dentro de otro,
ya que no cuenta con una etiqueta de apertura y cierre, es por ello, que es
sumamente importante respetar los tabuladores. Por ejemplo, Pug sabe que la
etiqueta head y body van dentro de html, debido a que estas dos tiene un tab más
que html. De la misma forma, Pug sabe que title va dentro de head por que
title tiene un tab más que titile.
Clases de estilo
Otra de las ventajas que ofrece Pug, es la forma en que no permite definir las
clases de estilo, pues solo tenemos que agregar un punto (.) antes de cada clase
de estilo. Veamos el siguiente ejemplo:
1. div.myclass.myclass2
1. .myclass.myclass2
Cuando un elemento empieza con punto (.). Pug asume que es un div, por lo que
el resultado anterior es el mismo que si iniciáramos con div.
Establecer un ID a un elemento
1. .myclass.myclass2#myID
503 | Página
Por otra parte, si lo que buscamos es agregarle texto a un elemento, solo
tenemos que agregar un espacio en blanco y agregar el texto:
El resultado:
Pug permite definir los atributos de varias formas, pero nos centraremos en las
dos principales formas. La primera y más utilizada es definir todas las
propiedades en línea, en donde cada atributo es colocado uno enseguida del otro,
pero separados con una coma y todos dentro de un par de paréntesis.
1. link(rel='stylesheet', href='/styles.css')
1. link(
2. rel='stylesheet'
3. href='/styles.css'
4. )
Tipo de código
Pug no solo permite agregar fácilmente etiquetas HTML, si no que permite hacer
paginas dinámicas mediante la inclusión de fragmentos de código Javascript, con
los cuales es posible agregar variables, ciclos, condiciones, etc.
Unbuffered Code
Página | 504
Pug HTML Output
1. - for (var x = 0; x < 3; x++) 1. <li>item</li>
2. li item 2. <li>item</li>
3. <li>item</li>
Buffered code
Unescaped Buffered
Iteraciones
Pug soporta dos tipos de iteraciones, each y while, las cuales funcionan
exactamente igual que en cualquier lenguaje de programación.
Each
505 | Página
Como resultado tenemos la iteración del arreglo definido en el mismo ciclo, el
cual consta te de 3 elementos. El valor de cada iteración se guarda en la variable
val, que luego es mostrada en pantalla utilizando un bloque buffered (=).
While
API home
El API Home o página de bienviva, debe de ser una página sencilla alojada en el
home de la URL del API, en este caso sería api.localhost:8080. En esta página se
aconsejable brindar un mensaje al usuario para que sepa que está en el API.
También se aconseja mostrar los términos de uso y una liga a la documentación
de los servicios disponibles.
Página | 506
Si regresamos al archivo api.js, podremos ver que hemos definido un router
para escuchar en la raíz del subdominio:
Pug nos proporciona el método renderFile, el cual sirve para compilar la plantilla
y darnos un documento HTML como respuesta. Puede recibir básicamente dos
parámetros, uno de la URL a la plantilla y el segundo es un objeto que sirve como
parámetros para la plantilla, aunque en este caso, solo utilizamos un parámetro.
Dicho esto, podríamos resumir que cuando el home (/) del api sea ejecutado,
Pug abrirá la plantilla api-index.pug y nos retornará el HTML de la página. Ahora
bien, seguramente te estarás preguntando como desarrollamos la plantilla.
1. doctype html
2. html
3. head
4. link(href='https://fonts.googleapis.com/css?family=Roboto', rel='stylesheet')
5. link(
6. rel='stylesheet'
7. href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css'
8. integrity='sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u'
9. crossorigin='anonymous'
10. )
11. link(rel='stylesheet', href='/public/apidoc/api-styles.css')
12. body
13. .api-alert
14. p.title Mini Twitter API REST
15. p.body
16. | Esta API es provista como parte de la aplicación Mini Twitter,
17. | la cual es parte del libro
18. a(href='#')
19. strong "Desarrollo de aplicaciones reactivas con React, NodeJS y MongoDB"
20. | , por lo que su uso es únicamente con fines educativos y por
21. | ningún motivo deberá ser empleada para aplicaciones productivas.
22. .footer
23. button.btn.btn-warning(data-toggle='modal', data-target='#myModal')
24. | Terminos de uso
25. a.btn.btn-primary(href='/catalog') Ver documentación
26.
27. #myModal.modal.fade(
28. tabindex='-1', role='dialog', aria-labelledby='myModalLabel',
29. aria-hidden='true')
30. .modal-dialog
31. .modal-content
32. .modal-header
33. button.close(type='button', data-dismiss='modal',
34. aria-hidden='true') ×
35. h4#myModalLabel.modal-title Terminos de uso
36. .modal-body
37. p El API de Mini Twitter es provista por Oscar Blancarte, autor del libro
38. strong "Desarrollo de aplicaciones reactivas con React, NodeJs y MongoDB"
39. | con fines exclusivamente educativos.
40. p Esta API es provista
41. strong "tal cual"
42. | esta, y el autor se deslizanda de cualquier problema o falla
43. | resultante de su uso. En ningún momento el autor será responsable
44. | por ningún daño directo o indirecto por la pérdida o publicación
45. | de información sensible.
46. strong El usuario es el único responsable por el uso y la información
507 | Página
47. | que este pública.
48. .modal-footer
49. button.btn.btn-primary(type='button', data-dismiss='modal') Cerrar
50.
51.
52. script(src='https://code.jquery.com/jquery.js')
53. script(src='//netdna.bootstrapcdn.com/bootstrap/3.0.3/js/bootstrap.min.js')
El body está compuesto básicamente por dos secciones, el panel que podemos
ver en pantalla y un diálogo que muestra los términos de uso. La primera sección
(líneas 13 a 25) corresponde a lo que ve el usuario en pantalla, el cual contiene
un título (línea 14), el cuerpo o mensaje (líneas 15 a 21) y el footer, que es
donde ponemos los botones (líneas 22 a 25). Observemos que el botón para los
términos de uso (línea 23), contiene los atributos data-toggle y data-target, los
cuales son provistos por Bootstrap para crear paneles modales, el primer atributo
es el tipo de pantalla que queremos, en este caso modal y el segundo es el ID
del elemento que vamos a mostrar cuando se presione el botón. El otro botón
nos manda a /catalog, en donde estarán listados todos los servicios disponibles.
Service catalog
Como parte del API, siempre deberemos tener una sección donde listemos los
servicios disponibles. Los servicios pueden ser mostrados por categorías si son
muchos o listar todos en una misma sección, si el número de servicios es
reducido, como es nuestro caso, una solo pantalla servirá para mostrarlos.
Página | 508
Fig. 211 - API Catalog.
Como podemos observar, el catalogo es simplemente una lista con los servicios
disponibles con la información básica:
509 | Página
Lo primero que crearemos será, el archivo api-catalog.pug en el path
/public/apidoc, el cual se ve de la siguiente manera:
1. doctype html
2. html(lang="es")
3. head
4. title Mini Twitter API REST
5. link(href='https://fonts.googleapis.com/css?family=Roboto', rel='stylesheet')
6. link(rel='stylesheet'
7. href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css'
8. integrity='sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u'
9. crossorigin='anonymous')
10. link(rel='stylesheet', href='/public/apidoc/api-styles.css')
11. body
12. .container
13. .row
14. .col-xs-12
15. .method-templete
16. .list-group
17. each item in services
18. a.list-group-item(href=item.apiURLPage)
19. if item.secure
20. span.badge secure
21. h4.list-group-item-heading #{item.title}
22. span.label.label-success #{item.method}
23. span.label.label-primary #{item.url}
24. p.list-group-item-text #{item.desc}
25. script(src='https://code.jquery.com/jquery.js')
26. script(src='//netdna.bootstrapcdn.com/bootstrap/3.0.3/js/bootstrap.min.js')
Esta página recibe como parámetro un objeto llamado “services”, que no es más
que un array con los datos de todos los servicios disponibles, el cual vamos a
iterar en la línea 17 para representar cada servicio. Vamos a utilizar la variable
“ítem” para guardar los datos de cada servicio. Los atributos disponibles para
cada servicio son:
Página | 510
Como podrás ver, este router muestra el archivo api-catalog.pug y le manda
como parámetro el objeto meta, el cual es un archivo que contiene la lista de
servicios, el cual vamos a explicar.
Ahora bien, esta página requiere del archivo catalog.js, el cual deberemos de
crear en el path /public/apidoc/meta y se verá de la siguiente manera:
Este archivo solo exporta un objeto llamado services, el cual es creado a partir
de una serie de objetos, es decir un archivo por servicio.
Vamos a explicar la estructura que deberá tener cada archivo, la cual es la misma
para todos, pero la información cambia:
1. module.exports = {
2. apiURLPage: "/catalog/addtweets-post",
3. title:"Creación de nuevo Tweet",
4. desc:"Servico utilizado para la creación de un nuevo Tweet",
5. secure: true,
6. url: "/secure/tweets",
7. method:"POST",
8. urlParams: [],
511 | Página
9. requestFormat:"{}",
10. dataParams: "{}",
11. successResponse: "{}",
12. errorResponse:"{}"
13. }
Debido a que son un total de 15 archivos y que son bastante repetitivos, vamos
a limitarnos a mostrar solo uno, con la única finalidad darnos una idea de cómo
quedaría un archivo terminado. El resto de archivos lo podemos encontrar en el
repositorio de Github.
1. module.exports = {
2. apiURLPage: "/catalog/followers-get",
3. title:"Consulta de seguidores de un usuario determinado",
4. desc:"Mediante este servico es posible recuperar los seguidores de un usuario d
eterminado por el url param 'username'",
5. secure: false,
6. url: "/followers/:username",
7. method:"GET",
8. urlParams: [
9. {
10. name: "username",
11. desc: "Nombre de usuario",
12. require: true
13. }
14. ],
15. requestFormat:"",
16. dataParams: "",
17. successResponse: "{\r\n \"ok\":true,\r\n \"body\":[\r\n {\r\n
\"_id\":\"5938bdd8a4df2379ccabc1aa\",\r\n \"userName\":\"emmanuel\",\r\n
\"name\":\"Emmauel Lopez\",\r\n \"description\":\"Nuevo en Twitte
r\",\r\n \"avatar\":\"<Base 64 Image>\",\r\n \"banner\":\"<Base 6
4 Image>\"\r\n },\r\n {\r\n \"_id\":\"5938bdd8a4df2379ccabc1aa\
",\r\n \"userName\":\"carlos\",\r\n \"name\":\"Carlos Hernandez\"
,\r\n \"description\":\"Nuevo en Twitter\",\r\n \"avatar\":\"<Bas
Página | 512
e 64 Image>\",\r\n \"banner\":\"<Base 64 Image>\"\r\n }\r\n ]\r\n}
",
18. errorResponse:"{\r\n \"ok\": false,\r\n \"message\": \"No existe el usuario
\"\r\n}"
19. }
Service documentation
La última página que nos faltaría, es donde podemos ver toda la documentación
del servicio, la cual se verá de la siguiente manera:
513 | Página
Fig. 214 - Documentación de un servicio.
1. doctype html
2. html(lang="es")
3. head
4. title Mini Twitter API REST
5. link(rel='stylesheet',
6. href='//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/default.min.css')
7. script(
8. src='//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js')
9. script.
10. hljs.initHighlightingOnLoad();
11. link(href='https://fonts.googleapis.com/css?family=Roboto', rel='stylesheet')
12. link(rel='stylesheet',
13. href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css',
14. integrity='sha384-
BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u',
15. crossorigin='anonymous')
Página | 514
16. link(rel='stylesheet', href='/public/apidoc/api-styles.css')
17. body
18. .container
19. .row
20. .col-xs-12
21. .method-templete
22. if secure
23. span.secure-icon 🔒
24. .form-group
25. label(for='name') Nombre
26. output#name #{title}
27. .form-group
28. label(for='desc') Descripción
29. output#desc #{desc}
30. .form-group
31. label(for='url') URL
32. output#url #{url}
33. .form-group
34. label(for='method') Method
35. output#method #{method}
36. .form-group
37. label(for='urlParams') URL Params
38. ul.list-group1
39. each item in urlParams
40. li.list-group-item
41. strong= item.name + ': '
42. span= item.desc
43. if item.require
44. span.badge.badge-warning.badge-pill requerido
45. else
46. li.list-group-item Sin parámetros
47. .form-group
48. label(for='requestFormat') Formato del request
49. <pre><code class="json">#{requestFormat}</code></pre>
50. .form-group
51. label(for='dataParams') Request
52. <pre><code class="json">#{dataParams}</code></pre>
53. .form-group
54. label(for='successResponse') Respuesta OK
55. <pre><code class="json">#{successResponse}</code></pre>
56. .form-group
57. label(for='errorResponse') Respuesta Error
58. <pre><code class="json">#{errorResponse}</code></pre>
59. if secure
60. .alert.alert-danger
61. strong 🔒 Servicio con seguridad
62. p Este es un servicio con seguridad habilitada, para poder ser
63. | ejecutado, es requerido que se le envíe el
64. strong token
65. | dentro del header
66. strong authorization
67. | de lo contrario, el servicio negará el acceso.
Mediante este archivo, creamos un simple formulario, el cual mostrará cada uno
de los valores contenidos en los objetos JSON que acabamos de analizar.
De las líneas 24 a 35 mostramos los campos title, desc, url y method, después
de esto, realizamos un each (línea 39) para cada URL param definido.
515 | Página
la librería se active (línea 10), reconocerá las clases de estilo y dará formato
automáticamente.
Esta sección la defino para nombrar todas las mejores que podríamos agregar al
API, las cuales por practicidad y no complicar mucho más el API, he decidido
darles una solución “rápida”, la cual puede no ser la mejor forma de implementar.
Todas las mejoras aquí planteadas las puedes tomar como ejercicios para
mejorar tus habilidades en el desarrollo de API’s.
Aprovisionamiento de imágenes
El problema con esta estrategia es que al retornar la imagen como base 64 dentro
de un objeto JSON, impide que el navegador utilice el cache para no cargar una
imagen que ya cargo antes. Por ejemplo, la foto del perfil:
Página | 516
Fig. 215 - Foto de perfil en base64
1. {
2. "ok": true,
3. "body": {
4. "_id": "593616dc3f66bd6ac4596328",
5. "name": "Name",
6. "description": "Descripción",
7. "userName": "user name",
8. "avatar": "http://api.site.com/profile/avatar/593616dc3f66bd6ac4596328",
9. "banner": "http://api.site.com/profile/banner/593616dc3f66bd6ac4596328",
10. "tweetCount": 44,
11. "following": 0,
12. "followers": 3,
13. "follow": false
14. }
15. }
Podemos apreciar los campos avatar y banner que en lugar de tener una imagen
en base 64, tiene un URL que lleva a donde está la imagen.
517 | Página
La segunda forma es seguir guardado el base 64 dentro de Mongo, pero
proporcionar un servicio del API que recupere la imagen por URL. El servicio
podrá tener URL params para saber qué imagen necesitamos y de que
usuario/tweet, por ejemplo /resources/:userId/avatar y
/resources/:userId/banner, con estos URL params podemos recuperar el Perfl
solicitado y hora si regresar la imagen en base64. Si bien la imagen la seguimos
mandando en base 64, el navegador es lo suficiente inteligente para saber que
un recurso solicitado por URL ya lo tiene en cache y evitar solicitarlo nuevamente.
Como acabamos de ver, es necesario crear un archivo JavaScript para cada uno
de los servicios que tenemos, lo cual podría ser bastante complicado de
administrar, para ello, podríamos crear una nueva colección para guardar la
documentación de los servicios y simplemente recuperarla cuando sea necesaria.
Página | 518
Resumen
Junto con el API, hemos creado una página especial para documentar cada uno
de los servicios que ofrece nuestra API, utilizando para ello, el motor de plantillas
Pug.
519 | Página
Producción
Capítulo 16
En este capítulo analizaremos técnicas para que el pase a producción sea lo más
simple posible, pero también implementar todas estas prácticas que harán de
nuestro sitio más rápido, seguro y robusto.
Producción vs desarrollo
Desde luego que en las grandes empresas hay más ambientes que solo desarrollo
y producción, como el ambiente de pruebas (para testing), QA (pruebas de UAT)
Página | 520
y Stress (pruebas de carga) y quizás algunos ambientes más. Sin embargo,
nosotros abordaremos solo desarrollo y producción.
En Windows:
1. "scripts": {
2. "start": "set NODE_ENV=production&&nodemon server.js"
3. },
En Linux
1. "scripts": {
2. "start": "NODE_ENV=production&&nodemon server.js"
3. },
521 | Página
Fig. 216 - Validando la variable NODE_ENV.
Este módulo tiene como finalidad facilitar el ciclo de desarrollo, pues evita tener
que compilar manualmente cada cambio. Sin embargo, el archivo bundle.js lo
crea en memoria y no en la carpeta /public, como debería.
Página | 522
El siguiente paso será indicarle a webpack que empaquete nuestra aplicación
para producción, para ello, tendremos que ir al archivo webpack.config.js y
agregar las siguientes líneas:
1. "scripts": {
2. "build": "webpack -p",
3. "server": "NODE_ENV=production&&node server.js",
4. "server_windows": "set NODE_ENV=production&&node server.js",
5. "start": "npm run build && npm run server_windows"
6. },
Primero que nada, observa que hemos cambiado el comando existente “start”
para que ejecute dos comandos en serie, primero que nada, ejecutará el
comando “build” (npm run build) y luego server_windows (npm run
server_windows). El script build es el encargado de ejecutar el comando webpack
-p (p=producción) y luego el comando server_windows establecerá la variable de
entorno NODE_ENV y seguido, iniciará el servidor (server.js).
523 | Página
este momento, será necesario apagar el server por completo e iniciar cada vez
que tengamos un nuevo cambio, de lo contrario, no se reflejará.
Ya con esta explicación, solo nos resta apagar el servidor e iniciarlo nuevamente
con el comando npm start. Una vez iniciado, intentamos entrar a la aplicación
Mini Twitter para comprobar que ahora si podemos ver la aplicación.
Una vez que estés en la aplicación, quiero que observar que el icono de React ha
cambiado de color rojo al color azul, y si le damos click, nos arrojará un mensaje
indicándonos que la aplicación está corriendo en modo productivo. De lado
izquierdo puedes ver como se veía la aplicación antes de los cambios, del lado
derecho, como se ve ahora.
Icono de React
Si no logras ver el icono de React en la parte superior
del navegador, es porque no has instalado el plugin
necesario, en tal caso, puedes regresar al inicio del
libro para aprender como instalar el plugin React
Developer Tools.
Puertos
Internet trabaja exclusivamente con los puertos 80 para HTTPS y 443 para
HTTPS, por lo que configurar nuestra aplicación para trabajar en estos puertos
Página | 524
es indispensable, de lo contrario, los usuarios no podrán acceder a nuestra página
con solo poner el dominio, si no que tendrán que adivinar el puerto en el cual
responde la aplicación.
Dado que nuestro proyecto escucha en el puerto 8080, será muy difícil que un
usuario pueda acceder a nuestra aplicación. Para solucionar esto tenemos dos
opciones, la primera y más simples, es cambiar el puerto de NodeJS al 80. La
segunda opción es crear un proxy que escuche en el puerto 80 y luego
redirecciones la llamada al nuestro servidor en el puerto configurado en NodeJS,
esto se puedo lograr con Apache, Nginx, etc.
Configurar un servidor proxy queda fuera del alcance de este libro, sin embargo,
quería mencionar la alternativa por si quieres investigar más a profundidad. Esto
nos deja únicamente con la primera opción, que es cambiar el puerto en NodeJS.
Para ello, tendremos que regresar al archivo server.js y modificar el puerto:
1. app.listen(80, function () {
2. console.log('Example app listening on port 80!')
3. })
Tras realizar este simple paso, podremos acceder a la aplicación con tan solo
poner en el navegador “localhost” sin necesidad de especificar el puerto.
525 | Página
Posibles problemas con el puerto 80
El puerto 80 es muy demandado, muchas aplicaciones
lo utilizan sin que nos demos cuenta, por lo que, si al
iniciar la aplicación sale un error de que el puerto está
siendo utilizado, tendremos que identificar que
aplicación lo utiliza y detenerlo. También, en
ocasiones nos solicita privilegios como administrador
para utilizarlo.
Comunicación segura
Por este motivo, utilizar conexiones seguras con HTTPS es indispensable, pues
protege la información encriptándola durante su viaje por internet, y una vez que
llega al destinatario, solo el cliente o el servidor sabrán como descifrarla.
Certificados comprados
Los certificados comprados son emitidos por autoridades de internet, los cuales
recopilan información de nuestra empresa y nos cobran una cantidad para
emitirlos.
Los certificados comprados, pueden ser validados por los navegadores, lo que
habilita el candado verde que podemos ver en la barra de navegación:
Página | 526
Fig. 219 - Validación de un certificado SSL comprado.
Existen varios proveedores que nos pueden vender certificados, como GoDaddy,
Comodo, Namecheap, Digicert, etc. Dado que la única diferencia que existe entre
todos los proveedores es el precio, puedes inclinarte por el que tenga el mejor
precio. En lo personal yo utilizo los certificados de Namecheap, pues ofrece
certificados desde 9 usd al año, lo cual es bastante económico.
Por otro lado, tenemos los certificados auto firmados, los cuales pueden ser
emitidos por quien sea, incluso, podemos crear nuestros propios certificados
nosotros mismos sin ningún costo.
527 | Página
Fig. 220 - Advertencia de conexión no segura.
Esta pantalla ara que la gente salga corriendo del sitio y solo los más valientes
tendrán el valor de entrar.
Para que el navegador nos permite acceder a la página, nos pedirá que
agreguemos el sitio a las excepciones de seguridad, por lo que tendremos que
dar click en “Avanzado” y luego en “Añadir excepción”.
Una vez realizado esto, en navegador nos permitirá entrar al sitio, pero nos
indicará que el sitio no es seguro:
Página | 528
Como podemos ver en la imagen, el navegador nos arroja un mensaje en rojo
indicando que no es seguro, lo que hará que los pocos que tuvieron el valor de
entrar duden de la seguridad del sitio.
En este punto, te estarás preguntando, entonces para que pueden servir este
tipo de certificados, la respuesta es simple, se utilizan con regularidad para sitios
de intranet o que solo lo acceder personas de confianza de la misma empresa,
las cuales saben que pueden confiar en el sitio y en el certificado. Sin embargo,
para el público en general, es totalmente desaconsejado.
Sea cual sea el certificado que utilicemos, al final del día, tendremos dos archivos,
un certificado y una llave, que será lo que necesitamos para agregar HTTPS a
nuestro servidor.
Para auto generar nuestro certificado, tendremos que descargar una librería
criptográfica que nos permite utilizar SSL. La más popular es OpenSSL, la cual
es OpenSource.
http://gnuwin32.sourceforge.net/packages/openssl.htm
https://www.openssl.org
529 | Página
Ya que OpenSSL está correctamente instalado, crearemos la carpeta cert en la
raíz del proyecto (/) y nos ubicaremos dentro de la carpeta desde la línea de
comandos, luego ejecutamos el siguiente comando:
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365
Una vez creados los certificados, los podremos ver desde nuestro editor:
Página | 530
Fig. 223 - Certificados creados exitosamente.
1. var fs = require('fs')
2. var https = require('https')
3. var http = require('http')
4.
5. . . .
6.
7. https.createServer({
8. key: fs.readFileSync('./certs/key.pem'),
9. cert: fs.readFileSync('./certs/cert.pem'),
10. passphrase: '1234'
11. }, app).listen(443, () => {
12. console.log('Example app listening on port 443!')
13. });
14.
15. http.createServer(function (req, res) {
16. res.writeHead(301, {"Location": "https://" + req.headers['host'] + req.url})
17. res.end()
18. }).listen(80)
19.
20. app.listen(80, function () {
21. console.log('Example app listening on port 80!')
22. })
Vamos a importar los módulos fs, http y https, los cuales ya viene por default
con NodeJS, por lo que no hay que instalar nada adicional.
531 | Página
Posibles problemas con el puerto 443
Al igual que el puerto 80, el puerto 443 es muy
solicitado por las aplicaciones, por lo que si el puerto
ya está siendo utilizado, tendremos que detener la
aplicación que lo utiliza.
Alta disponibilidad
Cluster
Página | 532
Una de las principales técnicas de alta disponibilidad son los cluster, los cuales
son un conjunto de servidores trabajando como uno solo, de tal forma que, si
uno falla, los demás pueden seguir operando.
Desde luego que tener múltiples servidores físicos es lo mejor, pues si todo un
equipo falla, otros podrán seguir operando. sin embargo, esto está fuera del
alcance de este libro, pues para lograr eso es necesario más configuraciones,
herramientas y equipos adicionales que no tenemos en un ambiente local.
Otra de las cosas a tomar en cuenta es que, NodeJS se ejecuta en un solo hilo,
por lo que en procesadores multicores, no se aprovechará su potencial, por este
motivo, el cluster es una buena opción para lanzar múltiples procesos y así
mejorar el performance ante la carga de trabajo.
El segundo paso será agregar todo el inicio del server dentro de una función, la
cual podremos reutilizar para crear las diferentes instancias del servidor. Para
ello, actualizaremos el archivo server.js para dejarlo de la siguiente manera:
533 | Página
23. bufferMaxEntries: 0,
24. reconnectTries: Number.MAX_VALUE, // Never stop trying to reconnect
25. reconnectInterval: 500,
26. autoReconnect: true,
27. loggerLevel: "error", //error / warn / info / debug
28. keepAlive: 120,
29. validateOptions: true
30. }
31.
32. let connectString = configuration.mongodb.development.connectionString
33. mongoose.connect(connectString, opts, function(err){
34. if (err) throw err;
35. console.log("==> Conexión establecida con MongoDB");
36. })
37.
38. app.use('*', require('cors')());
39.
40. app.use('/public', express.static(__dirname + '/public'))
41. app.use(bodyParser.urlencoded({extended: false}))
42. app.use(bodyParser.json({limit:'10mb'}))
43.
44. if(process.env.NODE_ENV !== 'production'){
45. app.use(require('webpack-dev-middleware')(compiler, {
46. noInfo: true,
47. publicPath: config.output.publicPath
48. }))
49. }
50.
51. app.use(vhost('api.*', api));
52.
53. app.get('/*', function (req, res) {
54. res.sendFile(path.join(__dirname, 'index.html'))
55. });
56.
57. https.createServer({
58. key: fs.readFileSync('./certs/key.pem'),
59. cert: fs.readFileSync('./certs/cert.pem'),
60. passphrase: '1234'
61. }, app).listen(443, () => {
62. console.log('Example app listening on port 443!')
63. });
64.
65.
66. http.createServer(function (req, res) {
67. res.writeHead(301, {
68. "Location": "https://" + req.headers['host'] + req.url })
69. res.end()
70. }).listen(80);
71. }
72.
73. if(require.main === module){
74. startServer();
75. } else {
76. module.exports = startServer;
77. }
De las líneas 16 a 71 hemos agregado todo el inicio del server dentro de la función
startServer, después en las líneas 73 a 77 definimos la forma en que se debe de
ejecutar el server. Cuando un archivo es ejecutado directamente mediante el
comando node, se establece el varlo require.main = ‘module’, por lo que si
ejecutamos esta clase directamente (node server.js) simplemente se ejecutará
la función de inicio del server. Por otra parte, si el archivo es ejecutado por otro
Página | 534
archivo, entonces solamente exportamos la función startServer para ser
utilizada por fuera.
Por otra parte, registramos el evento “exit”, el cual nos permitirá realizar una
acción cuando un servidor se apague, en tal caso. Mandamos un mensaje en
pantalla y volvemos a ejecutar la función startWorker para reponer la instancia
del server que fallo.
Ahora bien. La función startWorker tiene como finalidad ejecutar el método fork,
el cual inicia un nuevo proceso, ejecutando de nuevo este archivo pero con una
diferencia, y es que ahora cluster.isMaster será igual a false, lo que hará que
se ejecute la función startServer del archivo server.js (línea 22).
535 | Página
Finalmente actualizaremos los script del archivo package.json para remplazar el
archivo server.js por cluster.js:
1. "scripts": {
2. "build": "webpack -p",
3. "server": "NODE_ENV=production&&node cluster.js",
4. "server_windows": "set NODE_ENV=production&&node cluster.js",
5. "start": "npm run build && npm run server_windows"
6. },
Tras ejecutar el cluster, podemos observar cómo se han iniciado 8 procesos, pues
tengo un equipo con 8 cpus. Si vamos al administrador de tareas, podremos ver
varios procesos, los cuales corresponden a cada instancia del cluster + los
procesos propios del cluster:
Página | 536
Fig. 226 - Cluster process.
Una vez en los procesos, probemos con terminar algún, tiendo cuidado de no
matar el proceso del cluster, el cual podemos distinguir porque es el que menos
memoria consume:
Hosting y dominios
Una vez que toda nuestra aplicación ha sido configurada para operar en
producción, solo nos restaría conseguir un Hosting y un dominio apropiado para
nuestra aplicación.
537 | Página
Una de las herramientas que utilizo para comprobar la disponibilidad de un
dominio es https://who.is/, en la cual, solo tenemos que poder el dominio que
queremos y nos arrojará si está disponible o no, y de no estar disponible, te da
datos del actual dueño, por si quisieras contactarlo para negociar la compra.
Desde aquí mismo te permite hacer la compra del dominio, sin embargo, en mi
experiencia, tiene los costos más altos del mercado, por lo que yo recomiendo
comprarlos a google directamente en https://domains.google.
Para asegurar que el API sigue funcionando en nuestro dominio, tendremos que
modificar el vhost del archivo server.js:
1. app.use(vhost('api.<domain>', api));
Una vez que tiene tanto el dominio como el hosting, tendrás que configurar el
DNS apuntar tu dominio a tu servidor. Por suerte, DigitalOcean también tiene el
servicio de DNS sin ningún costo.
npm install
npm start
Página | 538
Resumen
En este capítulo hemos aprendido como llevar efectivamente una aplicación hasta
producción. Optimizando muestro aplicación para darle una mejor experiencia al
usuario.
En este punto, hemos terminado por completo todas las unidades de este libro y
hemos aprendido a crear aplicaciones reactivas con React, NodeJS y MongoDB y
hemos aprendido desde cómo crear un Hello World en React, hasta crear toda
una API REST con persistencia en MongoDB.
Sin duda, creo que, tras finalizar este libro, no deberías de tener problemas para
iniciarte con un proyecto real, pues has aprendido todas las fases del desarrollo
de una aplicación con React, desde el FrontEnd hasta el BackEnd, incluso,
pasando por la persistencia con MongoDB y el pase a producción.
539 | Página
CONCLUSIONES
¡Felicidades! Si estas leyendo estas líneas, es por que seguramanete has
concluido de leer este libro, lo que te combierte en un programador Full Stack
capaz de desarrollar una aplicación completa utilizando React, NodeJS, Express
y MongoDB (Stack MERN).
Como ya te habras dado cuenta a lo largo de este libro, desarrollar una aplicación
completa, requiera de varias tecnologías, las cuales, por lo general son
enseñadas de forma independiente, dejando al programador la tarea de
investigar como unir todas las tecnologías para crear una solo aplicación
funcional.
GRACIAS.
Página | 540