Introducción
En el post anterior vimos en que consistía el Server Side Rendering a nivel de teoría y conceptos. En este segundo post de la serie vamos a pasar a fase de implementación. Para ello, hemos elegido como tecnología React y como framework de server side rendering Nextjs.
Qué es Nextjs
Implementar Server Side Rendering en nuestro sitio web desde cero es una tarea bastante dura: algo así como si quisiéramos desarrollar web sin tirar de ningún framework. En nuestra ayuda viene Nextjs que nos proporciona un buen número de desafíos resueltos, haciéndonos fácil la tarea de dar soporte a renderizado en servidor.
Un tema importante a destacar es que Nextjs es una solución que toma partido tanto en cliente como en servidor, es decir te verás haciendo cosas tales como configurar Express para manejar rutas SEO friendly, por ejemplo.
Un listado de características que nos aporta Nextjs podría ser el siguiente:
- Navegación en cliente y servidor.
- Creación de URL's SEO friendly.
- Ejecución de llamadas a REST API desde cliente y servidor.
- Gestión de estilos CSS.
- Manejo del estado de Redux.
- Manejo de proxies para llamadas a API REST.
- ...
Ejemplo
Para mostrar los conceptos que vamos a cubrir implementaremos una aplicación simple que visualice la lista de usuarios miembros de un grupo de GitHub y su detalle. Para ello:
- Tiramos de las API REST que ofrece Github.
- Mostramos una ventana inicial con miembros de una organizacion (Lemoncode).
- Cuando pinchas sobre uno de los miembros del listado la aplicación navega a una página de detalle.
Lo que vamos a cubrir en este post de la serie:
- Cómo hacer un "Hola Mundo".
- Cómo configurar TypeScript para que funcione con este framework.
- Cómo gestionar la navegación entre páginas.
- Cómo hacer llamadas a API's REST.
- Cómo pasar parámetros por Querystring.
- Cómo crear URL's SEO friendly.
- Cómo implementar una página de visualización de detalle.
Hola Next
En este primer ejemplo vamos a montar lo mínimo para mostrar un "Hola Mundo" por pantalla.
El ejemplo completo con su guía paso a paso para reproducirlo puedes encontrarlo en la siguiente url: https://github.com/Lemoncode/nextjs-typescript-by-sample/tree/master/00-hello-next
Lo primero que hacemos es inicializar nuestro proyecto. Consejo: hazlo dentro de una subcarpeta cuyo nombre no tenga espacios ni mayúsculas.
npm init -y
El siguiente paso es instalar las librerías que vamos a usar: react, react-dom, y next.
npm install react react-dom next --save
Ahora nos toca añadir un comando para ejecutar nuestro sitio en local, lo añadimos al package.json
"scripts": { "dev": "next", "test": "echo \"Error: no test specified\" && exit 1" },
En el raíz del proyecto creamos una carpeta que vamos a llamar pages y bajo esa carpeta crearemos nuestras páginas. En concreto, creamos una sóla página que llamaremos index.js
./pages/index.js
const Index = () => ( <div> <p>Hello Next.js</p> </div> ); export default Index;
Aquí lo único que hacemos es instancia un componente que pinta un párrafo "Hello next.js".
Ahora desde la línea de comandos podemos ejecutar el ejemplo:
Y obtenemos el siguiente resultado:
La esctructura de carpetas que nos queda es la siguiente:
... Vale esto está "muy bonito" ¿ Pero que hemos ganado al montar esta aplicación con Nextjs? Pues si nos vamos a las devtools de nuestro navegador podemos ver cómo la página que se nos sirvío ya venía montada desde servidor (en una aplicación normal nos vendría el JSX listo para ejecutarse en el navegador).
Principales take aways de este ejemplo:
- Nextjs se encarga de hacer el render en servidor de la primera página que pidamos.
- Nextjs trae por defecto una convención de carpetas.
Hola TypeScript
Antes de seguir desarrollando nuestra aplicación, vamos a darle soporte a Typescript (los que seáis de ES6 podéis saltaros este ejemplo :)). Ésta suele ser una prueba de fuego para ver que nivel de madurez tiene una librería.
El ejemplo completo con su guía paso a paso para reproducirlo lo puedes encontrar en la siguiente url: https://github.com/Lemoncode/nextjs-typescript-by-sample/tree/master/01-hello-typescript
Partiríamos del ejemplo anterior, si directamente habéis llegado a este ejemplo podéis copiaros el ejemplo 00 de la ruta que se indicó en el apartado anterior y hacer un npm install para aseguraros que tenemos todas las dependencias de terceros instaladas en local.
Nextjs nos ofrece un plugin que ya trae configurado el soporte a typescript. Lo podemos instalar desde la linea de comandos:
npm install @zeit/next-typescript --save
También instalamos los typings de React:
npm install @types/react --save-dev
Una vez instalado configuramos nuestro tsconfig.json (la configuración del transpilador de typescript para nuestro proyecto):
./tsconfig.json
{ "compileOnSave": false, "compilerOptions": { "target": "esnext", "module": "esnext", "jsx": "preserve", "allowJs": true, "moduleResolution": "node", "allowSyntheticDefaultImports": true, "noUnusedLocals": true, "noUnusedParameters": true, "removeComments": false, "preserveConstEnums": true, "sourceMap": true, "skipLibCheck": true, "baseUrl": ".", "lib": [ "dom", "es2016" ] } }
Ahora tocaría el momento de añadir el plugin de typescript que trae next en nuestra configuración de webpack (si, nextjs usa por debajo webpack), peeero.... ¿Donde este el webpack.config para tocarlo? Nextjs nos lo oculta, pero podemos extenderlo creando un fichero que tenga el nombre next.config.js
./next.config.js
const withTypescript = require('@zeit/next-typescript') module.exports = withTypescript({ webpack(config, options) { return config } })
Ahora podemos cambiar la extensión de nuestra página index.jsx a index.tsx y modificar el fichero para utiliza Typescript:
./pages/index.tsx
const myLanguage : string = "Typescript"; const Index = () => ( <div> <p>Hello Next.js</p> <p>From {myLanguage}</p> </div> ); export default Index;
El plugin de TypeScript de nextjs usa bajo el capó Babel 7 para realizar la transpilación. Tenemos que añadir un fichero de configuración para babel:
./.babelrc
{ "presets": [ "next/babel", "@zeit/next-typescript/babel" ] }
Ya sólo nos queda ejecutar y ver que la aplicación sigue funcionando sin errores:
npm run dev
La esctructura de carpetas que nos queda es la siguiente:
Estructura de carpetas
Takeaways de este ejemplo:
- Nextjs trae un plugin que nos permite configurar fácilmente el soporte a Typescript.
- Nextjs monta por debajo webpack y como mecanismo de extensión. Nos ofrece un punto de entrada (fichero next.config.js) para mezclar nuestras configuraciones custom con las que ya trae de por si el framework.
Navegación
Empezamos con la parte interesante: vamos a añadir una segunda página a nuestra aplicación web, y montar una enlace para navegar entre una página y otra.
Aquí el comportamiento que queremos es:
- Si tengo la página A ya cargada en mi navegador y pulso en el link de la página B, quiero que:
- Si no se había cargado previamente la página B que la cargue en servidor.
- Si ya se había cargado, que se monte la página desde cliente.
- Si directamente pongo en la barra del navegador a la página B, quiero que el navegador vaya a servidor a pedir la página B, ésta se renderice en servidor y me de el HTML ya montado (esta solución es SEO friendly).
El ejemplo completo con su guía paso a paso para reproducirlo puedes encontrar en la siguiente url: https://github.com/Lemoncode/nextjs-typescript-by-sample/tree/master/02-navigation
Partiríamos del ejemplo anterior, si directamente habéis llegado a este ejemplo podéis copiaros el ejemplo 01 de la ruta que se indicó en el apartado anterior y hacer un npm install para asegurarnos que tenemos todas las dependencias de terceros instaladas en local.
Vamos a crear una página nueva debajo de pages, en este caso llamaremos al fichero user-info :
./pages/user-info.tsx
const UserInfoPage = () => ( <div> <h2>I'm the user info page</h2> </div> ); export default UserInfoPage;
Ahora volvemos a la página inicial y añadimos un enlace para poder navegar a la página que acabamos de crear. Aquí viene la parte interesante: si añadimos un anchor (tag <a>) tal cual, al pinchar en él siempre nos llevaría al servidor a pedir la página. Nosotros no queremos que haga ese comportamiento, sino todo lo contrario, que sirva la página desde cliente y la renderice en cliente. Sólo en el caso de que pidiéramos por primera vez la página desde la barra del navegador si querríamos que se renderizara en servidor ¿Cómo podemos diferenciar en nextjs entre enlaces que tienen que gestionarse en cliente y otros que por cualquier razón naveguen a otra página de servidor (aunque no estén dentro de nuestra aplicación React)? Para ello nextjs nos trae el componente Link que hace ese trabajo por nosotros. Envolvemos el anchor que hemos creado con nuestro Link:
./pages/index.tsx
import Link from 'next/link'; const myLanguage = "Typescript"; const Index = () => ( <div> <p>Hello Next.js</p> <p>From {myLanguage}</p> <Link href="/user-info"> <a>Navigate to user info page</a> </Link> </div> ); export default Index;
Así cuando pinchemos en el enlace, Link se encargara de manejar la navegación a la url correspondiente.
Ahora podemos ejecutar el ejemplo y vemos el resultado:
npm run dev
Navegando desde cliente
... Eeeeh, veo la navegación peeerooo... ¿De verdad no está yendo a servidor siempre? Veamos como trabaja Nextjs:
- La primera página se carga desde servidor (sólo carga esa página).
- Vamos a navegar a la segunda, al no estar cargada en cliente, si se la trae de servidor.
- Si volvemos a navegar a la página inicial ya no cargará nada de servidor (esta página ya esta disponible en cliente).
- Si pinchamos para navegar a la segunda página tampoco cargará nada debido a que ya cargó ese bundle en cliente.
Abramos las devtools de nuestro navegador y veamos ese comportamiento:
Carga bajo demanda
La esctructura de carpetas que nos queda es la siguiente:
Estructura de carpetas navegación
Takeaways de este ejemplo:
- Podemos ir creando más páginas de nuestra aplicación siempre que las pongamos debajo de la carpeta pages.
- Para hacer que un enlace en nuestro HTML no vaya a servidor, lo envolvemos con el componente Link que nos ofrece nextjs.
- Las páginas se van cargando bajo demanda, una vez cargadas en cliente no se vuelven a pedir a servidor.
Haciendo llamadas a API's REST
En toda aplicación web que se precie es normal que realicemos llamadas AJAX a API's Rest, es decir, queremos cargar datos para poder visualizarlos en en pantalla (dame la lista de clientes, la línea de detalle de un pedido, ...). En un escenario normal, esto es pan comido con React, ¿Qué solemos hacer?
- Nos enganchamos al ComponentDidMount
- Dentro ejecutamos la llamada ajax en cuestión (un fetch o igual tenemos ese código aislado en una función o clase).
- Nos quedamos escuchando a la resolución de la promesa de ese fetch para hacer un SetState.
De primeras, ¿encaja esto en el modelo de server side rendering? Imagínate que estamos pintando la página en el servidor, aquí cambia la cosa:
- La página se pinta y "muere", lo que devolvemos es un HTML ya montado (ya el navegador se encarga de que se muestre y engancha correctamente con el JSX).
- En este caso el ComponentDidMount se ejecutaría pero no tendríamos manera de esperar a que se resolviera la promesa.
- La página se serviría sin datos.
Nos hace falta un punto de entrada más especial para esto, estaría muy bien tener un método asíncrono que:
- Al ejecutarse en servidor nos permitiera esperar a que se resolvieran las promesas correspondientes y ya poder dibujar la página.
- Al ejecutarse en cliente nos permitiese lanzar las llamadas AJAX oportunas sin bloquear la hebra del navegador. Así, una vez resueltas las promesas, los datos deben pintarse.
Y eso en Nextjs se llama getInitialProps:
- Este es un método más que se añade al flujo de vida de nuestro componente React.
- Es un método asíncrono, que establece una espera hasta que terminen una serie de llamadas asíncronas.
- Este método nos permite que se lance en servidor y se suspenda el renderizado hasta que todas las promesas que hay en ese método se hayan resuelto.
- En cliente actuaría de forma parecida a un ComponentDidMount.
Resumiendo, es un método isomórfico, que en servidor nos permite parar la tubería de renderizado hasta tener los datos, y en cliente también se ejecuta pero permite a nuestra aplicación React seguir funcionando.
En este ejemplo vamos a pedirle a la API Rest de Github la lista de miembros que forman Lemoncode y a mostrar dicha lista de usuarios por pantalla.
El ejemplo completo con su guía paso a paso para reproducirlo puedes encontrar en la siguiente url: https://github.com/Lemoncode/nextjs-typescript-by-sample/tree/master/03-fetch
Empezamos:
Partiríamos del ejemplo anterior, si directamente habéis llegado a este ejemplo podéis copiaros el ejemplo 02 de la ruta que se indicó en el apartado anterior y hacer un npm install para asegurarnos que tenemos todas las dependencias de terceros instaladas en local.
Para poder hacer fetch tanto desde nodejs como desde el navegador nos vamos a bajar una librería que trae soporte para esto: isomorphic_unfetch. El autor de esta librería es Jason Miller, creador de preact. Abrimos la consola de comandos y ejecutamos:
npm install isomorphic-unfetch --save
Como estamos trabajando con TypeScript vamos a tipar nuestro modelo (la entidad de miembro), para lo que utilizaremos interfaces. Una ventaja de utilizar interfaces en vez de clases es que una vez que transpilemos los interfaces se eliminan, no añaden peso al bundle JavaScript que generemos.
./model/user.ts
export interface UserEntity { login: string; id: number; avatar_url: string; }
Vamos a aislar el acceso a la api rest en un fichero aparte. Aunque este código que vamos a ver está más relacionado con cómo se desarrolla con JavaScript moderno que con Server Side Rendering creo que merece la pena que nos paremos un poco y lo comprendamos (si estás al día en ES6/ES7 y TypeScript te lo puedes saltar) ¿Qué es lo que hacemos en este método?
- Importamos el interfaz de UserEntity que hemos creado previamente. Esto nos aporta tipado, es cómodo para manejarlo en nuestra aplicación, pero es algo opcional.
- Importamos también el fetch isomórfico de la librería que previamente hemos instalado. Esto nos permite compartir implementación de fetch tanto para nodejs como para navegador.
- Añadimos una constante para no ir harcodeando url's que apunten a la API Rest de Github.
- Creamos un método asíncrono, indicándole que pause su ejecución hasta que se resuelvan las promesas que se ejecutan dentro de esa función (las marcaremos con await). Más info sobre async /await: http://lemoncode.net/lemoncode-blog/2018/1/29/javascript-asincrono
- Ejecutamos el fetch contra la API de Github e indicamos que suspenda la ejecución de esta función hasta que no se resuelva (esto lo hacemos en dos pasos).
- Una vez que ya tenemos los datos en crudo, utilizando la función map para devolver un array tipado con los resultados. Si queréis saber más sobre map, reduce y otras hierbas os recomendamos este post: http://lemoncode.net/lemoncode-blog/2017/6/22/javascript-es6-no-mas-bucles-for.
./rest-api/github.ts
import {UserEntity} from '../model/user'; import fetch from 'isomorphic-unfetch'; const baseRoot = 'https://api.github.com'; const userCollectionURL = `${baseRoot}/orgs/lemoncode/members` export const getUserCollection = async () => { const res = await fetch(userCollectionURL) const data = await res.json(); return data.map( ({id, login, avatar_url,}) => ({ id, login, avatar_url, } as UserEntity) ); }
Aquí viene lo importante, vamos a añadir a nuestro componente index el método getInitialProps, en el que haremos la petición AJAX. Vayamos paso a paso:
Primero vamos a añadir unos imports. Algunos están más relacionados con TypeScript (queremos tener tipado de getInitialProps y también traernos el modelo que hemos creado) y otro con el método de acceso a API getUserCollection que hemos creado en el paso anterior.
./pages/index.ts
import * as React from 'react'; import Link from 'next/link'; import * as Next from 'next'; import { getUserCollection } from '../rest-api/github'; import { UserEntity } from '../model/user';
A continuación añadimos una propiedad que va a tener la lista de miembros. Por otro lado, vamos a tipar nuestro componente para que TypeScript reconozca el getInitialProps.
./pages/index.ts
interface Props { userCollection: UserEntity[], } const Index : Next.NextSFC<Props> = (props) => ( <div> <p>Hello Next.js</p> <Link href="/user-info"> <a>Navigate to user info page</a> </Link> </div> );
Muy bien, peeeroooo... ¿ Donde está el getInitialProps? Lo hemos dejado para el último paso, al final del mismo fichero añadimos la función, en ella le indicamos que:
- Es asíncrona.
- Realizamos el fetch de usuario, y esperamos a que se resuelva (await).
- Devolvemos un objeto en el que indicamos que propiedades son las que se van a asignar del componente afectado.
./pages/index.ts
Index.getInitialProps = async () => { const data = await getUserCollection(); console.log(data); return { userCollection: data, } } export default Index;
Un tema interesante a destacar: si ejecutamos la aplicación podemos ver como en la consola donde hemos hecho el npm run dev (sea un bash o cmd) se muestra en la misma la lista de usuarios. Es decir, se está ejecutando desde servidor (no aparece en la consola del navegador). Si quieres darle al coco, más abajo la solución ¿En que caso aparecería en la consola del navegador en vez de en la de servidor?
Muy bonito... pero yo no veo que se muestre nada en la página index, ¿Me estáis tomando el pelo? En absoluto :). Vamos con la parte de la visualización. Aquí no implementamos código específico de server side rendering (esto es, React estándar). Podríamos haber implementado la visualización de los componentes con un map y pocas líneas, pero hemos preferido componentizarlo para que veáis como de limpio y mantenible queda el código con un poquito más de trabajo.
Nos ponemos manos a la obra: lo que queremos mostrar es una tabla(*) con el listado de usuarios. Esta tabla la podemos descomponer en:
- Un componente que pinta la cabecera de la tabla.
- Un componente que sabe pintar una fila de la tabla.
- Un componente padre que aglutina la cabecera y pinta la fila. Será responsable de pintar un componente fila por cada miembro de la lista.
(*) Si estáis al día con los interfaces de usuarios más modernos, habréis dicho... ¡vaya! una tabla, que cosa más antigua :). Lo ideal sería montar una lista con tarjetas (cards). Si alguien se anima, que haga un fork del proyecto y que cree un ejemplo 07 con diseño moderno (Cards, material ui...). Por nuestra parte, encantados de revisar esa pull request y añadirla al respositorio.
Antes de ponernos a crear componentes hay que tener en cuenta un tema muy importante, ¿Donde los colocamos?: Lo natural que nos sale es ponerlos debajo de la carpeta pages, esto tiene un gran "pero", nextjs va a pensar que dichos components son páginas, es decir si intentamos acceder a la ruta de un subcomponente en vez de darnos un 404 va a intentar renderizarlos. La solución que le damos es que components queda como carpeta hermana de pages
Primero vamos a crear el componente que mostrará la cabecera de la tabla:
./components/user-collection/header.tsx
import * as React from 'react'; export const UserHeader = () => <tr> <th> Avatar </th> <th> Id </th> <th> Name </th> </tr>
Ahora el que muestra una fila de la tabla. Aquí recibimos con propiedad un usuario y mostramos su foto, id de usuario y nombre:
./components/user-collection/row.tsx
<p>Hello, World!</p>import * as React from 'react'; import { UserEntity } from 'model/user'; interface Props { user: UserEntity; } export const UserRow = (props: Props) => <tr> <td> <img src={props.user.avatar_url} style={{ maxWidth: '10rem' }} /> </td> <td> <span>{props.user.id}</span> </td> <td> <span>{props.user.login}</span> </td> </tr>
Creamos el componente de la tabla completa (aglutina el de cabecera y cuerpo que creamos antes):
./components/user-collection/user-table.tsx
import { UserEntity } from "model/user"; import { UserHeader } from "./header"; import { UserRow } from "./row"; interface Props { userCollection: UserEntity[], } export const UserTable = (props : Props) => <table> <thead> <UserHeader /> </thead> <tbody> { props.userCollection.map((user: UserEntity) => <UserRow user={user} key={user.id} /> ) } </tbody> </table>
Para exponer hacia afuera lo estrictamente necesario, vamos a crear un fichero index.js que exportará sólo los componentes que queramos hacer públicos.
./components/user-collection/index.ts
export {UserTable} from './user-table';
Y lo instanciamos en nuestra página Index.js.
Añadimos el import correspondiente:
./pages/index.tsx
import {UserTable} from '../components/user-collection';
Le damos uso en el componente Index:
./pages/index.tsx
const Index : Next.NextSFC<Props> = (props) => ( <div> <p>Hello Next.js</p> <UserTable userCollection={props.userCollection}/> <Link href="/user-info"> <a>Navigate to user info page</a> </Link> </div> )
Ahora si. Si ejecutamos, podemos ver la lista de usuarios:
npm run dev
Vale, me habéis dicho que getInitialProps se ejecutaba en servidor y cliente, pero sólo hemos visto lanzándose en server ¿Cómo podría verlo lanzándose en cliente? Muy fácil, arranca la aplicación y pincha en el enlace para navegar a la página de detalle. Abre las devtools (consola del navegador), dale al botón de back del navegador, podrás ver cómo ahora los datos de los miembros aparecen en la consola del navegador en vez de la consola de comandos. En este caso, Nextjs detecta que la aplicación ya se está ejecutando en cliente y hace la petición desde cliente ¿A que está chulo?
Petición fetch en servidor en primera carga (F5), petición fetch en cliente una vez que la página está disponible
Peeero el getInitialProps guarda los datos en propiedades ¿Qué pasa si quiero guardar en el estado del componente? Muy buena pregunta. La mayoría de veces que usemos server side rendering es para orientarlo a SEO, lo que suele traducirse en mostrar datos de sólo lectura (por ejemplo un listado de hoteles, la ficha de un hotel...). Los datos de lectura/escritura (los candidatos a estar en el state) los podemos cargar de forma normal. Aún así, si quieres cargarlos en el state, aquí tienes una propuesta: https://stackoverflow.com/questions/51134001/nextjs-getinitialprops-getderivedstatefromprops
La esctructura de carpetas que nos queda es la siguiente:
Takeaways de este ejemplo:
- getIntialProps nos permite lanzar código en servidor y esperar a que se completen llamadas asíncronas para tirar el render, y también se puede ejecutar en cliente.
- Si es la primera página que se sirve en el cliente (por ejemplo has tecleado la URL en la barra del navegador o le has dado a F5), getInitialProps se ejecuta en servidor y espera a que terminen de completarse todas las promesas para tirar el render y servir la página ya montada al cliente.
- Si ya está la aplicación React corriendo en cliente y la página destino a la que nos dirigimos ya se ha cargado previamente (por ejemplo navegamos de una página a otra), getInitialProps se ejecuta desde cliente. No hace falta ir a servidor para que se renderize la página.
- Aunque es algo que no tiene que ver con server side rendering:
- Si no conocías async / await en JavaScript es bueno que le eches un ojo.
- Si también es nuevo para ti map, reduce, filter... es bueno que le eches un ojo.
Pasando parametros por QueryString
Algo muy común cuando desarrollamos un sitio web es que queramos pasar parámetros por la url (QueryString) de una página a otra. Por ejemplo: si estoy mostrando un listado de hoteles y el usuario pincha en ir al detalle de un hotel ,puedo pasar el id del hotel por QueryString y así la página destino tienen la información necesaria para poder pedir datos sin tener que recurrir a artificios.
En este caso vamos a pasar a la página de detalle del usuario el login del usuario seleccionado (con eso podemos tirar de la API REST de Github y sacar el detalle). Es decir, la URL de nuestra aplicación a la que navegaríamos tendría la siguiente pinta:
http://localhost:3000/user-info?login=brauliodiez
En este caso el componente de navegación (Link) que hemos usado anteriormente nos permitiría meter los parámetros y el HOC que trae la librería de Nextjs, WithRouter ,nos permitirá leer esos parámetros en la página destino.
El ejemplo completo con su guía paso a paso para reproducirlo lo puedes encontrar en la siguiente url: https://github.com/Lemoncode/nextjs-typescript-by-sample/tree/master/04-querystring
Nos ponemos manos a la obra:
Partiríamos del ejemplo anterior, si directamente habéis llegado a este ejemplo podéis copiaros el ejemplo 03 de la ruta que se indicó en el apartado anterior y hacer un npm install para asegurarnos que tenemos todas las dependencias de terceros instaladas en local.
En el componente fila de la tabla vamos a añadir un import para trabajar con el componente Link:
./components/user-collection/row.tsx
import * as React from 'react'; import { UserEntity } from 'model/user'; import Link from 'next/link';
Añadimos un Link para navegar a la página de detalle:
./components/user-collection/row.tsx
<td> <Link href={`/user-info?login=${props.user.login}`}> <span>{props.user.login}</span> <a>{props.user.login}</a> </Link> </td>
Siguiente paso: en la página de user-info leer del querystring el parámetro que hemos pasado y mostrarlo. Para ello:
- Importamos el HOC (withRouter) que trae next para poder leer datos de rutas.
- Envolvemos nuestro componente con dicho HOC, y así, automáticamente en las propiedades nos inyecta las propiedades del QueryString.
./pages/user-info-tsx
import {withRouter} from 'next/router'; const UserInfoPage = withRouter((props) => ( <div> <h2>I'm the user info page</h2> <h3>{props.router.query.id}</h3> </div> ));
Ya podemos ejecutar y probar lo implementado:
- Página principal.
- Pinchamos en uno de los enlaces de miembro.
- Vemos como navegamos a la página de detalle y se nos muestra el parametro que nos venía en el QueryString.
Navegación pasando parametros por el querystring
La esctructura de carpetas que nos queda es la siguiente:
Estructura de carpetas
Takeaways de este ejemplo:
- El componente Link que trae Nextjs nos permite crear rutas con parámetros.
- El HOC withRouter que trae Nextjs nos permite inyectar a un componente las propiedades de Querystring que trae la ruta actual de nuestra página.
Creando URL's SEO friendly
Muy chulo lo del querystring, pero... le he enseñado esto al experto en SEO (el mismo que me amarga la vida cambiando headings, layouts...) y me dice que esa URL no le vale, que a Google no le gusta :-( ¿Qué hacemos? Nextjs al rescate, vamos al lío...
Para crear URL's amigables tenemos que tener en cuenta dos escenarios:
- Cliente: El estándar de una aplicación SPA es que desde cliente se navegue de una página a otra. Creamos un alias en el routing de turno que se implemente en cliente.
- Este caso sería el típico en el que estoy viendo un listado de hoteles y pincho para ver el detalle de uno.
- Aquí nos apoyamos en el componente Link que trae una propiedad "as" para crear sinónimos.
- Servidor: El caso en que nuestra ruta llegue directamente a servidor y tengamos que manejar mapeos de alias de servidor:
- Este caso sería: un amigo nos envía la ruta a un hotel en concreto para que ver que nos parece.
- Aquí nos metemos con Express y en el prepare antes de hacer un get(*) comprobaremos si la URL es un alias para pasarle el nombre normal.
Oye ¿ Ya estáis con cosas "hippies" de nodejs? Yo tengo mi corazoncito Microsoft y sólo lo hago con IIS ¿Qué pasa conmigo?
- Lo primero plantéate separar lo que es API REST de servidor (que seguirían funcionando con .Net) de lo que es el sitio de front end (ese correría con Express). Aunque te quedaras con IIS es bueno hacer esta separación, en el fondo son mundos diferentes y cada uno tiene sus necesidades diferentes.
- Lo segundo: en principio hay fontanería que te permite correr nodejs desde IIS. Eso lleva en funcionamiento desde el año 2011. Agradecería mucho que alguien con experiencia en este tema nos comentara impresiones en los comentarios del siguiente post:
- Instalando y ejecutando nodejs en IIS: https://www.hanselman.com/blog/InstallingAndRunningNodejsApplicationsWithinIISOnWindowsAreYouMad.aspx
En este ejemplo vamos a cambiar la URL no amigable:
http://localhost:3000/user-info?login=brauliodiez
Por esta:
http://localhost:3000/user-info/login/brauliodiez
El ejemplo completo con su guía paso a paso para reproducirlo puedes encontrar en la siguiente url: https://github.com/Lemoncode/nextjs-typescript-by-sample/tree/master/05-friendly-url
Empezamos:
Partiríamos del ejemplo anterior, si directamente habéis llegado a este ejemplo podéis copiaros el ejemplo 04 de la ruta que se indicó en el apartado anterior y hacer un npm install para asegurarnos que tenemos todas las dependencias de terceros instaladas en local.
Hacer que en el cliente usemos un alias es cuestión de cambiar una línea de código cuando instanciamos el HOC Link en el componente que muestra un usuario. Simplemente añadimos un atributo as y le definimos la ruta alias que vamos a usar:
./components/user-collection/row.tsx
<td> <Link as={`user-info/login/${props.user.login}`} href={`/user-info?login=${props.user.login}`}> <a>{props.user.login}</a> </Link> </td>
Si ahora ejecutamos, vemos que la cosa funciona. Pincho en el enlace y me navega a mi url amistosa:
Oye muy bonito pero si le doy un F5 al navegador o pego la URL en otro navegador me da un 404 de la muerte... Correcto, nos queda configurar la parte servidora, vamos a ello.
Nos instalamos Express:
Vamos a crear el fichero server.js donde arrancamos y configuramos nuestro servidor
- Le añadimos Express.
- Resolvemos cualquier entrada que nos llegue (va al sistema de ficheros y busca el ficheor que cuadre con la URL).
- Nos quedamos escuchando en el puerto 3000.
./server.js
const express = require('express') const next = require('next') const dev = process.env.NODE_ENV !== 'production' const app = next({ dev }) const handle = app.getRequestHandler() app.prepare() .then(() => { const server = express() server.get('*', (req, res) => { return handle(req, res) }) server.listen(3000, (err) => { if (err) throw err console.log('> Ready on http://localhost:3000') }) }) .catch((ex) => { console.error(ex.stack) process.exit(1) })
En ese mismo código, justo antes del server.get('*'), vamos a meter el caso para tratar nuestro alias:
./server.js
app.prepare() .then(() => { const server = express() // Aquí definimos el alias // just antes del server.get('*' server.get('/user-info/login/:login', (req, res) => { const actualPage = '/user-info'; const queryParams = {login: req.params.login}; app.render(req, res, actualPage, queryParams); }) server.get('*', (req, res) => { return handle(req, res) })
Nos falta cambiar el package.json para decirle que explicitamente arranque con el server.js que acabamos de crear. Ya no tiramos en dev de npm run next, si no que lanzamos node server.js
"scripts": { "dev": "node server.js", "test": "echo \"Error: no test specified\" && exit 1" },
Si ahora ejecutamos, ya tenemos nuestro alias a prueba de F5 y copiar/pegar rutas en otro navegador :)
npm run dev
Friendly URL
La estructura de carpetas que nos queda es la siguiente:
Estructura de carpetas
Takeaways:
- Configurar URL's amistosas (alias) es fácil en el lado cliente, es cuestión de añadir un parámetro más al componente Link que trae Nextjs.
- Para configurar las URL's amistosas en el lado de servidor, nos hace falta un poco más de trabajo: instalar Express y añadir el código para tratarlas.
Implementando una página de detalle
Vamos a por el último paso: que nuestra página de detalle tire una query contra la API REST de Github y nos de el detalle del usuario seleccionado. Esto parece fácil, ¿ Vamos a aprender algo nuevo en este ejemplo? Sí, ademas de asentar conocimientos, vamos a ver cómo leer los parámetros de querystring de nuestra url desde getInitialProps.
En este ejemplo vamos a trabajar con la página de user-info:
- Crearemos una interfaz más en el modelo de datos de API para tener tipada la información detallada de un usuario.
- Extendemos el fichero en el que accedemos a la API de github y añadiremos una entrada más para leer el detalle de un usuario.
- En el componente user-info utilizamos getInitialProps y leemos los datos del usuario que queremos mostrar (accediendo a la función de API que hemos creados en el paso anterior).
- En el componente user-info mostramos los datos que hemos cargado.
Vamos al lío:
El ejemplo completo con su guía paso a paso para reproducirlo puedes encontrar en la siguiente url: https://github.com/Lemoncode/nextjs-typescript-by-sample/tree/master/06-detail-page
Empezamos:
Partiríamos del ejemplo anterior. Si habéis llegado a este ejemplo directamente, podéis copiaros el ejemplo 05 de la ruta que se indicó en el apartado anterior y hacer un npm install para asegurarnos que tenemos todas las dependencias de terceros instaladas en local.
Vamos a crear una nueva entrada en nuestro modelo de api. Es decir, crear un nuevo tipo en Typescript. De este modo, añadimos tipado y obtenemos ayuda en el IDE como intellisense o detección de errores si nos equivocamos al teclear el nombre de una propiedad:
./model/user-detail.ts
export interface UserDetailEntity { login: string; id: number; avatar_url: string; name : string; company : string; followers : string; }
Vamos a extender nuestro fichero de consumo de API con la función que lee de la lista de detalle:
Primero añadimos el import a la nueva entidad:
./rest-api/github.ts
import { UserDetailEntity } from '../model/user-detail';
Después añadimos la nueva función (y el nuevo endpoint):
./rest-api/github.ts
const userDetailsURL = `${baseRoot}/users`; export const getUserDetail = async (userlogin: string) : Promise<UserDetailEntity> => { const fullUserDetailURL = `${userDetailsURL}/${userlogin}`; const res = await fetch(fullUserDetailURL) const data = await res.json(); console.log(data); const { id, login, avatar_url, name, company, followers } = data; return { id, login, avatar_url, name, company, followers }; }
Si no has podido ponerte al día con JavaScript moderno (ES6, / ES7), puede que haya un par de cosas que te suene a chino en este código:
La primera:
const { id, login, avatar_url, name, company, followers } = data;
Esto se llama destructuring. Esa línea de código es equivalente a esta (más trabajo para tirar estas líneas y más fácil que te equivoques):
const id = data.id; const login = data.login; const avatar_url = data.avatar_url; const name = data.name; const company = data.company; const followers = data.followers;
La segunda:
const { id, login, avatar_url, name, company, followers } = data; return { id, login, avatar_url, name, company, followers };
En ES6, si el nombre de la variable que vas a asignar a una propiedad es la misma no tienes que repetirla, es decir, este código sería el mismo que (lo mismo, más fácil equivocarse si lo programas de la manera antigua):
const { id, login, avatar_url, name, company, followers } = data; return { id: id, login: login, avatar_url: avatar_url, name: name, company: company, followers: followers };
Un punto a favor de Typescript es que tiene en cuenta todos estos temas.
Si quieres saber más de estos tópicos tenemos un webinar gratuito sobre qué cosas nuevas trae ES6: https://www.youtube.com/watch?v=ytpqRmkiAkQ
Hasta aquí todo como siempre, vamos al turrón. En nuestro componente de user-info vamos a importar la entidad y la funcíon que llama a la REST API para leer el detalle de un usuario:
./pages/user-info.tsx
import { getUserDetail } from '../rest-api/github'; import { UserDetailEntity } from '../model/user-detail';
Vamos crear un interfaz con las propiedades que necesitamos:
./pages/user-info.tsx
interface Props { userLogin : string; userDetail : UserDetailEntity; }
Vamos a tipar el componente y mostrar los datos que corresponden, a tener en cuenta:
- Todavía no cargamos los datos (lo haremos con el getInitialProps).
- Para usar una sintaxis más cómoda, renombramos el componente como innerUserInfoPage y lo envolvemos con el HOC de WithRouter (UserInfoPage). Esto lo veremos en el siguiente paso.
const InnerUserInfoPage : Next.NextSFC<Props> = (props) => ( <div> <h2>I'm the user info page</h2> <p>User ID Selected: {props.userLogin}</p> <img src={props.userDetail.avatar_url} style={{ maxWidth: '10rem' }} /> <p>User name: {props.userDetail.name}</p> <p>Company: {props.userDetail.company}</p> <p>Followers: {props.userDetail.followers}</p> </div> );
Implementamos el getInitialProps, aquí:
- Leemos del QueryString el valor de la propiedad login.
- Tiramos de la API de Github, para que nos de el detalle del usuario que hemos recogido en el paso anterior (este método es asíncrono y esperamos a la respuesta).
- Devolvemos un objeto con las propiedades afectadas (que se mapearán al objeto Props del componente). Ojo, esta vez hemos querido devolver dos propiedades para que veáis que se puede devolver más de una.
- Wrapeamos nuestro InnerUserInfoPage para añadirle el HOC withRouter de navegación (el resultado de esto lo llamaremos UserInfoPage).
InnerUserInfoPage.getInitialProps = async (data) => { const query = data.query; const login = query.login as string; const userDetail = await getUserDetail(login); return { userLogin: login, userDetail } } const UserInfoPage = withRouter(InnerUserInfoPage);
Y ya lo tenemos... vamos a probar a ver que pasa:
npm run dev
La esctructura de carpetas que nos queda es la siguiente:
Estructura de carpetas
Takeaways:
- En el getInitialProps tenemos a nuestra disposición los parámetro del QueryString de la URL que estamos accediendo.
- Si no has tenido oportunidad de ponerte al día con ES6 es bueno que te pongas a ello, vas a ganar mucho en productividad.
Conclusiones
Server Side Rendering es un topic duro de digerir, aquí van unos consejos:
- Antes de ponerte a implementar, entiende muy bien los conceptos y para que se utiliza. En la primera parte de este post damos una introducción: http://lemoncode.net/lemoncode-blog/2018/5/13/server-side-rendering-i-conceptos.
- Antes de implantarlo en un proyecto real:
- Asegúrate que resuelve un problema de tu negocio y es realmente necesario añadirlo. Volviendo a los ejemplos de los hoteles:
- Para la parte de la web que muestra el listado de hoteles y detalle de cada uno, podrías plantearte usar un CMS, o tirar de un interfaz potente realizado con React y usando Server Side Rendering (necesitas soporte a SEO).
- Para la parte del motor de reservas (cuando ya vas a reservar una habitación), o la intranet de administración, en principio no me plantearía utilizar server side rendering (eso no lo rastrea un navegador), salvo que hubiera unos requisitos de performance en primera carga.
- Juega y crea ejemplos pequeños para ver que tienes todos los flancos cubiertos.
- Asegúrate que resuelve un problema de tu negocio y es realmente necesario añadirlo. Volviendo a los ejemplos de los hoteles:
- Busca una librería popular como Nextjs que te resuelva la mayoría de desafíos. Reinventar la rueda puede ser divertido y se aprende mucho si te lo tomas como un ejercicio para tirar luego a basura. Para aplicar a código en producción no suele ser buena idea.
Sobre Nextjs:
- Es una librería sólida y battle tested, va por su versión 6.
- Nos resuelve un montón de problemas.
- Nos tenemos que ajustar a lo que ofrece tanto en servidor como cliente.
En próximas entregas
¿Ha terminado la serie en este post? Me quedan un montón de dudas y lagunas sobre como utilizar esta librería... Para nada, estamos planteando siguientes entregas, temas que cubriremos:
- Cómo depurar en el lado servidor.
- Cómo desplegar un proyecto.
- Cómo estilar:
- Utlizando Layouts.
- Trabajando con CSS.
- Trabajando con CSS IN JS.
- Integrando Redux en Server Side Rendering.
Esperamos ir sacando cabeza en las próximas semanas e ir publicando nuevos posts. Gracias por haber llegado hasta aquí , espero que te haya resultado útil.
Agradecimientos
Este post cuenta con un revisor de lujo: Erik Rasmussen, autor de propuestas y proyectos open source tales como: Redux-Form, Ducks Modular Redux, React Final Forms.
¿Con ganas de aprender desarrollo Front End?
Si tienes ganas de ponerte al día en el mundo del Front End, impartimos un máster online con clases en vivo. Para más información: http://lemoncode.net/master-frontend